mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Merge pull request #44 from phranck/feat/blue-theme-and-color-sync
Feat: Add Blue/Violet palettes, sync theme colors and order between SDK and landing page
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// BluePalette.swift
|
||||
// TUIkit — Terminal UI Framework for Swift
|
||||
//
|
||||
// Created by LAYERED.work
|
||||
// CC BY-NC-SA 4.0
|
||||
|
||||
/// Blue VFD terminal palette.
|
||||
///
|
||||
/// Inspired by vintage vacuum fluorescent displays (VFDs) found in
|
||||
/// audio equipment, cash registers, and instrument panels. Uses the
|
||||
/// characteristic bright cyan-blue glow on a near-black background.
|
||||
public struct BluePalette: Palette {
|
||||
public let id = "blue"
|
||||
public let name = "Blue"
|
||||
|
||||
// Background hierarchy
|
||||
public let background = Color.hex(0x060708) // App background (darkest)
|
||||
public let backgroundSecondary = Color.hex(0x0E1825) // Container body background (brighter)
|
||||
public let backgroundTertiary = Color.hex(0x0A121C) // Header/footer background
|
||||
|
||||
// Blue text hierarchy
|
||||
public let foreground = Color.hex(0x00AAFF) // Bright VFD blue - primary text
|
||||
public let foregroundSecondary = Color.hex(0x0088CC) // Medium blue - secondary text
|
||||
public let foregroundTertiary = Color.hex(0x006699) // Dim blue - tertiary/muted text
|
||||
|
||||
// Accent colors
|
||||
public let accent = Color.hex(0x33BBFF) // Lighter blue for highlights
|
||||
public let accentSecondary = Color.hex(0x0099DD) // Darker accent
|
||||
|
||||
// Semantic colors (stay in blue family)
|
||||
public let success = Color.hex(0x33CCFF) // Cyan-blue
|
||||
public let warning = Color.hex(0x66CCFF) // Light cyan
|
||||
public let error = Color.hex(0xFF6633) // Orange-red (contrast)
|
||||
public let info = Color.hex(0x99DDFF) // Pale blue
|
||||
|
||||
// UI elements
|
||||
public let border = Color.hex(0x2D4A5A) // Subtle blue border
|
||||
public let borderFocused = Color.hex(0x00AAFF) // Bright when focused
|
||||
public let selection = Color.hex(0x33BBFF) // Bright blue for selection text
|
||||
public let selectionBackground = Color.hex(0x1A3A4D) // Dark blue for selection bar bg
|
||||
|
||||
// Status bar
|
||||
public let statusBarBackground = Color.hex(0x0F1822) // Dark blue for status bar
|
||||
public let statusBarForeground = Color.hex(0x0099EE) // Slightly dimmer than primary foreground
|
||||
public let statusBarHighlight = Color.hex(0x33BBFF)
|
||||
|
||||
// Container colors for block appearance
|
||||
public var containerBackground: Color { backgroundSecondary } // Body
|
||||
public var containerHeaderBackground: Color { backgroundTertiary } // Header/footer
|
||||
public var buttonBackground: Color { Color.hex(0x14304A) } // Lighter blue for buttons
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == BluePalette {
|
||||
/// Blue VFD terminal palette.
|
||||
public static var blue: BluePalette { BluePalette() }
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
//
|
||||
// GeneratedPalette.swift
|
||||
// TUIkit
|
||||
//
|
||||
// A palette that generates its entire color scheme from a single base hue.
|
||||
//
|
||||
|
||||
/// A palette that generates its entire color scheme from a single base hue.
|
||||
///
|
||||
/// All colors are derived algorithmically using HSL transformations.
|
||||
/// This allows creating new palettes with a single line of code.
|
||||
///
|
||||
/// # How it works
|
||||
///
|
||||
/// Given a base hue (0-360), the palette generates:
|
||||
/// - **Backgrounds**: very low lightness, subtle saturation tinted toward the hue
|
||||
/// - **Foregrounds**: high lightness with full saturation at the base hue
|
||||
/// - **Accents**: slightly lighter/shifted versions of the foreground
|
||||
/// - **Semantic colors**: hue-shifted variants (success = +120, warning = +60, error = +180, info = -60)
|
||||
/// - **UI elements**: mid-range lightness/saturation for borders, selection, status bar
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```swift
|
||||
/// // Create a violet palette
|
||||
/// let violet = GeneratedPalette(name: "Violet", hue: 270)
|
||||
///
|
||||
/// // Create a teal palette
|
||||
/// let teal = GeneratedPalette(name: "Teal", hue: 180)
|
||||
/// ```
|
||||
public struct GeneratedPalette: Palette, Sendable {
|
||||
public let id: String
|
||||
public let name: String
|
||||
|
||||
// Background hierarchy
|
||||
public let background: Color
|
||||
public let backgroundSecondary: Color
|
||||
public let backgroundTertiary: Color
|
||||
|
||||
// Foreground hierarchy
|
||||
public let foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
|
||||
// Accents
|
||||
public let accent: Color
|
||||
public let accentSecondary: Color
|
||||
|
||||
// Semantic
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border: Color
|
||||
public let borderFocused: Color
|
||||
public let selection: Color
|
||||
public let selectionBackground: Color
|
||||
|
||||
// Status bar
|
||||
public let statusBarBackground: Color
|
||||
public let statusBarForeground: Color
|
||||
public let statusBarHighlight: Color
|
||||
|
||||
// Container (block appearance)
|
||||
public let containerBackground: Color
|
||||
public let containerHeaderBackground: Color
|
||||
public let buttonBackground: Color
|
||||
|
||||
/// Creates a generated palette from a single base hue.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The display name for the palette.
|
||||
/// - hue: The base hue (0-360). Examples: red=0, green=120, blue=240, violet=270.
|
||||
/// - saturation: The base saturation (0-100, default: 100). Lower values produce more muted palettes.
|
||||
public init(name: String, hue: Double, saturation: Double = 100) {
|
||||
self.id = "generated-\(name.lowercased())"
|
||||
self.name = name
|
||||
|
||||
let baseSaturation = min(100, max(0, saturation))
|
||||
|
||||
// --- Backgrounds: very dark, subtly tinted ---
|
||||
// App background — near black with a hint of the hue
|
||||
self.background = Color.hsl(hue, baseSaturation * 0.30, 3)
|
||||
// Container body — slightly brighter
|
||||
self.backgroundSecondary = Color.hsl(hue, baseSaturation * 0.40, 10)
|
||||
// Header/footer — between app and body
|
||||
self.backgroundTertiary = Color.hsl(hue, baseSaturation * 0.35, 7)
|
||||
|
||||
// --- Foregrounds: bright, saturated text ---
|
||||
self.foreground = Color.hsl(hue, baseSaturation * 0.80, 70)
|
||||
self.foregroundSecondary = Color.hsl(hue, baseSaturation * 0.70, 55)
|
||||
self.foregroundTertiary = Color.hsl(hue, baseSaturation * 0.60, 40)
|
||||
|
||||
// --- Accents: lighter/brighter variant ---
|
||||
self.accent = Color.hsl(hue, baseSaturation * 0.85, 78)
|
||||
self.accentSecondary = Color.hsl(hue, baseSaturation * 0.75, 50)
|
||||
|
||||
// --- Semantic: hue-shifted from base ---
|
||||
// success = base + 120° (toward green family)
|
||||
let successHue = Self.wrapHue(hue + 120)
|
||||
self.success = Color.hsl(successHue, baseSaturation * 0.70, 65)
|
||||
|
||||
// warning = base + 60° (toward yellow family)
|
||||
let warningHue = Self.wrapHue(hue + 60)
|
||||
self.warning = Color.hsl(warningHue, baseSaturation * 0.80, 70)
|
||||
|
||||
// error = base + 180° (complementary)
|
||||
let errorHue = Self.wrapHue(hue + 180)
|
||||
self.error = Color.hsl(errorHue, baseSaturation * 0.85, 65)
|
||||
|
||||
// info = base − 60° (analogous cool side)
|
||||
let infoHue = Self.wrapHue(hue - 60)
|
||||
self.info = Color.hsl(infoHue, baseSaturation * 0.70, 70)
|
||||
|
||||
// --- UI elements ---
|
||||
self.border = Color.hsl(hue, baseSaturation * 0.40, 25)
|
||||
self.borderFocused = Color.hsl(hue, baseSaturation * 0.80, 70)
|
||||
self.selection = Color.hsl(hue, baseSaturation * 0.85, 78)
|
||||
self.selectionBackground = Color.hsl(hue, baseSaturation * 0.50, 18)
|
||||
|
||||
// --- Status bar ---
|
||||
self.statusBarBackground = Color.hsl(hue, baseSaturation * 0.35, 8)
|
||||
self.statusBarForeground = Color.hsl(hue, baseSaturation * 0.75, 65)
|
||||
self.statusBarHighlight = Color.hsl(hue, baseSaturation * 0.85, 78)
|
||||
|
||||
// --- Container (block appearance) ---
|
||||
self.containerBackground = Color.hsl(hue, baseSaturation * 0.40, 10)
|
||||
self.containerHeaderBackground = Color.hsl(hue, baseSaturation * 0.35, 7)
|
||||
self.buttonBackground = Color.hsl(hue, baseSaturation * 0.45, 15)
|
||||
}
|
||||
|
||||
/// Wraps a hue value to the 0-360 range.
|
||||
private static func wrapHue(_ hue: Double) -> Double {
|
||||
var wrapped = hue.truncatingRemainder(dividingBy: 360)
|
||||
if wrapped < 0 { wrapped += 360 }
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// MARK: - Preset Hues
|
||||
|
||||
/// Predefined hue values for common generated palettes.
|
||||
enum Hue {
|
||||
/// Green hue (120°).
|
||||
static let green: Double = 120
|
||||
/// Violet hue (270°).
|
||||
static let violet: Double = 270
|
||||
}
|
||||
|
||||
// MARK: - Presets
|
||||
|
||||
/// A green generated palette — for direct comparison with GreenPalette.
|
||||
public static let green = Self(name: "Gen. Green", hue: Hue.green)
|
||||
/// A violet generated palette.
|
||||
public static let violet = Self(name: "Violet", hue: Hue.violet)
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == GeneratedPalette {
|
||||
/// Violet generated palette (hue 270°).
|
||||
public static var violet: GeneratedPalette { GeneratedPalette.violet }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// VioletPalette.swift
|
||||
// TUIkit — Terminal UI Framework for Swift
|
||||
//
|
||||
// Created by LAYERED.work
|
||||
// CC BY-NC-SA 4.0
|
||||
|
||||
/// Violet terminal palette.
|
||||
///
|
||||
/// Inspired by retro computing aesthetics and sci-fi terminal displays.
|
||||
/// All colors are generated algorithmically from a single base hue (270°)
|
||||
/// using HSL transformations.
|
||||
public struct VioletPalette: Palette {
|
||||
public let id = "violet"
|
||||
public let name = "Violet"
|
||||
|
||||
/// The base hue used to generate all palette colors.
|
||||
private static let baseHue: Double = 270
|
||||
|
||||
// Background hierarchy
|
||||
public let background: Color
|
||||
public let backgroundSecondary: Color
|
||||
public let backgroundTertiary: Color
|
||||
|
||||
// Violet text hierarchy
|
||||
public let foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
|
||||
// Accent colors
|
||||
public let accent: Color
|
||||
public let accentSecondary: Color
|
||||
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border: Color
|
||||
public let borderFocused: Color
|
||||
public let selection: Color
|
||||
public let selectionBackground: Color
|
||||
|
||||
// Status bar
|
||||
public let statusBarBackground: Color
|
||||
public let statusBarForeground: Color
|
||||
public let statusBarHighlight: Color
|
||||
|
||||
// Container colors for block appearance
|
||||
public let containerBackground: Color
|
||||
public let containerHeaderBackground: Color
|
||||
public let buttonBackground: Color
|
||||
|
||||
public init() {
|
||||
let hue = Self.baseHue
|
||||
|
||||
// Backgrounds: very dark, subtly tinted
|
||||
self.background = Color.hsl(hue, 30, 3)
|
||||
self.backgroundSecondary = Color.hsl(hue, 40, 10)
|
||||
self.backgroundTertiary = Color.hsl(hue, 35, 7)
|
||||
|
||||
// Foregrounds: bright, saturated text
|
||||
self.foreground = Color.hsl(hue, 80, 70)
|
||||
self.foregroundSecondary = Color.hsl(hue, 70, 55)
|
||||
self.foregroundTertiary = Color.hsl(hue, 60, 40)
|
||||
|
||||
// Accents: lighter/brighter variant
|
||||
self.accent = Color.hsl(hue, 85, 78)
|
||||
self.accentSecondary = Color.hsl(hue, 75, 50)
|
||||
|
||||
// Semantic: hue-shifted from base
|
||||
self.success = Color.hsl(Self.wrapHue(hue + 120), 70, 65)
|
||||
self.warning = Color.hsl(Self.wrapHue(hue + 60), 80, 70)
|
||||
self.error = Color.hsl(Self.wrapHue(hue + 180), 85, 65)
|
||||
self.info = Color.hsl(Self.wrapHue(hue - 60), 70, 70)
|
||||
|
||||
// UI elements
|
||||
self.border = Color.hsl(hue, 40, 25)
|
||||
self.borderFocused = Color.hsl(hue, 80, 70)
|
||||
self.selection = Color.hsl(hue, 85, 78)
|
||||
self.selectionBackground = Color.hsl(hue, 50, 18)
|
||||
|
||||
// Status bar
|
||||
self.statusBarBackground = Color.hsl(hue, 35, 8)
|
||||
self.statusBarForeground = Color.hsl(hue, 75, 65)
|
||||
self.statusBarHighlight = Color.hsl(hue, 85, 78)
|
||||
|
||||
// Container (block appearance)
|
||||
self.containerBackground = Color.hsl(hue, 40, 10)
|
||||
self.containerHeaderBackground = Color.hsl(hue, 35, 7)
|
||||
self.buttonBackground = Color.hsl(hue, 45, 15)
|
||||
}
|
||||
|
||||
/// Wraps a hue value to the 0–360 range.
|
||||
private static func wrapHue(_ hue: Double) -> Double {
|
||||
var wrapped = hue.truncatingRemainder(dividingBy: 360)
|
||||
if wrapped < 0 { wrapped += 360 }
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == VioletPalette {
|
||||
/// Violet terminal palette.
|
||||
public static var violet: VioletPalette { VioletPalette() }
|
||||
}
|
||||
@@ -174,15 +174,15 @@ extension EnvironmentValues {
|
||||
struct PaletteRegistry {
|
||||
/// All available palettes in cycling order.
|
||||
///
|
||||
/// Order: Green → Gen. Green → Amber → White → Red → NCurses → Violet (generated)
|
||||
/// Order: Green → Amber → Red → Violet → Blue → White → NCurses
|
||||
static let all: [any Palette] = [
|
||||
GreenPalette(),
|
||||
GeneratedPalette.green,
|
||||
AmberPalette(),
|
||||
WhitePalette(),
|
||||
RedPalette(),
|
||||
VioletPalette(),
|
||||
BluePalette(),
|
||||
WhitePalette(),
|
||||
NCursesPalette(),
|
||||
GeneratedPalette.violet,
|
||||
]
|
||||
|
||||
/// Finds a palette by ID.
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// GeneratedPaletteTests.swift
|
||||
// TUIkit
|
||||
//
|
||||
// Tests for GeneratedPalette: hue-based palette generation.
|
||||
//
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import TUIkit
|
||||
|
||||
@Suite("Generated Palette Tests")
|
||||
struct GeneratedPaletteTests {
|
||||
|
||||
@Test("Generated palette creates ID from name")
|
||||
func generatedId() {
|
||||
let palette = GeneratedPalette(name: "Violet", hue: 270)
|
||||
#expect(palette.id == "generated-violet")
|
||||
#expect(palette.name == "Violet")
|
||||
}
|
||||
|
||||
@Test("Generated palette with different hues produces different colors")
|
||||
func differentHues() {
|
||||
let green = GeneratedPalette(name: "Green", hue: 120)
|
||||
let violet = GeneratedPalette(name: "Violet", hue: 270)
|
||||
#expect(green.foreground != violet.foreground)
|
||||
#expect(green.accent != violet.accent)
|
||||
}
|
||||
|
||||
@Test("Generated palette clamps saturation to 0-100")
|
||||
func saturationClamping() {
|
||||
// Should not crash with out-of-range values
|
||||
let oversaturated = GeneratedPalette(name: "Over", hue: 120, saturation: 200)
|
||||
let undersaturated = GeneratedPalette(name: "Under", hue: 120, saturation: -50)
|
||||
#expect(oversaturated.foreground != undersaturated.foreground)
|
||||
}
|
||||
|
||||
@Test("Generated palette preset hue constants")
|
||||
func presetHues() {
|
||||
#expect(GeneratedPalette.Hue.green == 120)
|
||||
#expect(GeneratedPalette.Hue.violet == 270)
|
||||
}
|
||||
|
||||
@Test("Generated palette presets exist")
|
||||
func presets() {
|
||||
let green = GeneratedPalette.green
|
||||
#expect(green.name == "Gen. Green")
|
||||
#expect(green.id == "generated-gen. green")
|
||||
|
||||
let violet = GeneratedPalette.violet
|
||||
#expect(violet.name == "Violet")
|
||||
#expect(violet.id == "generated-violet")
|
||||
}
|
||||
|
||||
@Test("Generated palette has distinct background hierarchy")
|
||||
func generatedBackgrounds() {
|
||||
let palette = GeneratedPalette(name: "Test", hue: 180)
|
||||
#expect(palette.background != palette.backgroundSecondary)
|
||||
#expect(palette.backgroundSecondary != palette.backgroundTertiary)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,19 +14,19 @@ struct PaletteRegistryTests {
|
||||
|
||||
@Test("Registry contains all predefined palettes")
|
||||
func registryCount() {
|
||||
// Green, Gen. Green, Amber, White, Red, NCurses, Violet = 7
|
||||
// Green, Amber, Red, Violet, Blue, White, NCurses = 7
|
||||
#expect(PaletteRegistry.all.count == 7)
|
||||
}
|
||||
|
||||
@Test("Registry cycling order starts with green")
|
||||
@Test("Registry cycling order follows color spectrum")
|
||||
func registryCyclingOrder() {
|
||||
#expect(PaletteRegistry.all[0].id == "green")
|
||||
#expect(PaletteRegistry.all[1].id == "generated-gen. green")
|
||||
#expect(PaletteRegistry.all[2].id == "amber")
|
||||
#expect(PaletteRegistry.all[3].id == "white")
|
||||
#expect(PaletteRegistry.all[4].id == "red")
|
||||
#expect(PaletteRegistry.all[5].id == "ncurses")
|
||||
#expect(PaletteRegistry.all[6].id == "generated-violet")
|
||||
#expect(PaletteRegistry.all[1].id == "amber")
|
||||
#expect(PaletteRegistry.all[2].id == "red")
|
||||
#expect(PaletteRegistry.all[3].id == "violet")
|
||||
#expect(PaletteRegistry.all[4].id == "blue")
|
||||
#expect(PaletteRegistry.all[5].id == "white")
|
||||
#expect(PaletteRegistry.all[6].id == "ncurses")
|
||||
}
|
||||
|
||||
@Test("Registry finds palette by ID")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// PredefinedPaletteTests.swift
|
||||
// TUIkit
|
||||
//
|
||||
// Tests for all predefined palette structs: Green, Amber, White, Red, NCurses.
|
||||
// Tests for all predefined palette structs: Green, Amber, Red, Violet, Blue, White, NCurses.
|
||||
//
|
||||
|
||||
import Testing
|
||||
@@ -97,6 +97,64 @@ struct RedPaletteTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Violet Palette Tests
|
||||
|
||||
@Suite("Violet Palette Tests")
|
||||
struct VioletPaletteTests {
|
||||
|
||||
@Test("Violet palette has correct identity")
|
||||
func violetIdentity() {
|
||||
let palette = VioletPalette()
|
||||
#expect(palette.id == "violet")
|
||||
#expect(palette.name == "Violet")
|
||||
}
|
||||
|
||||
@Test("Violet palette has distinct background hierarchy")
|
||||
func violetBackgrounds() {
|
||||
let palette = VioletPalette()
|
||||
#expect(palette.background != palette.backgroundSecondary)
|
||||
#expect(palette.backgroundSecondary != palette.backgroundTertiary)
|
||||
#expect(palette.background != palette.backgroundTertiary)
|
||||
}
|
||||
|
||||
@Test("Violet palette colors differ from green palette")
|
||||
func violetDiffersFromGreen() {
|
||||
let violet = VioletPalette()
|
||||
let green = GreenPalette()
|
||||
#expect(violet.foreground != green.foreground)
|
||||
#expect(violet.accent != green.accent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Blue Palette Tests
|
||||
|
||||
@Suite("Blue Palette Tests")
|
||||
struct BluePaletteTests {
|
||||
|
||||
@Test("Blue palette has correct identity")
|
||||
func blueIdentity() {
|
||||
let palette = BluePalette()
|
||||
#expect(palette.id == "blue")
|
||||
#expect(palette.name == "Blue")
|
||||
}
|
||||
|
||||
@Test("Blue palette has distinct background hierarchy")
|
||||
func blueBackgrounds() {
|
||||
let palette = BluePalette()
|
||||
#expect(palette.background != palette.backgroundSecondary)
|
||||
#expect(palette.backgroundSecondary != palette.backgroundTertiary)
|
||||
#expect(palette.background != palette.backgroundTertiary)
|
||||
}
|
||||
|
||||
@Test("Blue palette colors differ from violet palette")
|
||||
func blueDiffersFromViolet() {
|
||||
let blue = BluePalette()
|
||||
let violet = VioletPalette()
|
||||
#expect(blue.foreground != violet.foreground)
|
||||
#expect(blue.accent != violet.accent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NCurses Palette Tests
|
||||
|
||||
@Suite("NCurses Palette Tests")
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
/** Available phosphor themes matching TUIkit's built-in palettes. */
|
||||
export const themes = ["green", "amber", "white", "red"] as const;
|
||||
export const themes = ["green", "amber", "red", "violet", "blue", "white"] as const;
|
||||
export type Theme = (typeof themes)[number];
|
||||
|
||||
interface ThemeContextValue {
|
||||
@@ -18,7 +19,7 @@ interface ThemeContextValue {
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
theme: "amber",
|
||||
theme: "green",
|
||||
setTheme: () => {},
|
||||
});
|
||||
|
||||
@@ -33,13 +34,19 @@ function getInitialTheme(): Theme {
|
||||
const attr = document.documentElement.getAttribute("data-theme") as Theme | null;
|
||||
if (attr && themes.includes(attr)) return attr;
|
||||
}
|
||||
return "amber";
|
||||
return "green";
|
||||
}
|
||||
|
||||
/** Provides theme state and applies it to the document root via data-theme attribute. */
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
|
||||
|
||||
/** Sync React state with the theme the blocking script already applied to the DOM. */
|
||||
useEffect(() => {
|
||||
const actual = getInitialTheme();
|
||||
setThemeState(actual);
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback((next: Theme) => {
|
||||
setThemeState(next);
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
|
||||
import { useTheme, themes, type Theme } from "./ThemeProvider";
|
||||
|
||||
/** Color dot for each theme used in the switcher. */
|
||||
/** Color dot for each theme, matching Swift palette foreground colors. */
|
||||
const themeColors: Record<Theme, string> = {
|
||||
green: "#33ff33",
|
||||
amber: "#ffbf00",
|
||||
white: "#e4e4e7",
|
||||
red: "#ff3333",
|
||||
amber: "#ffaa00",
|
||||
red: "#ff4444",
|
||||
violet: "#bb77ff",
|
||||
blue: "#00aaff",
|
||||
white: "#e8e8e8",
|
||||
};
|
||||
|
||||
/** Theme labels for accessibility. */
|
||||
const themeLabels: Record<Theme, string> = {
|
||||
green: "Green",
|
||||
amber: "Amber",
|
||||
white: "White",
|
||||
red: "Red",
|
||||
violet: "Violet",
|
||||
blue: "Blue",
|
||||
white: "White",
|
||||
};
|
||||
|
||||
/** Compact theme switcher with colored dots and active indicator. */
|
||||
|
||||
+93
-45
@@ -9,61 +9,109 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Theme: Amber (default) */
|
||||
:root,
|
||||
[data-theme="amber"] {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #e8dcc2;
|
||||
--accent: #fbbf24;
|
||||
--accent-secondary: #f59e0b;
|
||||
--accent-glow: 251, 191, 36;
|
||||
--accent-glow-secondary: 245, 158, 11;
|
||||
--accent-glow-tertiary: 217, 119, 6;
|
||||
--headline-color: #ffbf00;
|
||||
--headline-glow: 255, 191, 0;
|
||||
--muted: #ad9a6e;
|
||||
--border: #27272a;
|
||||
--card: #111113;
|
||||
--card-hover: #18181b;
|
||||
}
|
||||
/* Themes: Green → Amber → Red → Violet → Blue → White */
|
||||
|
||||
/* Theme: Green */
|
||||
/* Theme: Green (default) */
|
||||
:root,
|
||||
[data-theme="green"] {
|
||||
--foreground: #c2e8d4;
|
||||
--accent: #6ee7b7;
|
||||
--accent-secondary: #34d399;
|
||||
--accent-glow: 110, 231, 183;
|
||||
--accent-glow-secondary: 52, 211, 153;
|
||||
--accent-glow-tertiary: 16, 185, 129;
|
||||
--background: #060a07;
|
||||
--foreground: #33ff33;
|
||||
--accent: #66ff66;
|
||||
--accent-secondary: #00cc00;
|
||||
--accent-glow: 102, 255, 102;
|
||||
--accent-glow-secondary: 0, 204, 0;
|
||||
--accent-glow-tertiary: 31, 143, 31;
|
||||
--headline-color: #33ff33;
|
||||
--headline-glow: 51, 255, 51;
|
||||
--muted: #7aad94;
|
||||
--muted: #1f8f1f;
|
||||
--border: #2d5a2d;
|
||||
--card: #0e271c;
|
||||
--card-hover: #0a1b13;
|
||||
}
|
||||
|
||||
/* Theme: White */
|
||||
[data-theme="white"] {
|
||||
--foreground: #d8d8da;
|
||||
--accent: #e4e4e7;
|
||||
--accent-secondary: #a1a1aa;
|
||||
--accent-glow: 228, 228, 231;
|
||||
--accent-glow-secondary: 161, 161, 170;
|
||||
--accent-glow-tertiary: 113, 113, 122;
|
||||
--headline-color: #f0f0f0;
|
||||
--headline-glow: 240, 240, 240;
|
||||
--muted: #8a8a90;
|
||||
/* Theme: Amber */
|
||||
[data-theme="amber"] {
|
||||
--background: #0a0706;
|
||||
--foreground: #ffaa00;
|
||||
--accent: #ffcc33;
|
||||
--accent-secondary: #cc9900;
|
||||
--accent-glow: 255, 204, 51;
|
||||
--accent-glow-secondary: 204, 153, 0;
|
||||
--accent-glow-tertiary: 143, 102, 0;
|
||||
--headline-color: #ffaa00;
|
||||
--headline-glow: 255, 170, 0;
|
||||
--muted: #8f6600;
|
||||
--border: #5a4a2d;
|
||||
--card: #251710;
|
||||
--card-hover: #1e110e;
|
||||
}
|
||||
|
||||
/* Theme: Red */
|
||||
[data-theme="red"] {
|
||||
--foreground: #e8c8c8;
|
||||
--accent: #f87171;
|
||||
--accent-secondary: #ef4444;
|
||||
--accent-glow: 248, 113, 113;
|
||||
--accent-glow-secondary: 239, 68, 68;
|
||||
--accent-glow-tertiary: 220, 38, 38;
|
||||
--headline-color: #ff3333;
|
||||
--headline-glow: 255, 51, 51;
|
||||
--muted: #ad7a7a;
|
||||
--background: #0a0606;
|
||||
--foreground: #ff4444;
|
||||
--accent: #ff6666;
|
||||
--accent-secondary: #cc4444;
|
||||
--accent-glow: 255, 68, 68;
|
||||
--accent-glow-secondary: 204, 51, 51;
|
||||
--accent-glow-tertiary: 143, 34, 34;
|
||||
--headline-color: #ff4444;
|
||||
--headline-glow: 255, 68, 68;
|
||||
--muted: #8f2222;
|
||||
--border: #5a2d2d;
|
||||
--card: #281112;
|
||||
--card-hover: #1e0f10;
|
||||
}
|
||||
|
||||
/* Theme: Violet */
|
||||
[data-theme="violet"] {
|
||||
--background: #08060a;
|
||||
--foreground: #bb77ff;
|
||||
--accent: #cc99ff;
|
||||
--accent-secondary: #9944dd;
|
||||
--accent-glow: 204, 153, 255;
|
||||
--accent-glow-secondary: 153, 68, 221;
|
||||
--accent-glow-tertiary: 119, 68, 170;
|
||||
--headline-color: #bb77ff;
|
||||
--headline-glow: 187, 119, 255;
|
||||
--muted: #7744aa;
|
||||
--border: #4a2d5a;
|
||||
--card: #1a1028;
|
||||
--card-hover: #120c1e;
|
||||
}
|
||||
|
||||
/* Theme: Blue (VFD) */
|
||||
[data-theme="blue"] {
|
||||
--background: #060708;
|
||||
--foreground: #00aaff;
|
||||
--accent: #33bbff;
|
||||
--accent-secondary: #0099dd;
|
||||
--accent-glow: 51, 187, 255;
|
||||
--accent-glow-secondary: 0, 153, 221;
|
||||
--accent-glow-tertiary: 0, 102, 153;
|
||||
--headline-color: #00aaff;
|
||||
--headline-glow: 0, 170, 255;
|
||||
--muted: #006699;
|
||||
--border: #2d4a5a;
|
||||
--card: #0e1825;
|
||||
--card-hover: #0a121c;
|
||||
}
|
||||
|
||||
/* Theme: White */
|
||||
[data-theme="white"] {
|
||||
--background: #06070a;
|
||||
--foreground: #e8e8e8;
|
||||
--accent: #ffffff;
|
||||
--accent-secondary: #c0c0c0;
|
||||
--accent-glow: 255, 255, 255;
|
||||
--accent-glow-secondary: 192, 192, 192;
|
||||
--accent-glow-tertiary: 120, 120, 120;
|
||||
--headline-color: #e8e8e8;
|
||||
--headline-glow: 232, 232, 232;
|
||||
--muted: #787878;
|
||||
--border: #484848;
|
||||
--card: #111a2a;
|
||||
--card-hover: #0d131d;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
+3
-3
@@ -36,13 +36,13 @@ export default function RootLayout({
|
||||
(function() {
|
||||
try {
|
||||
var stored = localStorage.getItem('tuikit-theme');
|
||||
if (stored && ['green','amber','white','red'].includes(stored)) {
|
||||
if (stored && ['green','amber','red','violet','blue','white'].includes(stored)) {
|
||||
document.documentElement.setAttribute('data-theme', stored);
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'amber');
|
||||
document.documentElement.setAttribute('data-theme', 'green');
|
||||
}
|
||||
} catch(e) {
|
||||
document.documentElement.setAttribute('data-theme', 'amber');
|
||||
document.documentElement.setAttribute('data-theme', 'green');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 908 KiB After Width: | Height: | Size: 867 KiB |
Reference in New Issue
Block a user