Merge pull request #7 from phranck/feature/container-config

refactor: Extract ContainerConfig and renderContainer helper (B.3)
This commit is contained in:
phranck
2026-01-30 14:16:01 +01:00
5 changed files with 231 additions and 226 deletions
+33 -56
View File
@@ -42,17 +42,8 @@ public struct Alert<Actions: View>: View {
/// The alert message.
public let message: String
/// The border style for the alert box (nil uses appearance default).
public let borderStyle: BorderStyle?
/// The border color.
public let borderColor: Color?
/// The title color.
public let titleColor: Color?
/// Whether to show a separator before the action buttons.
public let showFooterSeparator: Bool
/// The shared visual configuration.
public let config: ContainerConfig
/// The action views (typically buttons).
public let actions: Actions
@@ -78,10 +69,13 @@ public struct Alert<Actions: View>: View {
) {
self.title = title
self.message = message
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.showFooterSeparator = showFooterSeparator
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: EdgeInsets(horizontal: 2, vertical: 1),
showFooterSeparator: showFooterSeparator
)
self.actions = actions()
}
@@ -94,44 +88,24 @@ public struct Alert<Actions: View>: View {
extension Alert: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
let containerStyle = ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: showFooterSeparator,
borderStyle: borderStyle,
borderColor: borderColor
)
// Check if actions is EmptyView (no actions)
let hasActions = !(actions is EmptyView)
if hasActions {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: EdgeInsets(horizontal: 2, vertical: 1)
) {
Text(message)
} footer: {
actions
}
return container.renderToBuffer(context: context)
} else {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: false,
borderStyle: borderStyle,
borderColor: borderColor
),
padding: EdgeInsets(horizontal: 2, vertical: 1)
) {
Text(message)
}
return container.renderToBuffer(context: context)
}
let effectiveConfig = hasActions
? config
: ContainerConfig(
borderStyle: config.borderStyle,
borderColor: config.borderColor,
titleColor: config.titleColor,
padding: config.padding,
showFooterSeparator: false
)
return renderContainer(
title: title,
config: effectiveConfig,
content: Text(message),
footer: hasActions ? actions : nil,
context: context
)
}
}
@@ -155,10 +129,13 @@ extension Alert where Actions == EmptyView {
) {
self.title = title
self.message = message
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.showFooterSeparator = false
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: EdgeInsets(horizontal: 2, vertical: 1),
showFooterSeparator: false
)
self.actions = EmptyView()
}
}
+31 -60
View File
@@ -52,24 +52,12 @@ public struct Card<Content: View, Footer: View>: View {
/// The footer content (optional).
public let footer: Footer?
/// The border style (nil uses appearance default).
public let borderStyle: BorderStyle?
/// The border color.
public let borderColor: Color?
/// The title color.
public let titleColor: Color?
/// The shared visual configuration.
public let config: ContainerConfig
/// The background color (nil for transparent).
public let backgroundColor: Color?
/// The padding inside the card.
public let padding: EdgeInsets
/// Whether to show a separator before the footer.
public let showFooterSeparator: Bool
/// Creates a card with all options including footer.
///
/// - Parameters:
@@ -96,12 +84,14 @@ public struct Card<Content: View, Footer: View>: View {
self.title = title
self.content = content()
self.footer = footer()
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: showFooterSeparator
)
self.backgroundColor = backgroundColor
self.padding = padding
self.showFooterSeparator = showFooterSeparator
}
public var body: Never {
@@ -113,13 +103,6 @@ public struct Card<Content: View, Footer: View>: View {
extension Card: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
let containerStyle = ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: showFooterSeparator,
borderStyle: borderStyle,
borderColor: borderColor
)
// Wrap content with background if specified
let bodyContent: AnyView
if let bgColor = backgroundColor {
@@ -127,30 +110,14 @@ extension Card: Renderable {
} else {
bodyContent = AnyView(content)
}
if let footerView = footer {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: padding
) {
bodyContent
} footer: {
footerView
}
return container.renderToBuffer(context: context)
} else {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: padding
) {
bodyContent
}
return container.renderToBuffer(context: context)
}
return renderContainer(
title: title,
config: config,
content: bodyContent,
footer: footer,
context: context
)
}
}
@@ -179,12 +146,14 @@ extension Card where Footer == EmptyView {
self.title = title
self.content = content()
self.footer = nil
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: false
)
self.backgroundColor = backgroundColor
self.padding = padding
self.showFooterSeparator = false
}
}
@@ -211,11 +180,13 @@ extension Card where Footer == EmptyView {
self.title = nil
self.content = content()
self.footer = nil
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = nil
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: nil,
padding: padding,
showFooterSeparator: false
)
self.backgroundColor = backgroundColor
self.padding = padding
self.showFooterSeparator = false
}
}
+123 -1
View File
@@ -5,6 +5,65 @@
// A unified container component with Header/Body/Footer architecture.
//
// MARK: - Container Config
/// Shared visual configuration for container-type views.
///
/// Groups the common appearance properties used by ``Alert``, ``Dialog``,
/// ``Panel``, and ``Card``. Each view stores a `ContainerConfig` instead
/// of repeating the same five properties.
///
/// # Example
///
/// ```swift
/// let config = ContainerConfig(
/// borderStyle: .doubleLine,
/// borderColor: .cyan,
/// titleColor: .cyan
/// )
/// ```
public struct ContainerConfig: Sendable {
/// The border style (nil uses appearance default).
public var borderStyle: BorderStyle?
/// The border color (nil uses theme default).
public var borderColor: Color?
/// The title color (nil uses theme accent).
public var titleColor: Color?
/// The inner padding for the body content.
public var padding: EdgeInsets
/// Whether to show a separator line between body and footer.
public var showFooterSeparator: Bool
/// Creates a container configuration.
///
/// - Parameters:
/// - borderStyle: The border style (default: appearance default).
/// - borderColor: The border color (default: theme border).
/// - titleColor: The title color (default: theme accent).
/// - padding: The inner padding (default: horizontal 1, vertical 0).
/// - showFooterSeparator: Show separator before footer (default: true).
public init(
borderStyle: BorderStyle? = nil,
borderColor: Color? = nil,
titleColor: Color? = nil,
padding: EdgeInsets = EdgeInsets(horizontal: 1, vertical: 0),
showFooterSeparator: Bool = true
) {
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.padding = padding
self.showFooterSeparator = showFooterSeparator
}
/// Default configuration.
public static let `default` = ContainerConfig()
}
// MARK: - Container Style
/// Configuration options for container appearance.
@@ -44,11 +103,74 @@ public struct ContainerStyle: Sendable {
self.borderStyle = borderStyle
self.borderColor = borderColor
}
/// Creates a `ContainerStyle` from a ``ContainerConfig``.
///
/// - Parameter config: The container configuration to use.
public init(from config: ContainerConfig) {
self.showHeaderSeparator = true
self.showFooterSeparator = config.showFooterSeparator
self.borderStyle = config.borderStyle
self.borderColor = config.borderColor
}
/// Default container style.
public static let `default` = ContainerStyle()
}
// MARK: - Render Helper
/// Renders a ``ContainerView`` from a ``ContainerConfig`` and content/footer views.
///
/// Eliminates the duplicated `if/else` footer pattern found in Alert, Dialog,
/// Panel, and Card.
///
/// - Parameters:
/// - title: The container title (optional).
/// - config: The shared visual configuration.
/// - content: The body content view.
/// - footer: The footer view (optional).
/// - context: The current render context.
/// - Returns: The rendered frame buffer.
internal func renderContainer<Content: View, Footer: View>(
title: String?,
config: ContainerConfig,
content: Content,
footer: Footer?,
context: RenderContext
) -> FrameBuffer {
let containerStyle = ContainerStyle(from: config)
if let footerView = footer {
let container = ContainerView(
title: title,
titleColor: config.titleColor,
style: containerStyle,
padding: config.padding
) {
content
} footer: {
footerView
}
return container.renderToBuffer(context: context)
} else {
let container = ContainerView(
title: title,
titleColor: config.titleColor,
style: ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: false,
borderStyle: config.borderStyle,
borderColor: config.borderColor
),
padding: config.padding
) {
content
}
return container.renderToBuffer(context: context)
}
}
// MARK: - Container View
/// A unified container with optional header, body, and footer sections.
+22 -55
View File
@@ -60,20 +60,8 @@ public struct Dialog<Content: View, Footer: View>: View {
/// The footer content (typically buttons).
public let footer: Footer?
/// The border style (nil uses appearance default).
public let borderStyle: BorderStyle?
/// The border color.
public let borderColor: Color?
/// The title color.
public let titleColor: Color?
/// The inner padding.
public let padding: EdgeInsets
/// Whether to show a separator before the footer.
public let showFooterSeparator: Bool
/// The shared visual configuration.
public let config: ContainerConfig
/// Creates a dialog with content and footer.
///
@@ -97,11 +85,13 @@ public struct Dialog<Content: View, Footer: View>: View {
@ViewBuilder footer: () -> Footer
) {
self.title = title
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.padding = padding
self.showFooterSeparator = showFooterSeparator
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: showFooterSeparator
)
self.content = content()
self.footer = footer()
}
@@ -115,36 +105,13 @@ public struct Dialog<Content: View, Footer: View>: View {
extension Dialog: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
let containerStyle = ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: showFooterSeparator,
borderStyle: borderStyle,
borderColor: borderColor
renderContainer(
title: title,
config: config,
content: content,
footer: footer,
context: context
)
if let footerView = footer {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: padding
) {
content
} footer: {
footerView
}
return container.renderToBuffer(context: context)
} else {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: padding
) {
content
}
return container.renderToBuffer(context: context)
}
}
}
@@ -169,11 +136,13 @@ extension Dialog where Footer == EmptyView {
@ViewBuilder content: () -> Content
) {
self.title = title
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.padding = padding
self.showFooterSeparator = false
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: false
)
self.content = content()
self.footer = nil
}
@@ -228,5 +197,3 @@ extension Dialog where Footer == EmptyView {
)
}
}
+22 -54
View File
@@ -53,20 +53,8 @@ public struct Panel<Content: View, Footer: View>: View {
/// The footer content (typically buttons).
public let footer: Footer?
/// The border style (nil uses appearance default).
public let borderStyle: BorderStyle?
/// The border color.
public let borderColor: Color?
/// The title color.
public let titleColor: Color?
/// The padding inside the panel body.
public let padding: EdgeInsets
/// Whether to show a separator before the footer.
public let showFooterSeparator: Bool
/// The shared visual configuration.
public let config: ContainerConfig
/// Creates a panel with content and footer.
///
@@ -92,11 +80,13 @@ public struct Panel<Content: View, Footer: View>: View {
self.title = title
self.content = content()
self.footer = footer()
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.padding = padding
self.showFooterSeparator = showFooterSeparator
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: showFooterSeparator
)
}
public var body: Never {
@@ -127,11 +117,13 @@ extension Panel where Footer == EmptyView {
self.title = title
self.content = content()
self.footer = nil
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.padding = padding
self.showFooterSeparator = true
self.config = ContainerConfig(
borderStyle: borderStyle,
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: true
)
}
}
@@ -139,36 +131,12 @@ extension Panel where Footer == EmptyView {
extension Panel: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
// Create the ContainerView and render it
let containerStyle = ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: showFooterSeparator,
borderStyle: borderStyle,
borderColor: borderColor
renderContainer(
title: title,
config: config,
content: content,
footer: footer,
context: context
)
if let footerView = footer {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: padding
) {
content
} footer: {
footerView
}
return container.renderToBuffer(context: context)
} else {
let container = ContainerView(
title: title,
titleColor: titleColor,
style: containerStyle,
padding: padding
) {
content
}
return container.renderToBuffer(context: context)
}
}
}