diff --git a/Sources/TUIKit/Views/Alert.swift b/Sources/TUIKit/Views/Alert.swift index c61de8c..425408e 100644 --- a/Sources/TUIKit/Views/Alert.swift +++ b/Sources/TUIKit/Views/Alert.swift @@ -42,17 +42,8 @@ public struct Alert: 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: 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: 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() } } diff --git a/Sources/TUIKit/Views/Card.swift b/Sources/TUIKit/Views/Card.swift index bbd3d75..7a1d5f1 100644 --- a/Sources/TUIKit/Views/Card.swift +++ b/Sources/TUIKit/Views/Card.swift @@ -52,24 +52,12 @@ public struct Card: 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: 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: 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 } } diff --git a/Sources/TUIKit/Views/ContainerView.swift b/Sources/TUIKit/Views/ContainerView.swift index f6245e3..27645cc 100644 --- a/Sources/TUIKit/Views/ContainerView.swift +++ b/Sources/TUIKit/Views/ContainerView.swift @@ -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( + 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. diff --git a/Sources/TUIKit/Views/Dialog.swift b/Sources/TUIKit/Views/Dialog.swift index b9086b4..c77e5fc 100644 --- a/Sources/TUIKit/Views/Dialog.swift +++ b/Sources/TUIKit/Views/Dialog.swift @@ -60,20 +60,8 @@ public struct Dialog: 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: 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: 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 { ) } } - - diff --git a/Sources/TUIKit/Views/Panel.swift b/Sources/TUIKit/Views/Panel.swift index 16fba27..67196ab 100644 --- a/Sources/TUIKit/Views/Panel.swift +++ b/Sources/TUIKit/Views/Panel.swift @@ -53,20 +53,8 @@ public struct Panel: 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: 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) - } } }