From ce850e1b2960c4600868b51c9d2ffe0b06ebab0c Mon Sep 17 00:00:00 2001 From: phranck Date: Sat, 14 Feb 2026 03:14:14 +0100 Subject: [PATCH] Refactor: Extract TUIkitStyling and TUIkitImage modules - Extract 12 pure type definitions (Color, Palette, Appearance, BorderStyle, etc.) into TUIkitStyling module with no dependencies - Extract 3 image processing files (RGBAImage, ImageLoader, ASCIIConverter) into TUIkitImage module (deps: CSTBImage, TUIkitStyling) - Split 6 mixed files into type definitions (TUIkitStyling) and environment glue (TUIkit/Styling/) - Decouple ThemeManager from AppState via renderTrigger closure - Decouple ASCIIConverter from ANSIRenderer via local ANSIEscape constants - Add @_exported imports in Exports.swift for backward compatibility - All 1064 tests pass, no breaking API changes --- Package.swift | 17 ++++- Sources/TUIkit/App/App.swift | 4 +- Sources/TUIkit/Exports.swift | 9 +++ .../Styling/AppearanceEnvironment.swift | 59 +++++++++++++++ Sources/TUIkit/Styling/ColorEnvironment.swift | 55 ++++++++++++++ .../TUIkit/Styling/PaletteEnvironment.swift | 59 +++++++++++++++ .../Styling/TextContentTypeEnvironment.swift | 53 ++++++++++++++ .../Styling/TextCursorStyleEnvironment.swift | 71 +++++++++++++++++++ .../Styling/ViewConstants+EdgeInsets.swift | 15 ++++ .../ASCIIConverter.swift | 34 ++++++--- .../Image => TUIkitImage}/ImageLoader.swift | 30 ++++---- .../Image => TUIkitImage}/RGBAImage.swift | 34 ++++----- .../Styling => TUIkitStyling}/ANSIColor.swift | 14 ++-- .../Appearance.swift | 58 +-------------- .../BorderStyle.swift | 0 .../Styling => TUIkitStyling}/Color.swift | 54 +------------- .../ContentMode.swift | 0 .../Palettes/PalettePreset.swift | 0 .../SemanticColor.swift | 2 +- .../TextContentType.swift | 48 +------------ .../TextCursorStyle.swift | 66 +---------------- .../Styling => TUIkitStyling}/Theme.swift | 60 ++-------------- .../ThemeManager.swift | 22 +++--- .../ViewConstants.swift | 30 +++----- 24 files changed, 436 insertions(+), 358 deletions(-) create mode 100644 Sources/TUIkit/Exports.swift create mode 100644 Sources/TUIkit/Styling/AppearanceEnvironment.swift create mode 100644 Sources/TUIkit/Styling/ColorEnvironment.swift create mode 100644 Sources/TUIkit/Styling/PaletteEnvironment.swift create mode 100644 Sources/TUIkit/Styling/TextContentTypeEnvironment.swift create mode 100644 Sources/TUIkit/Styling/TextCursorStyleEnvironment.swift create mode 100644 Sources/TUIkit/Styling/ViewConstants+EdgeInsets.swift rename Sources/{TUIkit/Image => TUIkitImage}/ASCIIConverter.swift (95%) rename Sources/{TUIkit/Image => TUIkitImage}/ImageLoader.swift (91%) rename Sources/{TUIkit/Image => TUIkitImage}/RGBAImage.swift (88%) rename Sources/{TUIkit/Styling => TUIkitStyling}/ANSIColor.swift (83%) rename Sources/{TUIkit/Styling => TUIkitStyling}/Appearance.swift (74%) rename Sources/{TUIkit/Styling => TUIkitStyling}/BorderStyle.swift (100%) rename Sources/{TUIkit/Styling => TUIkitStyling}/Color.swift (91%) rename Sources/{TUIkit/Styling => TUIkitStyling}/ContentMode.swift (100%) rename Sources/{TUIkit/Styling => TUIkitStyling}/Palettes/PalettePreset.swift (100%) rename Sources/{TUIkit/Styling => TUIkitStyling}/SemanticColor.swift (97%) rename Sources/{TUIkit/Styling => TUIkitStyling}/TextContentType.swift (79%) rename Sources/{TUIkit/Styling => TUIkitStyling}/TextCursorStyle.swift (71%) rename Sources/{TUIkit/Styling => TUIkitStyling}/Theme.swift (66%) rename Sources/{TUIkit/Styling => TUIkitStyling}/ThemeManager.swift (87%) rename Sources/{TUIkit/Styling => TUIkitStyling}/ViewConstants.swift (57%) diff --git a/Package.swift b/Package.swift index b4096e8e..c8ab9dfa 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,14 @@ let package = Package( name: "TUIkit", targets: ["TUIkit"] ), + .library( + name: "TUIkitStyling", + targets: ["TUIkitStyling"] + ), + .library( + name: "TUIkitImage", + targets: ["TUIkitImage"] + ), .executable( name: "TUIkitExample", targets: ["TUIkitExample"] @@ -30,9 +38,16 @@ let package = Package( name: "CSTBImage", publicHeadersPath: "include" ), + .target( + name: "TUIkitStyling" + ), + .target( + name: "TUIkitImage", + dependencies: ["CSTBImage", "TUIkitStyling"] + ), .target( name: "TUIkit", - dependencies: ["CSTBImage"] + dependencies: ["TUIkitStyling", "TUIkitImage"] ), .executableTarget( name: "TUIkitExample", diff --git a/Sources/TUIkit/App/App.swift b/Sources/TUIkit/App/App.swift index 3d5ffc77..98fdfd3d 100644 --- a/Sources/TUIkit/App/App.swift +++ b/Sources/TUIkit/App/App.swift @@ -87,8 +87,8 @@ internal final class AppRunner { self.appHeader = AppHeaderState() self.focusManager = FocusManager() self.tuiContext = TUIContext() - self.paletteManager = ThemeManager(items: PaletteRegistry.all, appState: appState) - self.appearanceManager = ThemeManager(items: AppearanceRegistry.all, appState: appState) + self.paletteManager = ThemeManager(items: PaletteRegistry.all, renderTrigger: { [appState] in appState.setNeedsRender() }) + self.appearanceManager = ThemeManager(items: AppearanceRegistry.all, renderTrigger: { [appState] in appState.setNeedsRender() }) // Configure status bar style self.statusBar.style = .bordered diff --git a/Sources/TUIkit/Exports.swift b/Sources/TUIkit/Exports.swift new file mode 100644 index 00000000..cf67545c --- /dev/null +++ b/Sources/TUIkit/Exports.swift @@ -0,0 +1,9 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// Exports.swift +// +// Created by LAYERED.work +// License: MIT + +// Re-export sub-modules so `import TUIkit` provides full API access. +@_exported import TUIkitStyling +@_exported import TUIkitImage diff --git a/Sources/TUIkit/Styling/AppearanceEnvironment.swift b/Sources/TUIkit/Styling/AppearanceEnvironment.swift new file mode 100644 index 00000000..dbc15d7b --- /dev/null +++ b/Sources/TUIkit/Styling/AppearanceEnvironment.swift @@ -0,0 +1,59 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// AppearanceEnvironment.swift +// +// Created by LAYERED.work +// License: MIT + +import TUIkitStyling + +// MARK: - Appearance Environment Key + +/// Environment key for the current appearance. +private struct AppearanceKey: EnvironmentKey { + static let defaultValue: Appearance = .default +} + +extension EnvironmentValues { + /// The current appearance. + /// + /// Set an appearance at the app level and it propagates to all child views: + /// + /// ```swift + /// WindowGroup { + /// ContentView() + /// } + /// .appearance(.rounded) + /// ``` + /// + /// Access the appearance in `renderToBuffer(context:)`: + /// + /// ```swift + /// let appearance = context.environment.appearance + /// let borderStyle = appearance.borderStyle + /// ``` + public var appearance: Appearance { + get { self[AppearanceKey.self] } + set { self[AppearanceKey.self] = newValue } + } +} + +// MARK: - AppearanceManager Environment Key + +/// Environment key for the appearance manager. +private struct AppearanceManagerKey: EnvironmentKey { + static let defaultValue = ThemeManager(items: AppearanceRegistry.all) +} + +extension EnvironmentValues { + /// The appearance manager for cycling and setting appearances. + /// + /// ```swift + /// let appearanceManager = context.environment.appearanceManager + /// appearanceManager.cycleNext() + /// appearanceManager.setCurrent(Appearance.rounded) + /// ``` + public var appearanceManager: ThemeManager { + get { self[AppearanceManagerKey.self] } + set { self[AppearanceManagerKey.self] = newValue } + } +} diff --git a/Sources/TUIkit/Styling/ColorEnvironment.swift b/Sources/TUIkit/Styling/ColorEnvironment.swift new file mode 100644 index 00000000..b8cda6a9 --- /dev/null +++ b/Sources/TUIkit/Styling/ColorEnvironment.swift @@ -0,0 +1,55 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// ColorEnvironment.swift +// +// Created by LAYERED.work +// License: MIT + +import TUIkitStyling + +// MARK: - Foreground Style Environment + +/// Environment key for the foreground style. +/// +/// When set via `.foregroundStyle(_:)` on any View, this value propagates +/// down through the view hierarchy. Child views can read it from the +/// render context to apply the color. +private struct ForegroundStyleKey: EnvironmentKey { + static let defaultValue: Color? = nil +} + +extension EnvironmentValues { + /// The foreground style (color) for text and other content. + /// + /// Set via `.foregroundStyle(_:)` modifier on any View. + /// Returns `nil` if not explicitly set (use palette default). + public var foregroundStyle: Color? { + get { self[ForegroundStyleKey.self] } + set { self[ForegroundStyleKey.self] = newValue } + } +} + +// MARK: - View Extension for foregroundStyle + +extension View { + /// Sets the foreground style for this view and its children. + /// + /// The style propagates through the view hierarchy via the environment. + /// Child views that render text or other colored content should read + /// `context.environment.foregroundStyle` and apply it. + /// + /// ## Example + /// + /// ```swift + /// VStack { + /// Text("Red text") + /// Text("Also red") + /// } + /// .foregroundStyle(.red) + /// ``` + /// + /// - Parameter style: The color to apply as foreground style. + /// - Returns: A view with the foreground style set. + public func foregroundStyle(_ style: Color?) -> some View { + environment(\.foregroundStyle, style) + } +} diff --git a/Sources/TUIkit/Styling/PaletteEnvironment.swift b/Sources/TUIkit/Styling/PaletteEnvironment.swift new file mode 100644 index 00000000..1f86a7e8 --- /dev/null +++ b/Sources/TUIkit/Styling/PaletteEnvironment.swift @@ -0,0 +1,59 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// PaletteEnvironment.swift +// +// Created by LAYERED.work +// License: MIT + +import TUIkitStyling + +// MARK: - Palette Environment Key + +/// Environment key for the current palette. +private struct PaletteKey: EnvironmentKey { + static let defaultValue: any Palette = SystemPalette(.green) +} + +extension EnvironmentValues { + /// The current palette. + /// + /// Set a palette at the app level and it propagates to all child views: + /// + /// ```swift + /// WindowGroup { + /// ContentView() + /// } + /// .environment(\.palette, SystemPalette(.green)) + /// ``` + /// + /// Access the palette in `renderToBuffer(context:)`: + /// + /// ```swift + /// let palette = context.environment.palette + /// let fg = palette.foreground + /// ``` + public var palette: any Palette { + get { self[PaletteKey.self] } + set { self[PaletteKey.self] = newValue } + } +} + +// MARK: - PaletteManager Environment Key + +/// Environment key for the palette manager. +private struct PaletteManagerKey: EnvironmentKey { + static let defaultValue = ThemeManager(items: PaletteRegistry.all) +} + +extension EnvironmentValues { + /// The palette manager for cycling and setting palettes. + /// + /// ```swift + /// let paletteManager = context.environment.paletteManager + /// paletteManager.cycleNext() + /// paletteManager.setCurrent(SystemPalette(.amber)) + /// ``` + public var paletteManager: ThemeManager { + get { self[PaletteManagerKey.self] } + set { self[PaletteManagerKey.self] = newValue } + } +} diff --git a/Sources/TUIkit/Styling/TextContentTypeEnvironment.swift b/Sources/TUIkit/Styling/TextContentTypeEnvironment.swift new file mode 100644 index 00000000..6c112f13 --- /dev/null +++ b/Sources/TUIkit/Styling/TextContentTypeEnvironment.swift @@ -0,0 +1,53 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// TextContentTypeEnvironment.swift +// +// Created by LAYERED.work +// License: MIT + +import TUIkitStyling + +// MARK: - Environment Key + +/// Environment key for the text content type. +private struct TextContentTypeKey: EnvironmentKey { + static let defaultValue: TextContentType? = nil +} + +extension EnvironmentValues { + /// The text content type for text fields. + /// + /// When set, text fields filter both typed characters and pasted text + /// against the allowed character set of the content type. + /// + /// Set this value using the `.textContentType(_:)` modifier: + /// + /// ```swift + /// TextField("URL", text: $url) + /// .textContentType(.url) + /// ``` + public var textContentType: TextContentType? { + get { self[TextContentTypeKey.self] } + set { self[TextContentTypeKey.self] = newValue } + } +} + +// MARK: - View Extension + +extension View { + /// Sets the text content type for text fields within this view. + /// + /// When a content type is set, both typed characters and pasted text + /// are filtered against the allowed character set. Invalid characters + /// are silently dropped. + /// + /// ```swift + /// TextField("URL", text: $url) + /// .textContentType(.url) + /// ``` + /// + /// - Parameter type: The content type, or `nil` to disable filtering. + /// - Returns: A view with the content type applied. + public func textContentType(_ type: TextContentType?) -> some View { + environment(\.textContentType, type) + } +} diff --git a/Sources/TUIkit/Styling/TextCursorStyleEnvironment.swift b/Sources/TUIkit/Styling/TextCursorStyleEnvironment.swift new file mode 100644 index 00000000..04d0ccad --- /dev/null +++ b/Sources/TUIkit/Styling/TextCursorStyleEnvironment.swift @@ -0,0 +1,71 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// TextCursorStyleEnvironment.swift +// +// Created by LAYERED.work +// License: MIT + +import TUIkitStyling + +// MARK: - Environment Key + +/// Environment key for the text cursor style. +private struct TextCursorStyleKey: EnvironmentKey { + static let defaultValue = TextCursorStyle() +} + +extension EnvironmentValues { + /// The text cursor style for text fields. + /// + /// Set this value using the `.textCursor(_:)` modifier: + /// + /// ```swift + /// TextField("Name", text: $name) + /// .textCursor(.bar, animation: .blink) + /// ``` + public var textCursorStyle: TextCursorStyle { + get { self[TextCursorStyleKey.self] } + set { self[TextCursorStyleKey.self] = newValue } + } +} + +// MARK: - View Extension + +extension View { + /// Sets the text cursor style for text fields within this view. + /// + /// Use this modifier to customize the cursor appearance in ``TextField`` + /// and ``SecureField`` components. + /// + /// ```swift + /// TextField("Name", text: $name) + /// .textCursor(.bar) + /// ``` + /// + /// - Parameter style: The cursor style to use. + /// - Returns: A view with the cursor style applied. + public func textCursor(_ style: TextCursorStyle) -> some View { + environment(\.textCursorStyle, style) + } + + /// Sets the text cursor style with separate shape, animation, and speed parameters. + /// + /// Use this modifier when you want to specify shape, animation, and speed: + /// + /// ```swift + /// TextField("Code", text: $code) + /// .textCursor(.underscore, animation: .blink, speed: .fast) + /// ``` + /// + /// - Parameters: + /// - shape: The cursor shape. + /// - animation: The cursor animation. Defaults to `.pulse`. + /// - speed: The animation speed. Defaults to `.regular`. + /// - Returns: A view with the cursor style applied. + public func textCursor( + _ shape: TextCursorStyle.Shape, + animation: TextCursorStyle.Animation = .pulse, + speed: TextCursorStyle.Speed = .regular + ) -> some View { + environment(\.textCursorStyle, TextCursorStyle(shape: shape, animation: animation, speed: speed)) + } +} diff --git a/Sources/TUIkit/Styling/ViewConstants+EdgeInsets.swift b/Sources/TUIkit/Styling/ViewConstants+EdgeInsets.swift new file mode 100644 index 00000000..e754205a --- /dev/null +++ b/Sources/TUIkit/Styling/ViewConstants+EdgeInsets.swift @@ -0,0 +1,15 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// ViewConstants+EdgeInsets.swift +// +// Created by LAYERED.work +// License: MIT + +// MARK: - EdgeInsets Defaults + +extension EdgeInsets { + /// Default insets for containers (Card, Panel, etc.): 1 horizontal, 0 vertical. + static let containerDefault = EdgeInsets(horizontal: 1, vertical: 0) + + /// Default insets for dialogs: 2 horizontal, 1 vertical. + static let dialogDefault = EdgeInsets(horizontal: 2, vertical: 1) +} diff --git a/Sources/TUIkit/Image/ASCIIConverter.swift b/Sources/TUIkitImage/ASCIIConverter.swift similarity index 95% rename from Sources/TUIkit/Image/ASCIIConverter.swift rename to Sources/TUIkitImage/ASCIIConverter.swift index 56824f52..41223916 100644 --- a/Sources/TUIkit/Image/ASCIIConverter.swift +++ b/Sources/TUIkitImage/ASCIIConverter.swift @@ -4,6 +4,18 @@ // Created by LAYERED.work // License: MIT +import TUIkitStyling + +/// Standard ANSI escape sequences for ASCII art colorization. +private enum ANSIEscape { + /// The escape character. + static let escape = "\u{1B}" + /// The Control Sequence Introducer. + static let csi = "\(escape)[" + /// Reset all formatting. + static let reset = "\(csi)0m" +} + // MARK: - Character Set /// The set of characters used for ASCII art rendering. @@ -58,7 +70,7 @@ public enum DitheringMode: Sendable, Equatable { /// 3. Optionally apply dithering /// 4. Map each pixel to a character based on luminance /// 5. Colorize each character using the selected color mode -struct ASCIIConverter: Sendable { +public struct ASCIIConverter: Sendable { /// The character set to use for brightness mapping. let characterSet: ASCIICharacterSet @@ -70,7 +82,7 @@ struct ASCIIConverter: Sendable { let dithering: DitheringMode /// Creates a converter with the specified options. - init( + public init( characterSet: ASCIICharacterSet = .blocks, colorMode: ASCIIColorMode = .trueColor, dithering: DitheringMode = .none @@ -92,7 +104,7 @@ extension ASCIIConverter { /// - width: Target width in characters. /// - height: Target height in characters. /// - Returns: An array of ANSI-formatted strings representing the ASCII art. - func convert(_ image: RGBAImage, width: Int, height: Int) -> [String] { + public func convert(_ image: RGBAImage, width: Int, height: Int) -> [String] { guard image.width > 0, image.height > 0, width > 0, height > 0 else { return [] } @@ -153,7 +165,7 @@ extension ASCIIConverter { let colorCode = foregroundColorCode(for: pixel) if colorCode != lastColor { if !lastColor.isEmpty { - line += ANSIRenderer.reset + line += ANSIEscape.reset } line += colorCode lastColor = colorCode @@ -162,7 +174,7 @@ extension ASCIIConverter { } if !lastColor.isEmpty { - line += ANSIRenderer.reset + line += ANSIEscape.reset } lines.append(line) } @@ -259,7 +271,7 @@ extension ASCIIConverter { let colorCode = foregroundColorCode(for: avgPixel) if colorCode != lastColor { if !lastColor.isEmpty { - line += ANSIRenderer.reset + line += ANSIEscape.reset } line += colorCode lastColor = colorCode @@ -268,7 +280,7 @@ extension ASCIIConverter { } if !lastColor.isEmpty { - line += ANSIRenderer.reset + line += ANSIEscape.reset } lines.append(line) } @@ -285,16 +297,16 @@ extension ASCIIConverter { private func foregroundColorCode(for pixel: RGBA) -> String { switch colorMode { case .trueColor: - return "\(ANSIRenderer.csi)38;2;\(pixel.r);\(pixel.g);\(pixel.b)m" + return "\(ANSIEscape.csi)38;2;\(pixel.r);\(pixel.g);\(pixel.b)m" case .ansi256: let index = quantizeToANSI256(pixel) - return "\(ANSIRenderer.csi)38;5;\(index)m" + return "\(ANSIEscape.csi)38;5;\(index)m" case .grayscale: let gray = Int(pixel.luminance / 255.0 * 23.0) let index = 232 + min(max(gray, 0), 23) - return "\(ANSIRenderer.csi)38;5;\(index)m" + return "\(ANSIEscape.csi)38;5;\(index)m" case .mono: return "" @@ -446,7 +458,7 @@ extension ASCIIConverter { /// - overrideAspectRatio: An explicit width/height ratio. When `nil`, /// the source image's natural ratio is used. /// - Returns: The target width and height in characters. - static func targetSize( + public static func targetSize( imageWidth: Int, imageHeight: Int, maxWidth: Int, diff --git a/Sources/TUIkit/Image/ImageLoader.swift b/Sources/TUIkitImage/ImageLoader.swift similarity index 91% rename from Sources/TUIkit/Image/ImageLoader.swift rename to Sources/TUIkitImage/ImageLoader.swift index a5c89fba..7fc9b00d 100644 --- a/Sources/TUIkit/Image/ImageLoader.swift +++ b/Sources/TUIkitImage/ImageLoader.swift @@ -13,7 +13,7 @@ import Foundation /// /// Uses stb_image (bundled C library) on all platforms for consistent behavior. /// Supported formats: PNG, JPEG, GIF, BMP, TGA, HDR, PSD, PNM. -protocol ImageLoader: Sendable { +public protocol ImageLoader: Sendable { /// Loads an image from a file path. /// /// - Parameter path: The absolute file path to the image. @@ -32,7 +32,7 @@ protocol ImageLoader: Sendable { // MARK: - ImageLoadError /// Errors that can occur during image loading. -enum ImageLoadError: Error, LocalizedError, CustomStringConvertible { +public enum ImageLoadError: Error, LocalizedError, CustomStringConvertible { /// The file was not found at the given path. case fileNotFound(String) @@ -48,7 +48,7 @@ enum ImageLoadError: Error, LocalizedError, CustomStringConvertible { /// The image exceeds the maximum allowed pixel count. case imageTooLarge(pixelCount: Int, limit: Int) - var description: String { + public var description: String { switch self { case .fileNotFound(let path): return "Image file not found: \(path)" @@ -63,7 +63,7 @@ enum ImageLoadError: Error, LocalizedError, CustomStringConvertible { } } - var errorDescription: String? { description } + public var errorDescription: String? { description } } // MARK: - Platform Image Loader @@ -73,13 +73,15 @@ enum ImageLoadError: Error, LocalizedError, CustomStringConvertible { /// Supports PNG, JPEG, GIF, BMP, TGA, HDR, PSD, and PNM formats /// on both macOS and Linux. stb_image is a public-domain single-header /// C library bundled as a local `CSTBImage` target. -struct PlatformImageLoader: ImageLoader { +public struct PlatformImageLoader: ImageLoader { - func loadImage(from path: String) throws -> RGBAImage { + public init() {} + + public func loadImage(from path: String) throws -> RGBAImage { try loadImage(from: path, maxPixelCount: nil) } - func loadImage(from data: Data) throws -> RGBAImage { + public func loadImage(from data: Data) throws -> RGBAImage { try loadImage(from: data, maxPixelCount: nil) } @@ -90,7 +92,7 @@ struct PlatformImageLoader: ImageLoader { /// - maxPixelCount: The maximum allowed total pixel count, or `nil` for no limit. /// - Returns: The decoded image as `RGBAImage`. /// - Throws: `ImageLoadError` if the file cannot be read, decoded, or exceeds the limit. - func loadImage(from path: String, maxPixelCount: Int?) throws -> RGBAImage { + public func loadImage(from path: String, maxPixelCount: Int?) throws -> RGBAImage { guard FileManager.default.fileExists(atPath: path) else { throw ImageLoadError.fileNotFound(path) } @@ -120,7 +122,7 @@ struct PlatformImageLoader: ImageLoader { /// - maxPixelCount: The maximum allowed total pixel count, or `nil` for no limit. /// - Returns: The decoded image as `RGBAImage`. /// - Throws: `ImageLoadError` if the data cannot be decoded or exceeds the limit. - func loadImage(from data: Data, maxPixelCount: Int?) throws -> RGBAImage { + public func loadImage(from data: Data, maxPixelCount: Int?) throws -> RGBAImage { var width: Int32 = 0 var height: Int32 = 0 var channels: Int32 = 0 @@ -185,9 +187,9 @@ extension PlatformImageLoader { /// /// Cached entries persist for the lifetime of the application. /// Thread-safe via an internal lock. -final class URLImageCache: @unchecked Sendable { +public final class URLImageCache: @unchecked Sendable { /// Shared session cache. - static let shared = URLImageCache() + public static let shared = URLImageCache() private var cache: [String: RGBAImage] = [:] private let lock = NSLock() @@ -195,14 +197,14 @@ final class URLImageCache: @unchecked Sendable { private init() {} /// Returns a cached image for the given URL string, or nil. - func get(_ urlString: String) -> RGBAImage? { + public func get(_ urlString: String) -> RGBAImage? { lock.lock() defer { lock.unlock() } return cache[urlString] } /// Stores an image in the cache for the given URL string. - func set(_ urlString: String, image: RGBAImage) { + public func set(_ urlString: String, image: RGBAImage) { lock.lock() defer { lock.unlock() } cache[urlString] = image @@ -225,7 +227,7 @@ extension PlatformImageLoader { /// - maxPixelCount: The maximum allowed total pixel count, or `nil` for no limit. /// - Returns: The decoded image. /// - Throws: `ImageLoadError` on network or decoding failure, or if image exceeds size limit. - func loadImage( + public func loadImage( from urlString: String, cache: URLImageCache = .shared, timeout: TimeInterval = 30, diff --git a/Sources/TUIkit/Image/RGBAImage.swift b/Sources/TUIkitImage/RGBAImage.swift similarity index 88% rename from Sources/TUIkit/Image/RGBAImage.swift rename to Sources/TUIkitImage/RGBAImage.swift index d42cec37..36076c24 100644 --- a/Sources/TUIkit/Image/RGBAImage.swift +++ b/Sources/TUIkitImage/RGBAImage.swift @@ -10,14 +10,14 @@ /// /// Used as the intermediate representation for image data before /// ASCII art conversion. Each channel is stored as a `UInt8` (0-255). -struct RGBA: Sendable, Equatable { - var r: UInt8 - var g: UInt8 - var b: UInt8 - var a: UInt8 +public struct RGBA: Sendable, Equatable { + public var r: UInt8 + public var g: UInt8 + public var b: UInt8 + public var a: UInt8 /// Creates an opaque pixel with the given RGB values. - init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = 255) { + public init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = 255) { self.r = r self.g = g self.b = b @@ -32,7 +32,7 @@ extension RGBA { /// The perceived luminance using ITU-R BT.601 coefficients. /// /// Returns a value in the range 0.0 (black) to 255.0 (white). - var luminance: Double { + public var luminance: Double { Double(r) * 0.299 + Double(g) * 0.587 + Double(b) * 0.114 } } @@ -43,15 +43,15 @@ extension RGBA { /// /// This is the platform-independent representation produced by /// `ImageLoader` implementations and consumed by `ASCIIConverter`. -struct RGBAImage: Sendable { +public struct RGBAImage: Sendable { /// Image width in pixels. - let width: Int + public let width: Int /// Image height in pixels. - let height: Int + public let height: Int /// Row-major pixel data (`width * height` elements). - private(set) var pixels: [RGBA] + public private(set) var pixels: [RGBA] /// Creates an image from dimensions and pixel data. /// @@ -59,7 +59,7 @@ struct RGBAImage: Sendable { /// - width: Image width in pixels. /// - height: Image height in pixels. /// - pixels: Pixel data in row-major order. Must contain `width * height` elements. - init(width: Int, height: Int, pixels: [RGBA]) { + public init(width: Int, height: Int, pixels: [RGBA]) { precondition(pixels.count == width * height, "Pixel count must match width * height") self.width = width self.height = height @@ -77,7 +77,7 @@ extension RGBAImage { /// - x: Column (0-based, left to right). /// - y: Row (0-based, top to bottom). /// - Returns: The RGBA pixel value. - func pixel(at x: Int, _ y: Int) -> RGBA { + public func pixel(at x: Int, _ y: Int) -> RGBA { pixels[y * width + x] } @@ -87,7 +87,7 @@ extension RGBAImage { /// - x: Column (0-based). /// - y: Row (0-based). /// - value: The new pixel value. - mutating func setPixel(at x: Int, _ y: Int, value: RGBA) { + public mutating func setPixel(at x: Int, _ y: Int, value: RGBA) { pixels[y * width + x] = value } @@ -101,7 +101,7 @@ extension RGBAImage { /// - rError: Red channel error. /// - gError: Green channel error. /// - bError: Blue channel error. - mutating func addError(at x: Int, _ y: Int, rError: Double, gError: Double, bError: Double) { + public mutating func addError(at x: Int, _ y: Int, rError: Double, gError: Double, bError: Double) { let index = y * width + x let pixel = pixels[index] pixels[index] = RGBA( @@ -122,7 +122,7 @@ extension RGBAImage { /// - targetWidth: The desired width. /// - targetHeight: The desired height. /// - Returns: A new image with the specified dimensions. - func scaled(to targetWidth: Int, _ targetHeight: Int) -> RGBAImage { + public func scaled(to targetWidth: Int, _ targetHeight: Int) -> RGBAImage { guard targetWidth > 0, targetHeight > 0 else { return RGBAImage(width: 0, height: 0, pixels: []) } @@ -146,7 +146,7 @@ extension RGBAImage { /// - targetWidth: The desired width. /// - targetHeight: The desired height. /// - Returns: A new image with the specified dimensions. - func scaledBilinear(to targetWidth: Int, _ targetHeight: Int) -> RGBAImage { + public func scaledBilinear(to targetWidth: Int, _ targetHeight: Int) -> RGBAImage { guard targetWidth > 0, targetHeight > 0 else { return RGBAImage(width: 0, height: 0, pixels: []) } diff --git a/Sources/TUIkit/Styling/ANSIColor.swift b/Sources/TUIkitStyling/ANSIColor.swift similarity index 83% rename from Sources/TUIkit/Styling/ANSIColor.swift rename to Sources/TUIkitStyling/ANSIColor.swift index 34c9d41f..269df027 100644 --- a/Sources/TUIkit/Styling/ANSIColor.swift +++ b/Sources/TUIkitStyling/ANSIColor.swift @@ -5,7 +5,7 @@ // License: MIT /// The 8 standard ANSI colors. -enum ANSIColor: UInt8, Sendable { +public enum ANSIColor: UInt8, Sendable { case black = 0 case red = 1 case green = 2 @@ -17,29 +17,29 @@ enum ANSIColor: UInt8, Sendable { case `default` = 9 /// The ANSI code for foreground color (30-37, 39 for default). - var foregroundCode: UInt8 { + public var foregroundCode: UInt8 { 30 + rawValue } /// The ANSI code for background color (40-47, 49 for default). - var backgroundCode: UInt8 { + public var backgroundCode: UInt8 { 40 + rawValue } /// The ANSI code for bright foreground color (90-97). - var brightForegroundCode: UInt8 { + public var brightForegroundCode: UInt8 { 90 + rawValue } /// The ANSI code for bright background color (100-107). - var brightBackgroundCode: UInt8 { + public var brightBackgroundCode: UInt8 { 100 + rawValue } // MARK: - xterm Standard RGB Values /// The standard RGB values for this ANSI color (xterm defaults). - var rgbValues: (red: UInt8, green: UInt8, blue: UInt8) { + public var rgbValues: (red: UInt8, green: UInt8, blue: UInt8) { switch self { case .black: return (0, 0, 0) case .red: return (205, 0, 0) @@ -54,7 +54,7 @@ enum ANSIColor: UInt8, Sendable { } /// The bright RGB values for this ANSI color (xterm defaults). - var brightRGBValues: (red: UInt8, green: UInt8, blue: UInt8) { + public var brightRGBValues: (red: UInt8, green: UInt8, blue: UInt8) { switch self { case .black: return (127, 127, 127) case .red: return (255, 0, 0) diff --git a/Sources/TUIkit/Styling/Appearance.swift b/Sources/TUIkitStyling/Appearance.swift similarity index 74% rename from Sources/TUIkit/Styling/Appearance.swift rename to Sources/TUIkitStyling/Appearance.swift index 4b89e485..b829a547 100644 --- a/Sources/TUIkit/Styling/Appearance.swift +++ b/Sources/TUIkitStyling/Appearance.swift @@ -151,11 +151,11 @@ extension Appearance { // MARK: - Appearance Registry /// Registry of available appearances for cycling. -struct AppearanceRegistry { +public struct AppearanceRegistry { /// All available appearances in cycling order. /// /// Order: rounded (default) → line → doubleLine → heavy - static let all: [Appearance] = [ + public static let all: [Appearance] = [ .rounded, .line, .doubleLine, @@ -166,59 +166,7 @@ struct AppearanceRegistry { /// /// - Parameter id: The appearance ID to find. /// - Returns: The appearance, or nil if not found. - static func appearance(withId id: Appearance.ID) -> Appearance? { + public static func appearance(withId id: Appearance.ID) -> Appearance? { all.first { $0.rawId == id } } } - -// MARK: - Appearance Environment Key - -/// Environment key for the current appearance. -private struct AppearanceKey: EnvironmentKey { - static let defaultValue: Appearance = .default -} - -extension EnvironmentValues { - /// The current appearance. - /// - /// Set an appearance at the app level and it propagates to all child views: - /// - /// ```swift - /// WindowGroup { - /// ContentView() - /// } - /// .appearance(.rounded) - /// ``` - /// - /// Access the appearance in `renderToBuffer(context:)`: - /// - /// ```swift - /// let appearance = context.environment.appearance - /// let borderStyle = appearance.borderStyle - /// ``` - public var appearance: Appearance { - get { self[AppearanceKey.self] } - set { self[AppearanceKey.self] = newValue } - } -} - -// MARK: - AppearanceManager Environment Key - -/// Environment key for the appearance manager. -private struct AppearanceManagerKey: EnvironmentKey { - static let defaultValue = ThemeManager(items: AppearanceRegistry.all) -} - -extension EnvironmentValues { - /// The appearance manager for cycling and setting appearances. - /// - /// ```swift - /// let appearanceManager = context.environment.appearanceManager - /// appearanceManager.cycleNext() - /// appearanceManager.setCurrent(Appearance.rounded) - /// ``` - public var appearanceManager: ThemeManager { - get { self[AppearanceManagerKey.self] } - set { self[AppearanceManagerKey.self] = newValue } - } -} diff --git a/Sources/TUIkit/Styling/BorderStyle.swift b/Sources/TUIkitStyling/BorderStyle.swift similarity index 100% rename from Sources/TUIkit/Styling/BorderStyle.swift rename to Sources/TUIkitStyling/BorderStyle.swift diff --git a/Sources/TUIkit/Styling/Color.swift b/Sources/TUIkitStyling/Color.swift similarity index 91% rename from Sources/TUIkit/Styling/Color.swift rename to Sources/TUIkitStyling/Color.swift index 0a4accbe..828f3580 100644 --- a/Sources/TUIkit/Styling/Color.swift +++ b/Sources/TUIkitStyling/Color.swift @@ -24,10 +24,10 @@ /// ``` public struct Color: Sendable, Equatable { /// The internal color value. - let value: ColorValue + public let value: ColorValue /// Internal enum for different color types. - enum ColorValue: Sendable, Equatable { + public enum ColorValue: Sendable, Equatable { case standard(ANSIColor) case bright(ANSIColor) case palette256(UInt8) @@ -372,7 +372,7 @@ public extension Color { // MARK: - Internal API -extension Color { +public extension Color { /// Converts RGB components to HSL (hue 0–360, saturation 0–100, lightness 0–100). /// /// - Parameters: @@ -483,51 +483,3 @@ private extension Color { return .hsl(hue, saturation, min(100, max(0, newLightness))) } } - -// MARK: - Foreground Style Environment - -/// Environment key for the foreground style. -/// -/// When set via `.foregroundStyle(_:)` on any View, this value propagates -/// down through the view hierarchy. Child views can read it from the -/// render context to apply the color. -private struct ForegroundStyleKey: EnvironmentKey { - static let defaultValue: Color? = nil -} - -extension EnvironmentValues { - /// The foreground style (color) for text and other content. - /// - /// Set via `.foregroundStyle(_:)` modifier on any View. - /// Returns `nil` if not explicitly set (use palette default). - public var foregroundStyle: Color? { - get { self[ForegroundStyleKey.self] } - set { self[ForegroundStyleKey.self] = newValue } - } -} - -// MARK: - View Extension for foregroundStyle - -extension View { - /// Sets the foreground style for this view and its children. - /// - /// The style propagates through the view hierarchy via the environment. - /// Child views that render text or other colored content should read - /// `context.environment.foregroundStyle` and apply it. - /// - /// ## Example - /// - /// ```swift - /// VStack { - /// Text("Red text") - /// Text("Also red") - /// } - /// .foregroundStyle(.red) - /// ``` - /// - /// - Parameter style: The color to apply as foreground style. - /// - Returns: A view with the foreground style set. - public func foregroundStyle(_ style: Color?) -> some View { - environment(\.foregroundStyle, style) - } -} diff --git a/Sources/TUIkit/Styling/ContentMode.swift b/Sources/TUIkitStyling/ContentMode.swift similarity index 100% rename from Sources/TUIkit/Styling/ContentMode.swift rename to Sources/TUIkitStyling/ContentMode.swift diff --git a/Sources/TUIkit/Styling/Palettes/PalettePreset.swift b/Sources/TUIkitStyling/Palettes/PalettePreset.swift similarity index 100% rename from Sources/TUIkit/Styling/Palettes/PalettePreset.swift rename to Sources/TUIkitStyling/Palettes/PalettePreset.swift diff --git a/Sources/TUIkit/Styling/SemanticColor.swift b/Sources/TUIkitStyling/SemanticColor.swift similarity index 97% rename from Sources/TUIkit/Styling/SemanticColor.swift rename to Sources/TUIkitStyling/SemanticColor.swift index aebb205f..152f1af1 100644 --- a/Sources/TUIkit/Styling/SemanticColor.swift +++ b/Sources/TUIkitStyling/SemanticColor.swift @@ -16,7 +16,7 @@ /// ```swift /// Text("Hello").foregroundStyle(.palette.accent) /// ``` -enum SemanticColor: String, Sendable, Equatable { +public enum SemanticColor: String, Sendable, Equatable { // Background case background case statusBarBackground diff --git a/Sources/TUIkit/Styling/TextContentType.swift b/Sources/TUIkitStyling/TextContentType.swift similarity index 79% rename from Sources/TUIkit/Styling/TextContentType.swift rename to Sources/TUIkitStyling/TextContentType.swift index 8ab4d7d0..3d1c9495 100644 --- a/Sources/TUIkit/Styling/TextContentType.swift +++ b/Sources/TUIkitStyling/TextContentType.swift @@ -75,7 +75,7 @@ public enum TextContentType: Sendable, Equatable { // MARK: - Character Filtering -extension TextContentType { +public extension TextContentType { /// The set of Unicode scalars allowed for this content type. /// /// `.password` returns `nil` to indicate no filtering. @@ -161,49 +161,3 @@ private extension TextContentType { /// Decimal characters (digits, minus sign, and decimal point). static let decimalCharacters = CharacterSet(charactersIn: "0123456789-.") } - -// MARK: - Environment Key - -/// Environment key for the text content type. -private struct TextContentTypeKey: EnvironmentKey { - static let defaultValue: TextContentType? = nil -} - -extension EnvironmentValues { - /// The text content type for text fields. - /// - /// When set, text fields filter both typed characters and pasted text - /// against the allowed character set of the content type. - /// - /// Set this value using the `.textContentType(_:)` modifier: - /// - /// ```swift - /// TextField("URL", text: $url) - /// .textContentType(.url) - /// ``` - public var textContentType: TextContentType? { - get { self[TextContentTypeKey.self] } - set { self[TextContentTypeKey.self] = newValue } - } -} - -// MARK: - View Extension - -extension View { - /// Sets the text content type for text fields within this view. - /// - /// When a content type is set, both typed characters and pasted text - /// are filtered against the allowed character set. Invalid characters - /// are silently dropped. - /// - /// ```swift - /// TextField("URL", text: $url) - /// .textContentType(.url) - /// ``` - /// - /// - Parameter type: The content type, or `nil` to disable filtering. - /// - Returns: A view with the content type applied. - public func textContentType(_ type: TextContentType?) -> some View { - environment(\.textContentType, type) - } -} diff --git a/Sources/TUIkit/Styling/TextCursorStyle.swift b/Sources/TUIkitStyling/TextCursorStyle.swift similarity index 71% rename from Sources/TUIkit/Styling/TextCursorStyle.swift rename to Sources/TUIkitStyling/TextCursorStyle.swift index 58a572af..f6ef43e4 100644 --- a/Sources/TUIkit/Styling/TextCursorStyle.swift +++ b/Sources/TUIkitStyling/TextCursorStyle.swift @@ -1,4 +1,4 @@ -// TUIKit - Terminal UI Kit for Swift +// 🖥️ TUIKit — Terminal UI Kit for Swift // TextCursorStyle.swift // // Created by LAYERED.work @@ -173,67 +173,3 @@ extension TextCursorStyle { /// An underscore cursor with blink animation at regular speed. public static let underscore = TextCursorStyle(shape: .underscore, animation: .blink, speed: .regular) } - -// MARK: - Environment Key - -/// Environment key for the text cursor style. -private struct TextCursorStyleKey: EnvironmentKey { - static let defaultValue = TextCursorStyle() -} - -extension EnvironmentValues { - /// The text cursor style for text fields. - /// - /// Set this value using the `.textCursor(_:)` modifier: - /// - /// ```swift - /// TextField("Name", text: $name) - /// .textCursor(.bar, animation: .blink) - /// ``` - public var textCursorStyle: TextCursorStyle { - get { self[TextCursorStyleKey.self] } - set { self[TextCursorStyleKey.self] = newValue } - } -} - -// MARK: - View Extension - -extension View { - /// Sets the text cursor style for text fields within this view. - /// - /// Use this modifier to customize the cursor appearance in ``TextField`` - /// and ``SecureField`` components. - /// - /// ```swift - /// TextField("Name", text: $name) - /// .textCursor(.bar) - /// ``` - /// - /// - Parameter style: The cursor style to use. - /// - Returns: A view with the cursor style applied. - public func textCursor(_ style: TextCursorStyle) -> some View { - environment(\.textCursorStyle, style) - } - - /// Sets the text cursor style with separate shape, animation, and speed parameters. - /// - /// Use this modifier when you want to specify shape, animation, and speed: - /// - /// ```swift - /// TextField("Code", text: $code) - /// .textCursor(.underscore, animation: .blink, speed: .fast) - /// ``` - /// - /// - Parameters: - /// - shape: The cursor shape. - /// - animation: The cursor animation. Defaults to `.pulse`. - /// - speed: The animation speed. Defaults to `.regular`. - /// - Returns: A view with the cursor style applied. - public func textCursor( - _ shape: TextCursorStyle.Shape, - animation: TextCursorStyle.Animation = .pulse, - speed: TextCursorStyle.Speed = .regular - ) -> some View { - environment(\.textCursorStyle, TextCursorStyle(shape: shape, animation: animation, speed: speed)) - } -} diff --git a/Sources/TUIkit/Styling/Theme.swift b/Sources/TUIkitStyling/Theme.swift similarity index 66% rename from Sources/TUIkit/Styling/Theme.swift rename to Sources/TUIkitStyling/Theme.swift index c68a7b9a..5dfb9f05 100644 --- a/Sources/TUIkit/Styling/Theme.swift +++ b/Sources/TUIkitStyling/Theme.swift @@ -109,74 +109,22 @@ extension Palette { public var cursorColor: Color { accent } } -// MARK: - Palette Environment Key - -/// Environment key for the current palette. -private struct PaletteKey: EnvironmentKey { - static let defaultValue: any Palette = SystemPalette(.green) -} - -extension EnvironmentValues { - /// The current palette. - /// - /// Set a palette at the app level and it propagates to all child views: - /// - /// ```swift - /// WindowGroup { - /// ContentView() - /// } - /// .environment(\.palette, SystemPalette(.green)) - /// ``` - /// - /// Access the palette in `renderToBuffer(context:)`: - /// - /// ```swift - /// let palette = context.environment.palette - /// let fg = palette.foreground - /// ``` - public var palette: any Palette { - get { self[PaletteKey.self] } - set { self[PaletteKey.self] = newValue } - } -} - // MARK: - Palette Registry /// Registry of available palettes. -struct PaletteRegistry { +public struct PaletteRegistry { /// All available palettes in cycling order, built from ``SystemPalette/Preset``. /// /// Order: Green → Amber → Red → Violet → Blue → White - static let all: [any Palette] = SystemPalette.Preset.allCases.map { SystemPalette($0) } + public static let all: [any Palette] = SystemPalette.Preset.allCases.map { SystemPalette($0) } /// Finds a palette by ID. - static func palette(withId id: String) -> (any Palette)? { + public static func palette(withId id: String) -> (any Palette)? { all.first { $0.id == id } } /// Finds a palette by name. - static func palette(withName name: String) -> (any Palette)? { + public static func palette(withName name: String) -> (any Palette)? { all.first { $0.name == name } } } - -// MARK: - PaletteManager Environment Key - -/// Environment key for the palette manager. -private struct PaletteManagerKey: EnvironmentKey { - static let defaultValue = ThemeManager(items: PaletteRegistry.all) -} - -extension EnvironmentValues { - /// The palette manager for cycling and setting palettes. - /// - /// ```swift - /// let paletteManager = context.environment.paletteManager - /// paletteManager.cycleNext() - /// paletteManager.setCurrent(SystemPalette(.amber)) - /// ``` - public var paletteManager: ThemeManager { - get { self[PaletteManagerKey.self] } - set { self[PaletteManagerKey.self] = newValue } - } -} diff --git a/Sources/TUIkit/Styling/ThemeManager.swift b/Sources/TUIkitStyling/ThemeManager.swift similarity index 87% rename from Sources/TUIkit/Styling/ThemeManager.swift rename to Sources/TUIkitStyling/ThemeManager.swift index 2f7b3aed..b12c440d 100644 --- a/Sources/TUIkit/Styling/ThemeManager.swift +++ b/Sources/TUIkitStyling/ThemeManager.swift @@ -57,7 +57,7 @@ public protocol Cyclable: Sendable { /// # Render Integration /// /// On every change the manager triggers a re-render through the -/// injected ``AppState`` instance. The `RenderLoop` picks up the +/// injected render trigger closure. The `RenderLoop` picks up the /// current item via ``currentPalette`` / ``currentAppearance`` when /// building the environment for the next frame. public final class ThemeManager: @unchecked Sendable { @@ -65,27 +65,27 @@ public final class ThemeManager: @unchecked Sendable { private var currentIndex: Int = 0 /// All available items in cycling order. - let items: [any Cyclable] + public let items: [any Cyclable] - /// The app state used to trigger re-renders on theme changes. - private let appState: AppState + /// Closure that triggers a re-render when the theme changes. + private let renderTrigger: @Sendable () -> Void /// Creates a theme manager with the given items. /// /// - Parameters: /// - items: The items to cycle through. Must not be empty. - /// - appState: The app state instance for triggering re-renders. - init(items: [any Cyclable], appState: AppState) { + /// - renderTrigger: A closure that triggers a re-render when the selection changes. + public init(items: [any Cyclable], renderTrigger: @escaping @Sendable () -> Void) { precondition(!items.isEmpty, "ThemeManager requires at least one item") self.items = items - self.appState = appState + self.renderTrigger = renderTrigger } - /// Creates a theme manager with a default `AppState` instance. + /// Creates a theme manager with a no-op render trigger. /// /// Used for environment key defaults only. - convenience init(items: [any Cyclable]) { - self.init(items: items, appState: AppState()) + public convenience init(items: [any Cyclable]) { + self.init(items: items, renderTrigger: {}) } // MARK: - Current Item @@ -142,7 +142,7 @@ public extension ThemeManager { private extension ThemeManager { /// Triggers a re-render so the `RenderLoop` picks up the new current item. func applyCurrentItem() { - appState.setNeedsRender() + renderTrigger() } } diff --git a/Sources/TUIkit/Styling/ViewConstants.swift b/Sources/TUIkitStyling/ViewConstants.swift similarity index 57% rename from Sources/TUIkit/Styling/ViewConstants.swift rename to Sources/TUIkitStyling/ViewConstants.swift index 505f6660..c1d5ea48 100644 --- a/Sources/TUIkit/Styling/ViewConstants.swift +++ b/Sources/TUIkitStyling/ViewConstants.swift @@ -11,46 +11,36 @@ /// Keeping opacity values and other visual parameters in one place ensures /// consistency and makes global adjustments easy. All values are `Double` /// for direct use with ``Color/opacity(_:)``. -enum ViewConstants { +public enum ViewConstants { // MARK: - Focus & Selection Opacity /// Minimum accent opacity during focus pulsing animation (dim phase). - static let focusPulseMin: Double = 0.35 + public static let focusPulseMin: Double = 0.35 /// Maximum accent opacity during focus pulsing animation (bright phase). - static let focusPulseMax: Double = 0.50 + public static let focusPulseMax: Double = 0.50 /// Background opacity for selected (but unfocused) rows. - static let selectedBackground: Double = 0.25 + public static let selectedBackground: Double = 0.25 /// Background opacity for alternating row tinting. - static let alternatingRowBackground: Double = 0.15 + public static let alternatingRowBackground: Double = 0.15 /// Accent opacity for focus borders and indicator caps in their dim state. - static let focusBorderDim: Double = 0.20 + public static let focusBorderDim: Double = 0.20 /// Foreground opacity for disabled interactive controls. - static let disabledForeground: Double = 0.50 + public static let disabledForeground: Double = 0.50 /// Accent opacity for selection indicator bullets. - static let selectionIndicator: Double = 0.60 + public static let selectionIndicator: Double = 0.60 /// Accent opacity for focused button caps pulsing bright phase. - static let buttonCapPulseBright: Double = 0.45 + public static let buttonCapPulseBright: Double = 0.45 // MARK: - Default Strings /// Default placeholder text for empty List and Table views. - static let emptyListPlaceholder = "No items" -} - -// MARK: - EdgeInsets Defaults - -extension EdgeInsets { - /// Default insets for containers (Card, Panel, etc.): 1 horizontal, 0 vertical. - static let containerDefault = EdgeInsets(horizontal: 1, vertical: 0) - - /// Default insets for dialogs: 2 horizontal, 1 vertical. - static let dialogDefault = EdgeInsets(horizontal: 2, vertical: 1) + public static let emptyListPlaceholder = "No items" }