From 6de2677e9753f11f2559486da1f12699779bfc43 Mon Sep 17 00:00:00 2001 From: Chris <72043878+cwilson613@users.noreply.github.com> Date: Sat, 16 May 2026 11:47:10 -0400 Subject: [PATCH] feat: add tweakcn ui theming --- Cargo.lock | 20 +- Cargo.toml | 2 +- crates/flynt-app/assets/styles/components.css | 17 +- crates/flynt-app/assets/styles/layout.css | 2 +- crates/flynt-app/assets/styles/reset.css | 8 +- crates/flynt-app/assets/styles/settings.css | 95 +- crates/flynt-app/assets/styles/tabs.css | 1 + .../assets/vendor/tweakcn-presets.json | 1330 ++++++++++++++++- crates/flynt-app/src/app.rs | 25 +- crates/flynt-app/src/lib.rs | 1 + crates/flynt-app/src/theme.rs | 1042 +++++++++++++ crates/flynt-app/src/views/settings.rs | 205 ++- crates/flynt-core/src/models.rs | 29 + docs/onboarding.md | 2 +- docs/ui-guide.md | 4 +- 15 files changed, 2612 insertions(+), 171 deletions(-) create mode 100644 crates/flynt-app/src/theme.rs diff --git a/Cargo.lock b/Cargo.lock index 3f8d884d..d4789166 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2222,7 +2222,7 @@ dependencies = [ [[package]] name = "flynt-agent" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "async-trait", @@ -2248,7 +2248,7 @@ dependencies = [ [[package]] name = "flynt-app" -version = "0.10.5" +version = "0.10.6" dependencies = [ "agent-client-protocol", "anyhow", @@ -2291,7 +2291,7 @@ dependencies = [ [[package]] name = "flynt-core" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "base64", @@ -2313,7 +2313,7 @@ dependencies = [ [[package]] name = "flynt-flow" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "serde", @@ -2325,7 +2325,7 @@ dependencies = [ [[package]] name = "flynt-forge" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "async-trait", @@ -2347,14 +2347,14 @@ dependencies = [ [[package]] name = "flynt-git-helper" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", ] [[package]] name = "flynt-mobile" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "chrono", @@ -2378,7 +2378,7 @@ dependencies = [ [[package]] name = "flynt-models" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "chrono", @@ -2390,7 +2390,7 @@ dependencies = [ [[package]] name = "flynt-store" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "chrono", @@ -4739,7 +4739,7 @@ dependencies = [ [[package]] name = "omegon-design" -version = "0.10.5" +version = "0.10.6" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 1807d384..d39feeac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.10.5" +version = "0.10.6" edition = "2024" authors = ["Black Meridian"] diff --git a/crates/flynt-app/assets/styles/components.css b/crates/flynt-app/assets/styles/components.css index 3e3c6895..21a32026 100644 --- a/crates/flynt-app/assets/styles/components.css +++ b/crates/flynt-app/assets/styles/components.css @@ -734,6 +734,7 @@ align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-3); + border: 1px solid transparent; border-radius: var(--radius); font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); @@ -754,12 +755,12 @@ .btn-ghost { background: transparent; - color: var(--muted-foreground); + color: var(--control-muted-fg, var(--muted-foreground)); } .btn-ghost:hover { - background: var(--accent); - color: var(--foreground); + background: var(--control-hover); + color: var(--control-hover-fg, var(--foreground)); } .btn-destructive { @@ -773,20 +774,20 @@ /* ── Inputs ───────────────────────────────────────────────────────────────── */ .input { - background: var(--surface); - border: 1px solid var(--border); + background: var(--control-bg); + border: 1px solid var(--control-border); border-radius: var(--radius); padding: var(--space-2) var(--space-3); font-size: var(--font-size-base); - color: var(--foreground); + color: var(--control-fg); transition: border-color var(--duration-fast) var(--ease), box-shadow var(--duration-fast) var(--ease); width: 100%; } .input:focus { - border-color: var(--ring); - box-shadow: 0 0 0 2px rgba(42, 180, 200, 0.2); + border-color: var(--focus); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus) 24%, transparent); outline: none; } diff --git a/crates/flynt-app/assets/styles/layout.css b/crates/flynt-app/assets/styles/layout.css index 896aec85..a9453863 100644 --- a/crates/flynt-app/assets/styles/layout.css +++ b/crates/flynt-app/assets/styles/layout.css @@ -91,7 +91,7 @@ .toolbar-search { width: 100%; - background: var(--surface); + background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 4px var(--space-3); diff --git a/crates/flynt-app/assets/styles/reset.css b/crates/flynt-app/assets/styles/reset.css index 861e78ba..489ce318 100644 --- a/crates/flynt-app/assets/styles/reset.css +++ b/crates/flynt-app/assets/styles/reset.css @@ -82,16 +82,16 @@ button:active:not(:disabled) { } ::-webkit-scrollbar-thumb { - background: var(--dim); + background: var(--scrollbar-thumb, var(--dim)); border-radius: var(--radius-full); } ::-webkit-scrollbar-thumb:hover { - background: var(--muted-foreground); + background: var(--scrollbar-thumb-hover, var(--muted-foreground)); } /* Selections */ ::selection { - background: rgba(42, 180, 200, 0.25); - color: var(--foreground); + background: var(--selection); + color: var(--selection-foreground); } diff --git a/crates/flynt-app/assets/styles/settings.css b/crates/flynt-app/assets/styles/settings.css index 82f194c9..698f5c23 100644 --- a/crates/flynt-app/assets/styles/settings.css +++ b/crates/flynt-app/assets/styles/settings.css @@ -9,7 +9,8 @@ doesn't reflow content by ~15px. Without this, text rewrap flickers as the operator moves the mouse near the right edge. */ scrollbar-gutter: stable; - background: var(--background); + background: var(--panel-bg); + color: var(--panel-fg); } /* Split layout: sidebar (left) + scrollable content (right). @@ -47,8 +48,9 @@ /* ── Sidebar ────────────────────────────────────────────────────────────── */ .settings-sidebar { - border-right: 1px solid var(--border); - background: var(--background); + border-right: 1px solid var(--chrome-border); + background: var(--chrome-bg); + color: var(--chrome-fg); padding: var(--space-6) var(--space-3); overflow-y: auto; scrollbar-gutter: stable; @@ -63,7 +65,7 @@ text-align: left; padding: 6px 10px; font-size: var(--font-size-sm); - color: var(--foreground); + color: var(--chrome-fg); background: transparent; border: none; border-radius: 4px; @@ -71,12 +73,16 @@ transition: background var(--duration-fast) var(--ease); } -.settings-nav-item:hover { background: var(--surface, rgba(255,255,255,0.04)); } +.settings-nav-item:hover { + background: var(--control-hover); + color: var(--control-hover-fg, var(--foreground)); +} .settings-nav-item.active { - color: var(--primary); - background: var(--surface-active, rgba(42, 180, 200, 0.10)); + color: var(--selection-foreground); + background: var(--selection); font-weight: var(--font-weight-medium); + box-shadow: inset 3px 0 0 var(--primary); } .settings-nav-item.nested { @@ -148,7 +154,7 @@ color: var(--muted-foreground); margin-bottom: var(--space-4); padding-bottom: var(--space-2); - border-bottom: 1px solid var(--border-dim); + border-bottom: 1px solid var(--divider); } /* ── Row ──────────────────────────────────────────────────────────────────── */ @@ -186,6 +192,12 @@ max-width: 120px; } +.theme-import-input { + flex: 1 1 360px; + min-width: 260px; + max-width: 520px; +} + .settings-path { font-family: var(--font-mono); font-size: var(--font-size-sm); @@ -202,6 +214,38 @@ } /* ── Theme grid ───────────────────────────────────────────────────────────── */ +.theme-stack { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.theme-actions { + display: flex; + align-items: center; + gap: var(--space-3); + flex-wrap: wrap; +} + +.theme-actions .btn-ghost { + background: var(--control-bg); + border-color: var(--control-border); + color: var(--control-fg); +} + +.theme-actions .btn-ghost:hover { + background: var(--control-hover); + border-color: var(--focus); + color: var(--control-hover-fg, var(--foreground)); +} + +.settings-inline-msg { + font-size: var(--font-size-sm); +} + +.settings-inline-msg.ok { color: var(--success); } +.settings-inline-msg.err { color: var(--error); } + .theme-grid { display: flex; gap: var(--space-3); @@ -215,8 +259,9 @@ gap: var(--space-2); padding: var(--space-2); border-radius: var(--radius-md); - border: 2px solid var(--border); - background: var(--card); + border: 2px solid var(--panel-border); + background: var(--elevated-bg); + color: var(--elevated-fg); cursor: pointer; transition: border-color var(--duration-fast) var(--ease), box-shadow var(--duration-fast) var(--ease); @@ -273,7 +318,13 @@ .theme-name { font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); - color: var(--foreground); + color: var(--elevated-fg); +} + +.theme-kind { + margin-top: calc(var(--space-1) * -1); + font-size: var(--font-size-xs); + color: var(--muted-foreground); } .theme-active-badge { @@ -295,9 +346,9 @@ width: 40px; height: 32px; border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--card); - color: var(--muted-foreground); + border: 1px solid var(--control-border); + background: var(--control-bg); + color: var(--control-muted-fg, var(--muted-foreground)); font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); transition: border-color var(--duration-fast) var(--ease), @@ -312,8 +363,8 @@ .font-size-btn.active { border-color: var(--primary); - background: var(--accent); - color: var(--primary); + background: var(--selection); + color: var(--selection-foreground); } /* ── Sync radio ───────────────────────────────────────────────────────────── */ @@ -1214,6 +1265,10 @@ display: flex; flex-direction: column; position: relative; + background: var(--panel-bg); + color: var(--panel-fg); + border: 1px solid var(--panel-border); + box-shadow: 0 24px 80px color-mix(in srgb, var(--background) 70%, transparent); } /* Close button sits in the top-right of the modal, outside the split. */ @@ -1227,7 +1282,7 @@ border-radius: 4px; border: none; background: transparent; - color: var(--muted-foreground); + color: var(--control-muted-fg, var(--muted-foreground)); font-size: 22px; line-height: 1; cursor: pointer; @@ -1236,15 +1291,15 @@ } .settings-modal-close:hover { - background: var(--surface, rgba(255,255,255,0.06)); - color: var(--foreground); + background: var(--control-hover); + color: var(--control-hover-fg, var(--foreground)); } /* Settings root inside the modal: drop the outer height: 100% chain and let the modal dimensions drive sizing. */ .settings-modal-dialog .settings-root { height: 100%; - background: var(--background); + background: var(--panel-bg); } .settings-modal-dialog .settings-root-split { diff --git a/crates/flynt-app/assets/styles/tabs.css b/crates/flynt-app/assets/styles/tabs.css index 0349b9c2..a3054836 100644 --- a/crates/flynt-app/assets/styles/tabs.css +++ b/crates/flynt-app/assets/styles/tabs.css @@ -37,6 +37,7 @@ .tab.active { background: var(--background); color: var(--foreground); + box-shadow: inset 0 1px 0 var(--border); } /* Active indicator — bottom accent line like Obsidian */ .tab.active::after { diff --git a/crates/flynt-app/assets/vendor/tweakcn-presets.json b/crates/flynt-app/assets/vendor/tweakcn-presets.json index cb1a235c..1c5e5b99 100644 --- a/crates/flynt-app/assets/vendor/tweakcn-presets.json +++ b/crates/flynt-app/assets/vendor/tweakcn-presets.json @@ -1,30 +1,4 @@ { - "default": { - "name": "Default", - "description": "Neutral dark palette with cool primary.", - "vars": { - "--background": "#0c0c0c", - "--foreground": "#f5f5f5", - "--card": "#111111", - "--card-foreground": "#f5f5f5", - "--popover": "#111111", - "--popover-foreground": "#f5f5f5", - "--primary": "#6c8cff", - "--primary-foreground": "#0c0c0c", - "--secondary": "#1a1a1a", - "--secondary-foreground": "#f5f5f5", - "--muted": "#1a1a1a", - "--muted-foreground": "#888888", - "--accent": "#1a1a1a", - "--accent-foreground": "#f5f5f5", - "--destructive": "#ef4444", - "--destructive-foreground": "#fafafa", - "--border": "#2a2a2a", - "--input": "#2a2a2a", - "--ring": "#6c8cff", - "--radius": "6px" - } - }, "light": { "name": "Light", "description": "Clean light palette for daytime work.", @@ -51,56 +25,1264 @@ "--radius": "6px" } }, - "amber": { - "name": "Amber", - "description": "Warm dark palette with amber accent.", - "vars": { - "--background": "#0c0a09", - "--foreground": "#fafaf9", - "--card": "#171513", - "--card-foreground": "#fafaf9", - "--popover": "#171513", - "--popover-foreground": "#fafaf9", - "--primary": "#f59e0b", - "--primary-foreground": "#0c0a09", - "--secondary": "#292524", - "--secondary-foreground": "#fafaf9", - "--muted": "#292524", - "--muted-foreground": "#a8a29e", - "--accent": "#292524", - "--accent-foreground": "#fafaf9", - "--destructive": "#ef4444", - "--destructive-foreground": "#fafaf9", - "--border": "#3a3633", - "--input": "#3a3633", - "--ring": "#f59e0b", - "--radius": "8px" + "modern-minimal": { + "name": "Modern Minimal", + "description": "A theme based on the Modern Minimal color palette.", + "cssVars": { + "theme": { + "font-sans": "Inter, sans-serif", + "font-mono": "JetBrains Mono, monospace", + "font-serif": "Source Serif 4, serif", + "radius": "0.375rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(1.00 0 0)", + "foreground": "oklch(0.32 0 0)", + "card": "oklch(1.00 0 0)", + "card-foreground": "oklch(0.32 0 0)", + "popover": "oklch(1.00 0 0)", + "popover-foreground": "oklch(0.32 0 0)", + "primary": "oklch(0.62 0.19 259.81)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.97 0.00 264.54)", + "secondary-foreground": "oklch(0.45 0.03 256.80)", + "muted": "oklch(0.98 0.00 247.84)", + "muted-foreground": "oklch(0.55 0.02 264.36)", + "accent": "oklch(0.95 0.03 236.82)", + "accent-foreground": "oklch(0.38 0.14 265.52)", + "destructive": "oklch(0.64 0.21 25.33)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.93 0.01 264.53)", + "input": "oklch(0.93 0.01 264.53)", + "ring": "oklch(0.62 0.19 259.81)", + "chart-1": "oklch(0.62 0.19 259.81)", + "chart-2": "oklch(0.55 0.22 262.88)", + "chart-3": "oklch(0.49 0.22 264.38)", + "chart-4": "oklch(0.42 0.18 265.64)", + "chart-5": "oklch(0.38 0.14 265.52)", + "radius": "0.375rem", + "sidebar": "oklch(0.98 0.00 247.84)", + "sidebar-foreground": "oklch(0.32 0 0)", + "sidebar-primary": "oklch(0.62 0.19 259.81)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.95 0.03 236.82)", + "sidebar-accent-foreground": "oklch(0.38 0.14 265.52)", + "sidebar-border": "oklch(0.93 0.01 264.53)", + "sidebar-ring": "oklch(0.62 0.19 259.81)", + "font-sans": "Inter, sans-serif", + "font-serif": "Source Serif 4, serif", + "font-mono": "JetBrains Mono, monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.1", + "shadow-blur": "3px", + "shadow-spread": "0px", + "shadow-offset-x": "0", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-sm": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow-md": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)", + "shadow-lg": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)", + "shadow-xl": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)", + "shadow-2xl": "0 1px 3px 0px hsl(0 0% 0% / 0.25)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.20 0 0)", + "foreground": "oklch(0.92 0 0)", + "card": "oklch(0.27 0 0)", + "card-foreground": "oklch(0.92 0 0)", + "popover": "oklch(0.27 0 0)", + "popover-foreground": "oklch(0.92 0 0)", + "primary": "oklch(0.62 0.19 259.81)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.27 0 0)", + "secondary-foreground": "oklch(0.92 0 0)", + "muted": "oklch(0.27 0 0)", + "muted-foreground": "oklch(0.72 0 0)", + "accent": "oklch(0.38 0.14 265.52)", + "accent-foreground": "oklch(0.88 0.06 254.13)", + "destructive": "oklch(0.64 0.21 25.33)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.37 0 0)", + "input": "oklch(0.37 0 0)", + "ring": "oklch(0.62 0.19 259.81)", + "chart-1": "oklch(0.71 0.14 254.62)", + "chart-2": "oklch(0.62 0.19 259.81)", + "chart-3": "oklch(0.55 0.22 262.88)", + "chart-4": "oklch(0.49 0.22 264.38)", + "chart-5": "oklch(0.42 0.18 265.64)", + "radius": "0.375rem", + "sidebar": "oklch(0.20 0 0)", + "sidebar-foreground": "oklch(0.92 0 0)", + "sidebar-primary": "oklch(0.62 0.19 259.81)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.38 0.14 265.52)", + "sidebar-accent-foreground": "oklch(0.88 0.06 254.13)", + "sidebar-border": "oklch(0.37 0 0)", + "sidebar-ring": "oklch(0.62 0.19 259.81)", + "font-sans": "Inter, sans-serif", + "font-serif": "Source Serif 4, serif", + "font-mono": "JetBrains Mono, monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.1", + "shadow-blur": "3px", + "shadow-spread": "0px", + "shadow-offset-x": "0", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-sm": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow-md": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)", + "shadow-lg": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)", + "shadow-xl": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)", + "shadow-2xl": "0 1px 3px 0px hsl(0 0% 0% / 0.25)" + } } }, - "ocean": { - "name": "Ocean", - "description": "Cool blue palette with teal primary.", - "vars": { - "--background": "#0a0f1c", - "--foreground": "#e2e8f0", - "--card": "#0f172a", - "--card-foreground": "#e2e8f0", - "--popover": "#0f172a", - "--popover-foreground": "#e2e8f0", - "--primary": "#06b6d4", - "--primary-foreground": "#0a0f1c", - "--secondary": "#1e293b", - "--secondary-foreground": "#e2e8f0", - "--muted": "#1e293b", - "--muted-foreground": "#94a3b8", - "--accent": "#1e293b", - "--accent-foreground": "#e2e8f0", - "--destructive": "#ef4444", - "--destructive-foreground": "#fafafa", - "--border": "#334155", - "--input": "#334155", - "--ring": "#06b6d4", - "--radius": "4px" + "twitter": { + "name": "Twitter", + "description": "A theme based on the Twitter color palette.", + "cssVars": { + "theme": { + "font-sans": "Open Sans, sans-serif", + "font-mono": "Menlo, monospace", + "font-serif": "Georgia, serif", + "radius": "1.3rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(1.00 0 0)", + "foreground": "oklch(0.19 0.01 248.51)", + "card": "oklch(0.98 0.00 197.14)", + "card-foreground": "oklch(0.19 0.01 248.51)", + "popover": "oklch(1.00 0 0)", + "popover-foreground": "oklch(0.19 0.01 248.51)", + "primary": "oklch(0.67 0.16 245.00)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.19 0.01 248.51)", + "secondary-foreground": "oklch(1.00 0 0)", + "muted": "oklch(0.92 0.00 286.37)", + "muted-foreground": "oklch(0.19 0.01 248.51)", + "accent": "oklch(0.94 0.02 250.85)", + "accent-foreground": "oklch(0.67 0.16 245.00)", + "destructive": "oklch(0.62 0.24 25.77)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.93 0.01 231.66)", + "input": "oklch(0.98 0.00 228.78)", + "ring": "oklch(0.68 0.16 243.35)", + "chart-1": "oklch(0.67 0.16 245.00)", + "chart-2": "oklch(0.69 0.16 160.35)", + "chart-3": "oklch(0.82 0.16 82.53)", + "chart-4": "oklch(0.71 0.18 151.71)", + "chart-5": "oklch(0.59 0.22 10.58)", + "radius": "1.3rem", + "sidebar": "oklch(0.98 0.00 197.14)", + "sidebar-foreground": "oklch(0.19 0.01 248.51)", + "sidebar-primary": "oklch(0.67 0.16 245.00)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.94 0.02 250.85)", + "sidebar-accent-foreground": "oklch(0.67 0.16 245.00)", + "sidebar-border": "oklch(0.93 0.01 238.52)", + "sidebar-ring": "oklch(0.68 0.16 243.35)", + "font-sans": "Open Sans, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Menlo, monospace", + "shadow-color": "rgba(29,161,242,0.15)", + "shadow-opacity": "0", + "shadow-blur": "0px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "2px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-xs": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-sm": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 1px 2px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 1px 2px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-md": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 2px 4px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-lg": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 4px 6px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-xl": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 8px 10px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-2xl": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0 0 0)", + "foreground": "oklch(0.93 0.00 228.79)", + "card": "oklch(0.21 0.01 274.53)", + "card-foreground": "oklch(0.89 0 0)", + "popover": "oklch(0 0 0)", + "popover-foreground": "oklch(0.93 0.00 228.79)", + "primary": "oklch(0.67 0.16 245.01)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.96 0.00 219.53)", + "secondary-foreground": "oklch(0.19 0.01 248.51)", + "muted": "oklch(0.21 0 0)", + "muted-foreground": "oklch(0.56 0.01 247.97)", + "accent": "oklch(0.19 0.03 242.55)", + "accent-foreground": "oklch(0.67 0.16 245.01)", + "destructive": "oklch(0.62 0.24 25.77)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.27 0.00 248.00)", + "input": "oklch(0.30 0.03 244.82)", + "ring": "oklch(0.68 0.16 243.35)", + "chart-1": "oklch(0.67 0.16 245.00)", + "chart-2": "oklch(0.69 0.16 160.35)", + "chart-3": "oklch(0.82 0.16 82.53)", + "chart-4": "oklch(0.71 0.18 151.71)", + "chart-5": "oklch(0.59 0.22 10.58)", + "radius": "1.3rem", + "sidebar": "oklch(0.21 0.01 274.53)", + "sidebar-foreground": "oklch(0.89 0 0)", + "sidebar-primary": "oklch(0.68 0.16 243.35)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.19 0.03 242.55)", + "sidebar-accent-foreground": "oklch(0.67 0.16 245.01)", + "sidebar-border": "oklch(0.38 0.02 240.59)", + "sidebar-ring": "oklch(0.68 0.16 243.35)", + "font-sans": "Open Sans, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Menlo, monospace", + "shadow-color": "rgba(29,161,242,0.25)", + "shadow-opacity": "0", + "shadow-blur": "0px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "2px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-xs": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-sm": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 1px 2px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 1px 2px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-md": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 2px 4px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-lg": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 4px 6px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-xl": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00), 0px 8px 10px -1px hsl(202.82 89.12% 53.14% / 0.00)", + "shadow-2xl": "0px 2px 0px 0px hsl(202.82 89.12% 53.14% / 0.00)" + } + } + }, + "bubblegum": { + "name": "Bubblegum", + "description": "A theme based on the Bubblegum color palette.", + "cssVars": { + "theme": { + "font-sans": "Poppins, sans-serif", + "font-mono": "Fira Code, monospace", + "font-serif": "Lora, serif", + "radius": "0.4rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.94 0.02 345.70)", + "foreground": "oklch(0.47 0 0)", + "card": "oklch(0.95 0.05 86.89)", + "card-foreground": "oklch(0.47 0 0)", + "popover": "oklch(1.00 0 0)", + "popover-foreground": "oklch(0.47 0 0)", + "primary": "oklch(0.62 0.18 348.14)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.81 0.07 198.19)", + "secondary-foreground": "oklch(0.32 0 0)", + "muted": "oklch(0.88 0.05 212.10)", + "muted-foreground": "oklch(0.58 0 0)", + "accent": "oklch(0.92 0.08 87.67)", + "accent-foreground": "oklch(0.32 0 0)", + "destructive": "oklch(0.71 0.17 21.96)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.62 0.18 348.14)", + "input": "oklch(0.92 0 0)", + "ring": "oklch(0.70 0.16 350.75)", + "chart-1": "oklch(0.70 0.16 350.75)", + "chart-2": "oklch(0.82 0.08 212.09)", + "chart-3": "oklch(0.92 0.08 87.67)", + "chart-4": "oklch(0.80 0.11 348.18)", + "chart-5": "oklch(0.62 0.19 353.91)", + "radius": "0.4rem", + "sidebar": "oklch(0.91 0.04 343.09)", + "sidebar-foreground": "oklch(0.32 0 0)", + "sidebar-primary": "oklch(0.66 0.21 354.31)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.82 0.11 346.02)", + "sidebar-accent-foreground": "oklch(0.32 0 0)", + "sidebar-border": "oklch(0.95 0.03 307.17)", + "sidebar-ring": "oklch(0.66 0.21 354.31)", + "font-sans": "Poppins, sans-serif", + "font-serif": "Lora, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(325.78 58.18% 56.86% / 0.5)", + "shadow-opacity": "1.0", + "shadow-blur": "0px", + "shadow-spread": "0px", + "shadow-offset-x": "3px", + "shadow-offset-y": "3px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 0.50)", + "shadow-xs": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 0.50)", + "shadow-sm": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00), 3px 1px 2px -1px hsl(325.78 58.18% 56.86% / 1.00)", + "shadow": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00), 3px 1px 2px -1px hsl(325.78 58.18% 56.86% / 1.00)", + "shadow-md": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00), 3px 2px 4px -1px hsl(325.78 58.18% 56.86% / 1.00)", + "shadow-lg": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00), 3px 4px 6px -1px hsl(325.78 58.18% 56.86% / 1.00)", + "shadow-xl": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 1.00), 3px 8px 10px -1px hsl(325.78 58.18% 56.86% / 1.00)", + "shadow-2xl": "3px 3px 0px 0px hsl(325.78 58.18% 56.86% / 2.50)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.25 0.03 234.16)", + "foreground": "oklch(0.93 0.02 349.08)", + "card": "oklch(0.29 0.03 233.54)", + "card-foreground": "oklch(0.93 0.02 349.08)", + "popover": "oklch(0.29 0.03 233.54)", + "popover-foreground": "oklch(0.93 0.02 349.08)", + "primary": "oklch(0.92 0.08 87.67)", + "primary-foreground": "oklch(0.25 0.03 234.16)", + "secondary": "oklch(0.78 0.08 4.13)", + "secondary-foreground": "oklch(0.25 0.03 234.16)", + "muted": "oklch(0.27 0.01 255.58)", + "muted-foreground": "oklch(0.78 0.08 4.13)", + "accent": "oklch(0.67 0.10 356.98)", + "accent-foreground": "oklch(0.93 0.02 349.08)", + "destructive": "oklch(0.67 0.18 350.36)", + "destructive-foreground": "oklch(0.25 0.03 234.16)", + "border": "oklch(0.39 0.04 242.22)", + "input": "oklch(0.31 0.03 232.00)", + "ring": "oklch(0.70 0.09 201.87)", + "chart-1": "oklch(0.70 0.09 201.87)", + "chart-2": "oklch(0.78 0.08 4.13)", + "chart-3": "oklch(0.67 0.10 356.98)", + "chart-4": "oklch(0.44 0.07 217.08)", + "chart-5": "oklch(0.27 0.01 255.58)", + "radius": "0.4rem", + "sidebar": "oklch(0.23 0.03 235.97)", + "sidebar-foreground": "oklch(0.97 0.00 264.54)", + "sidebar-primary": "oklch(0.66 0.21 354.31)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.82 0.11 346.02)", + "sidebar-accent-foreground": "oklch(0.28 0.03 256.85)", + "sidebar-border": "oklch(0.37 0.03 259.73)", + "sidebar-ring": "oklch(0.66 0.21 354.31)", + "font-sans": "Poppins, sans-serif", + "font-serif": "Lora, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "#324859", + "shadow-opacity": "1.0", + "shadow-blur": "0px", + "shadow-spread": "0px", + "shadow-offset-x": "3px", + "shadow-offset-y": "3px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 0.50)", + "shadow-xs": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 0.50)", + "shadow-sm": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00), 3px 1px 2px -1px hsl(206.15 28.06% 27.25% / 1.00)", + "shadow": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00), 3px 1px 2px -1px hsl(206.15 28.06% 27.25% / 1.00)", + "shadow-md": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00), 3px 2px 4px -1px hsl(206.15 28.06% 27.25% / 1.00)", + "shadow-lg": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00), 3px 4px 6px -1px hsl(206.15 28.06% 27.25% / 1.00)", + "shadow-xl": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 1.00), 3px 8px 10px -1px hsl(206.15 28.06% 27.25% / 1.00)", + "shadow-2xl": "3px 3px 0px 0px hsl(206.15 28.06% 27.25% / 2.50)" + } + } + }, + "catppuccin": { + "name": "Catppuccin", + "description": "A theme based on the Catppuccin color palette.", + "cssVars": { + "theme": { + "font-sans": "Montserrat, sans-serif", + "font-mono": "Fira Code, monospace", + "font-serif": "Georgia, serif", + "radius": "0.35rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.96 0.01 264.53)", + "foreground": "oklch(0.44 0.04 279.33)", + "card": "oklch(1.00 0 0)", + "card-foreground": "oklch(0.44 0.04 279.33)", + "popover": "oklch(0.86 0.01 268.48)", + "popover-foreground": "oklch(0.44 0.04 279.33)", + "primary": "oklch(0.55 0.25 297.02)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.86 0.01 268.48)", + "secondary-foreground": "oklch(0.44 0.04 279.33)", + "muted": "oklch(0.91 0.01 264.51)", + "muted-foreground": "oklch(0.55 0.03 279.08)", + "accent": "oklch(0.68 0.14 235.38)", + "accent-foreground": "oklch(1.00 0 0)", + "destructive": "oklch(0.55 0.22 19.81)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.81 0.02 271.20)", + "input": "oklch(0.86 0.01 268.48)", + "ring": "oklch(0.55 0.25 297.02)", + "chart-1": "oklch(0.55 0.25 297.02)", + "chart-2": "oklch(0.68 0.14 235.38)", + "chart-3": "oklch(0.63 0.18 140.44)", + "chart-4": "oklch(0.69 0.20 42.43)", + "chart-5": "oklch(0.71 0.10 33.10)", + "radius": "0.35rem", + "sidebar": "oklch(0.93 0.01 264.52)", + "sidebar-foreground": "oklch(0.44 0.04 279.33)", + "sidebar-primary": "oklch(0.55 0.25 297.02)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.68 0.14 235.38)", + "sidebar-accent-foreground": "oklch(1.00 0 0)", + "sidebar-border": "oklch(0.81 0.02 271.20)", + "sidebar-ring": "oklch(0.55 0.25 297.02)", + "font-sans": "Montserrat, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(240 30% 25%)", + "shadow-opacity": "0.12", + "shadow-blur": "6px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "4px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 4px 6px 0px hsl(240 30% 25% / 0.06)", + "shadow-xs": "0px 4px 6px 0px hsl(240 30% 25% / 0.06)", + "shadow-sm": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12)", + "shadow": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12)", + "shadow-md": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 2px 4px -1px hsl(240 30% 25% / 0.12)", + "shadow-lg": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 4px 6px -1px hsl(240 30% 25% / 0.12)", + "shadow-xl": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 8px 10px -1px hsl(240 30% 25% / 0.12)", + "shadow-2xl": "0px 4px 6px 0px hsl(240 30% 25% / 0.30)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.22 0.03 284.06)", + "foreground": "oklch(0.88 0.04 272.28)", + "card": "oklch(0.24 0.03 283.91)", + "card-foreground": "oklch(0.88 0.04 272.28)", + "popover": "oklch(0.40 0.03 280.15)", + "popover-foreground": "oklch(0.88 0.04 272.28)", + "primary": "oklch(0.79 0.12 304.77)", + "primary-foreground": "oklch(0.24 0.03 283.91)", + "secondary": "oklch(0.48 0.03 278.64)", + "secondary-foreground": "oklch(0.88 0.04 272.28)", + "muted": "oklch(0.30 0.03 276.21)", + "muted-foreground": "oklch(0.75 0.04 273.93)", + "accent": "oklch(0.85 0.08 210.25)", + "accent-foreground": "oklch(0.24 0.03 283.91)", + "destructive": "oklch(0.76 0.13 2.76)", + "destructive-foreground": "oklch(0.24 0.03 283.91)", + "border": "oklch(0.32 0.03 281.98)", + "input": "oklch(0.32 0.03 281.98)", + "ring": "oklch(0.79 0.12 304.77)", + "chart-1": "oklch(0.79 0.12 304.77)", + "chart-2": "oklch(0.85 0.08 210.25)", + "chart-3": "oklch(0.86 0.11 142.72)", + "chart-4": "oklch(0.82 0.10 52.63)", + "chart-5": "oklch(0.92 0.02 30.49)", + "radius": "0.35rem", + "sidebar": "oklch(0.18 0.02 284.20)", + "sidebar-foreground": "oklch(0.88 0.04 272.28)", + "sidebar-primary": "oklch(0.79 0.12 304.77)", + "sidebar-primary-foreground": "oklch(0.24 0.03 283.91)", + "sidebar-accent": "oklch(0.85 0.08 210.25)", + "sidebar-accent-foreground": "oklch(0.24 0.03 283.91)", + "sidebar-border": "oklch(0.40 0.03 280.15)", + "sidebar-ring": "oklch(0.79 0.12 304.77)", + "font-sans": "Montserrat, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(240 30% 25%)", + "shadow-opacity": "0.12", + "shadow-blur": "6px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "4px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 4px 6px 0px hsl(240 30% 25% / 0.06)", + "shadow-xs": "0px 4px 6px 0px hsl(240 30% 25% / 0.06)", + "shadow-sm": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12)", + "shadow": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12)", + "shadow-md": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 2px 4px -1px hsl(240 30% 25% / 0.12)", + "shadow-lg": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 4px 6px -1px hsl(240 30% 25% / 0.12)", + "shadow-xl": "0px 4px 6px 0px hsl(240 30% 25% / 0.12), 0px 8px 10px -1px hsl(240 30% 25% / 0.12)", + "shadow-2xl": "0px 4px 6px 0px hsl(240 30% 25% / 0.30)" + } + } + }, + "graphite": { + "name": "Graphite", + "description": "A theme based on the Graphite color palette.", + "cssVars": { + "theme": { + "font-sans": "Inter, sans-serif", + "font-mono": "Fira Code, monospace", + "font-serif": "Georgia, serif", + "radius": "0.35rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.96 0 0)", + "foreground": "oklch(0.32 0 0)", + "card": "oklch(0.97 0 0)", + "card-foreground": "oklch(0.32 0 0)", + "popover": "oklch(0.97 0 0)", + "popover-foreground": "oklch(0.32 0 0)", + "primary": "oklch(0.49 0 0)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.91 0 0)", + "secondary-foreground": "oklch(0.32 0 0)", + "muted": "oklch(0.89 0 0)", + "muted-foreground": "oklch(0.51 0 0)", + "accent": "oklch(0.81 0 0)", + "accent-foreground": "oklch(0.32 0 0)", + "destructive": "oklch(0.56 0.19 25.86)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.86 0 0)", + "input": "oklch(0.91 0 0)", + "ring": "oklch(0.49 0 0)", + "chart-1": "oklch(0.49 0 0)", + "chart-2": "oklch(0.49 0.04 196.03)", + "chart-3": "oklch(0.65 0 0)", + "chart-4": "oklch(0.73 0 0)", + "chart-5": "oklch(0.81 0 0)", + "radius": "0.35rem", + "sidebar": "oklch(0.94 0 0)", + "sidebar-foreground": "oklch(0.32 0 0)", + "sidebar-primary": "oklch(0.49 0 0)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.81 0 0)", + "sidebar-accent-foreground": "oklch(0.32 0 0)", + "sidebar-border": "oklch(0.86 0 0)", + "sidebar-ring": "oklch(0.49 0 0)", + "font-sans": "Montserrat, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(0 0% 20% / 0.1)", + "shadow-opacity": "0.15", + "shadow-blur": "0px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "2px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 2px 0px 0px hsl(0 0% 20% / 0.07)", + "shadow-xs": "0px 2px 0px 0px hsl(0 0% 20% / 0.07)", + "shadow-sm": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 1px 2px -1px hsl(0 0% 20% / 0.15)", + "shadow": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 1px 2px -1px hsl(0 0% 20% / 0.15)", + "shadow-md": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 2px 4px -1px hsl(0 0% 20% / 0.15)", + "shadow-lg": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 4px 6px -1px hsl(0 0% 20% / 0.15)", + "shadow-xl": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 8px 10px -1px hsl(0 0% 20% / 0.15)", + "shadow-2xl": "0px 2px 0px 0px hsl(0 0% 20% / 0.38)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.22 0 0)", + "foreground": "oklch(0.89 0 0)", + "card": "oklch(0.24 0 0)", + "card-foreground": "oklch(0.89 0 0)", + "popover": "oklch(0.24 0 0)", + "popover-foreground": "oklch(0.89 0 0)", + "primary": "oklch(0.71 0 0)", + "primary-foreground": "oklch(0.22 0 0)", + "secondary": "oklch(0.31 0 0)", + "secondary-foreground": "oklch(0.89 0 0)", + "muted": "oklch(0.29 0 0)", + "muted-foreground": "oklch(0.60 0 0)", + "accent": "oklch(0.37 0 0)", + "accent-foreground": "oklch(0.89 0 0)", + "destructive": "oklch(0.66 0.15 22.17)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.33 0 0)", + "input": "oklch(0.31 0 0)", + "ring": "oklch(0.71 0 0)", + "chart-1": "oklch(0.71 0 0)", + "chart-2": "oklch(0.67 0.03 206.35)", + "chart-3": "oklch(0.55 0 0)", + "chart-4": "oklch(0.46 0 0)", + "chart-5": "oklch(0.37 0 0)", + "radius": "0.35rem", + "sidebar": "oklch(0.24 0 0)", + "sidebar-foreground": "oklch(0.89 0 0)", + "sidebar-primary": "oklch(0.71 0 0)", + "sidebar-primary-foreground": "oklch(0.22 0 0)", + "sidebar-accent": "oklch(0.37 0 0)", + "sidebar-accent-foreground": "oklch(0.89 0 0)", + "sidebar-border": "oklch(0.33 0 0)", + "sidebar-ring": "oklch(0.71 0 0)", + "font-sans": "Inter, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(0 0% 20% / 0.1)", + "shadow-opacity": "0.15", + "shadow-blur": "0px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "2px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 2px 0px 0px hsl(0 0% 20% / 0.07)", + "shadow-xs": "0px 2px 0px 0px hsl(0 0% 20% / 0.07)", + "shadow-sm": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 1px 2px -1px hsl(0 0% 20% / 0.15)", + "shadow": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 1px 2px -1px hsl(0 0% 20% / 0.15)", + "shadow-md": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 2px 4px -1px hsl(0 0% 20% / 0.15)", + "shadow-lg": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 4px 6px -1px hsl(0 0% 20% / 0.15)", + "shadow-xl": "0px 2px 0px 0px hsl(0 0% 20% / 0.15), 0px 8px 10px -1px hsl(0 0% 20% / 0.15)", + "shadow-2xl": "0px 2px 0px 0px hsl(0 0% 20% / 0.38)" + } + } + }, + "perpetuity": { + "name": "Perpetuity", + "description": "A theme based on the Perpetuity color palette.", + "cssVars": { + "theme": { + "font-sans": "Source Code Pro, monospace", + "font-mono": "Source Code Pro, monospace", + "font-serif": "Source Code Pro, monospace", + "radius": "0.125rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.95 0.01 197.01)", + "foreground": "oklch(0.38 0.06 212.66)", + "card": "oklch(0.97 0.01 197.07)", + "card-foreground": "oklch(0.38 0.06 212.66)", + "popover": "oklch(0.97 0.01 197.07)", + "popover-foreground": "oklch(0.38 0.06 212.66)", + "primary": "oklch(0.56 0.09 203.28)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.92 0.02 196.84)", + "secondary-foreground": "oklch(0.38 0.06 212.66)", + "muted": "oklch(0.93 0.01 196.97)", + "muted-foreground": "oklch(0.54 0.06 201.57)", + "accent": "oklch(0.90 0.03 201.89)", + "accent-foreground": "oklch(0.38 0.06 212.66)", + "destructive": "oklch(0.57 0.19 25.54)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.89 0.02 204.41)", + "input": "oklch(0.92 0.02 196.84)", + "ring": "oklch(0.56 0.09 203.28)", + "chart-1": "oklch(0.56 0.09 203.28)", + "chart-2": "oklch(0.64 0.10 201.59)", + "chart-3": "oklch(0.71 0.11 201.25)", + "chart-4": "oklch(0.77 0.10 201.18)", + "chart-5": "oklch(0.83 0.08 200.97)", + "radius": "0.125rem", + "sidebar": "oklch(0.93 0.02 205.32)", + "sidebar-foreground": "oklch(0.38 0.06 212.66)", + "sidebar-primary": "oklch(0.56 0.09 203.28)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.90 0.03 201.89)", + "sidebar-accent-foreground": "oklch(0.38 0.06 212.66)", + "sidebar-border": "oklch(0.89 0.02 204.41)", + "sidebar-ring": "oklch(0.56 0.09 203.28)", + "font-sans": "Courier New, monospace", + "font-serif": "Courier New, monospace", + "font-mono": "Courier New, monospace", + "shadow-color": "hsl(185 70% 30% / 0.15)", + "shadow-opacity": "0.15", + "shadow-blur": "2px", + "shadow-spread": "0px", + "shadow-offset-x": "1px", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "1px 1px 2px 0px hsl(185 70% 30% / 0.07)", + "shadow-xs": "1px 1px 2px 0px hsl(185 70% 30% / 0.07)", + "shadow-sm": "1px 1px 2px 0px hsl(185 70% 30% / 0.15), 1px 1px 2px -1px hsl(185 70% 30% / 0.15)", + "shadow": "1px 1px 2px 0px hsl(185 70% 30% / 0.15), 1px 1px 2px -1px hsl(185 70% 30% / 0.15)", + "shadow-md": "1px 1px 2px 0px hsl(185 70% 30% / 0.15), 1px 2px 4px -1px hsl(185 70% 30% / 0.15)", + "shadow-lg": "1px 1px 2px 0px hsl(185 70% 30% / 0.15), 1px 4px 6px -1px hsl(185 70% 30% / 0.15)", + "shadow-xl": "1px 1px 2px 0px hsl(185 70% 30% / 0.15), 1px 8px 10px -1px hsl(185 70% 30% / 0.15)", + "shadow-2xl": "1px 1px 2px 0px hsl(185 70% 30% / 0.38)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.21 0.02 224.45)", + "foreground": "oklch(0.85 0.13 195.04)", + "card": "oklch(0.23 0.03 216.07)", + "card-foreground": "oklch(0.85 0.13 195.04)", + "popover": "oklch(0.23 0.03 216.07)", + "popover-foreground": "oklch(0.85 0.13 195.04)", + "primary": "oklch(0.85 0.13 195.04)", + "primary-foreground": "oklch(0.21 0.02 224.45)", + "secondary": "oklch(0.38 0.06 216.50)", + "secondary-foreground": "oklch(0.85 0.13 195.04)", + "muted": "oklch(0.29 0.04 218.82)", + "muted-foreground": "oklch(0.66 0.10 195.05)", + "accent": "oklch(0.38 0.06 216.50)", + "accent-foreground": "oklch(0.85 0.13 195.04)", + "destructive": "oklch(0.62 0.21 25.81)", + "destructive-foreground": "oklch(0.96 0 0)", + "border": "oklch(0.38 0.06 216.50)", + "input": "oklch(0.38 0.06 216.50)", + "ring": "oklch(0.85 0.13 195.04)", + "chart-1": "oklch(0.85 0.13 195.04)", + "chart-2": "oklch(0.66 0.10 195.05)", + "chart-3": "oklch(0.58 0.08 195.07)", + "chart-4": "oklch(0.43 0.06 202.62)", + "chart-5": "oklch(0.31 0.05 204.16)", + "radius": "0.125rem", + "sidebar": "oklch(0.21 0.02 224.45)", + "sidebar-foreground": "oklch(0.85 0.13 195.04)", + "sidebar-primary": "oklch(0.85 0.13 195.04)", + "sidebar-primary-foreground": "oklch(0.21 0.02 224.45)", + "sidebar-accent": "oklch(0.38 0.06 216.50)", + "sidebar-accent-foreground": "oklch(0.85 0.13 195.04)", + "sidebar-border": "oklch(0.38 0.06 216.50)", + "sidebar-ring": "oklch(0.85 0.13 195.04)", + "font-sans": "Source Code Pro, monospace", + "font-serif": "Source Code Pro, monospace", + "font-mono": "Source Code Pro, monospace", + "shadow-color": "hsl(180 70% 60% / 0.2)", + "shadow-opacity": "0.2", + "shadow-blur": "2px", + "shadow-spread": "0px", + "shadow-offset-x": "1px", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "1px 1px 2px 0px hsl(180 70% 60% / 0.10)", + "shadow-xs": "1px 1px 2px 0px hsl(180 70% 60% / 0.10)", + "shadow-sm": "1px 1px 2px 0px hsl(180 70% 60% / 0.20), 1px 1px 2px -1px hsl(180 70% 60% / 0.20)", + "shadow": "1px 1px 2px 0px hsl(180 70% 60% / 0.20), 1px 1px 2px -1px hsl(180 70% 60% / 0.20)", + "shadow-md": "1px 1px 2px 0px hsl(180 70% 60% / 0.20), 1px 2px 4px -1px hsl(180 70% 60% / 0.20)", + "shadow-lg": "1px 1px 2px 0px hsl(180 70% 60% / 0.20), 1px 4px 6px -1px hsl(180 70% 60% / 0.20)", + "shadow-xl": "1px 1px 2px 0px hsl(180 70% 60% / 0.20), 1px 8px 10px -1px hsl(180 70% 60% / 0.20)", + "shadow-2xl": "1px 1px 2px 0px hsl(180 70% 60% / 0.50)" + } + } + }, + "supabase": { + "name": "Supabase", + "description": "A theme based on the Supabase color palette.", + "cssVars": { + "theme": { + "font-sans": "Outfit, sans-serif", + "font-mono": "monospace", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "radius": "0.5rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.99 0 0)", + "foreground": "oklch(0.20 0 0)", + "card": "oklch(0.99 0 0)", + "card-foreground": "oklch(0.20 0 0)", + "popover": "oklch(0.99 0 0)", + "popover-foreground": "oklch(0.44 0 0)", + "primary": "oklch(0.83 0.13 160.91)", + "primary-foreground": "oklch(0.26 0.01 166.46)", + "secondary": "oklch(0.99 0 0)", + "secondary-foreground": "oklch(0.20 0 0)", + "muted": "oklch(0.95 0 0)", + "muted-foreground": "oklch(0.24 0 0)", + "accent": "oklch(0.95 0 0)", + "accent-foreground": "oklch(0.24 0 0)", + "destructive": "oklch(0.55 0.19 32.73)", + "destructive-foreground": "oklch(0.99 0.00 17.21)", + "border": "oklch(0.90 0 0)", + "input": "oklch(0.97 0 0)", + "ring": "oklch(0.83 0.13 160.91)", + "chart-1": "oklch(0.83 0.13 160.91)", + "chart-2": "oklch(0.62 0.19 259.81)", + "chart-3": "oklch(0.61 0.22 292.72)", + "chart-4": "oklch(0.77 0.16 70.08)", + "chart-5": "oklch(0.70 0.15 162.48)", + "radius": "0.5rem", + "sidebar": "oklch(0.99 0 0)", + "sidebar-foreground": "oklch(0.55 0 0)", + "sidebar-primary": "oklch(0.83 0.13 160.91)", + "sidebar-primary-foreground": "oklch(0.26 0.01 166.46)", + "sidebar-accent": "oklch(0.95 0 0)", + "sidebar-accent-foreground": "oklch(0.24 0 0)", + "sidebar-border": "oklch(0.90 0 0)", + "sidebar-ring": "oklch(0.83 0.13 160.91)", + "font-sans": "Outfit, sans-serif", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "font-mono": "monospace", + "shadow-color": "#000000", + "shadow-opacity": "0.17", + "shadow-blur": "3px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "1px", + "letter-spacing": "0.025em", + "spacing": "0.25rem", + "shadow-2xs": "0px 1px 3px 0px hsl(0 0% 0% / 0.09)", + "shadow-xs": "0px 1px 3px 0px hsl(0 0% 0% / 0.09)", + "shadow-sm": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17)", + "shadow": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17)", + "shadow-md": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17)", + "shadow-lg": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17)", + "shadow-xl": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17)", + "shadow-2xl": "0px 1px 3px 0px hsl(0 0% 0% / 0.43)", + "tracking-normal": "0.025em" + }, + "dark": { + "background": "oklch(0.18 0 0)", + "foreground": "oklch(0.93 0.01 255.51)", + "card": "oklch(0.20 0 0)", + "card-foreground": "oklch(0.93 0.01 255.51)", + "popover": "oklch(0.26 0 0)", + "popover-foreground": "oklch(0.73 0 0)", + "primary": "oklch(0.44 0.10 156.76)", + "primary-foreground": "oklch(0.92 0.01 167.16)", + "secondary": "oklch(0.26 0 0)", + "secondary-foreground": "oklch(0.99 0 0)", + "muted": "oklch(0.24 0 0)", + "muted-foreground": "oklch(0.71 0 0)", + "accent": "oklch(0.31 0 0)", + "accent-foreground": "oklch(0.99 0 0)", + "destructive": "oklch(0.31 0.09 29.79)", + "destructive-foreground": "oklch(0.94 0.00 34.31)", + "border": "oklch(0.28 0 0)", + "input": "oklch(0.26 0 0)", + "ring": "oklch(0.80 0.18 151.71)", + "chart-1": "oklch(0.80 0.18 151.71)", + "chart-2": "oklch(0.71 0.14 254.62)", + "chart-3": "oklch(0.71 0.16 293.54)", + "chart-4": "oklch(0.84 0.16 84.43)", + "chart-5": "oklch(0.78 0.13 181.91)", + "radius": "0.5rem", + "sidebar": "oklch(0.18 0 0)", + "sidebar-foreground": "oklch(0.63 0 0)", + "sidebar-primary": "oklch(0.44 0.10 156.76)", + "sidebar-primary-foreground": "oklch(0.92 0.01 167.16)", + "sidebar-accent": "oklch(0.31 0 0)", + "sidebar-accent-foreground": "oklch(0.99 0 0)", + "sidebar-border": "oklch(0.28 0 0)", + "sidebar-ring": "oklch(0.80 0.18 151.71)", + "font-sans": "Outfit, sans-serif", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "font-mono": "monospace", + "shadow-color": "#000000", + "shadow-opacity": "0.17", + "shadow-blur": "3px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "1px", + "letter-spacing": "0.025em", + "spacing": "0.25rem", + "shadow-2xs": "0px 1px 3px 0px hsl(0 0% 0% / 0.09)", + "shadow-xs": "0px 1px 3px 0px hsl(0 0% 0% / 0.09)", + "shadow-sm": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17)", + "shadow": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17)", + "shadow-md": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17)", + "shadow-lg": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17)", + "shadow-xl": "0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17)", + "shadow-2xl": "0px 1px 3px 0px hsl(0 0% 0% / 0.43)" + } + } + }, + "cyberpunk": { + "name": "Cyberpunk", + "description": "A theme based on the Cyberpunk color palette.", + "cssVars": { + "theme": { + "font-sans": "Outfit, sans-serif", + "font-mono": "Fira Code, monospace", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "radius": "0.5rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.98 0.00 247.84)", + "foreground": "oklch(0.16 0.04 281.83)", + "card": "oklch(1.00 0 0)", + "card-foreground": "oklch(0.16 0.04 281.83)", + "popover": "oklch(1.00 0 0)", + "popover-foreground": "oklch(0.16 0.04 281.83)", + "primary": "oklch(0.67 0.29 341.41)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.96 0.02 286.02)", + "secondary-foreground": "oklch(0.16 0.04 281.83)", + "muted": "oklch(0.96 0.02 286.02)", + "muted-foreground": "oklch(0.16 0.04 281.83)", + "accent": "oklch(0.89 0.17 171.27)", + "accent-foreground": "oklch(0.16 0.04 281.83)", + "destructive": "oklch(0.65 0.23 34.04)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.92 0.01 225.09)", + "input": "oklch(0.92 0.01 225.09)", + "ring": "oklch(0.67 0.29 341.41)", + "chart-1": "oklch(0.67 0.29 341.41)", + "chart-2": "oklch(0.55 0.29 299.10)", + "chart-3": "oklch(0.84 0.15 209.29)", + "chart-4": "oklch(0.89 0.17 171.27)", + "chart-5": "oklch(0.92 0.19 101.41)", + "radius": "0.5rem", + "sidebar": "oklch(0.96 0.02 286.02)", + "sidebar-foreground": "oklch(0.16 0.04 281.83)", + "sidebar-primary": "oklch(0.67 0.29 341.41)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.89 0.17 171.27)", + "sidebar-accent-foreground": "oklch(0.16 0.04 281.83)", + "sidebar-border": "oklch(0.92 0.01 225.09)", + "sidebar-ring": "oklch(0.67 0.29 341.41)", + "font-sans": "Outfit, sans-serif", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.1", + "shadow-blur": "8px", + "shadow-spread": "-2px", + "shadow-offset-x": "0px", + "shadow-offset-y": "4px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 4px 8px -2px hsl(0 0% 0% / 0.05)", + "shadow-xs": "0px 4px 8px -2px hsl(0 0% 0% / 0.05)", + "shadow-sm": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 1px 2px -3px hsl(0 0% 0% / 0.10)", + "shadow": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 1px 2px -3px hsl(0 0% 0% / 0.10)", + "shadow-md": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 2px 4px -3px hsl(0 0% 0% / 0.10)", + "shadow-lg": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 4px 6px -3px hsl(0 0% 0% / 0.10)", + "shadow-xl": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 8px 10px -3px hsl(0 0% 0% / 0.10)", + "shadow-2xl": "0px 4px 8px -2px hsl(0 0% 0% / 0.25)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.16 0.04 281.83)", + "foreground": "oklch(0.95 0.01 260.73)", + "card": "oklch(0.25 0.06 281.14)", + "card-foreground": "oklch(0.95 0.01 260.73)", + "popover": "oklch(0.25 0.06 281.14)", + "popover-foreground": "oklch(0.95 0.01 260.73)", + "primary": "oklch(0.67 0.29 341.41)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.25 0.06 281.14)", + "secondary-foreground": "oklch(0.95 0.01 260.73)", + "muted": "oklch(0.25 0.06 281.14)", + "muted-foreground": "oklch(0.62 0.05 278.10)", + "accent": "oklch(0.89 0.17 171.27)", + "accent-foreground": "oklch(0.16 0.04 281.83)", + "destructive": "oklch(0.65 0.23 34.04)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.33 0.08 280.79)", + "input": "oklch(0.33 0.08 280.79)", + "ring": "oklch(0.67 0.29 341.41)", + "chart-1": "oklch(0.67 0.29 341.41)", + "chart-2": "oklch(0.55 0.29 299.10)", + "chart-3": "oklch(0.84 0.15 209.29)", + "chart-4": "oklch(0.89 0.17 171.27)", + "chart-5": "oklch(0.92 0.19 101.41)", + "radius": "0.5rem", + "sidebar": "oklch(0.16 0.04 281.83)", + "sidebar-foreground": "oklch(0.95 0.01 260.73)", + "sidebar-primary": "oklch(0.67 0.29 341.41)", + "sidebar-primary-foreground": "oklch(1.00 0 0)", + "sidebar-accent": "oklch(0.89 0.17 171.27)", + "sidebar-accent-foreground": "oklch(0.16 0.04 281.83)", + "sidebar-border": "oklch(0.33 0.08 280.79)", + "sidebar-ring": "oklch(0.67 0.29 341.41)", + "font-sans": "Outfit, sans-serif", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "font-mono": "Fira Code, monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.1", + "shadow-blur": "8px", + "shadow-spread": "-2px", + "shadow-offset-x": "0px", + "shadow-offset-y": "4px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 4px 8px -2px hsl(0 0% 0% / 0.05)", + "shadow-xs": "0px 4px 8px -2px hsl(0 0% 0% / 0.05)", + "shadow-sm": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 1px 2px -3px hsl(0 0% 0% / 0.10)", + "shadow": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 1px 2px -3px hsl(0 0% 0% / 0.10)", + "shadow-md": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 2px 4px -3px hsl(0 0% 0% / 0.10)", + "shadow-lg": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 4px 6px -3px hsl(0 0% 0% / 0.10)", + "shadow-xl": "0px 4px 8px -2px hsl(0 0% 0% / 0.10), 0px 8px 10px -3px hsl(0 0% 0% / 0.10)", + "shadow-2xl": "0px 4px 8px -2px hsl(0 0% 0% / 0.25)" + } + } + }, + "claude": { + "name": "Claude", + "description": "A theme based on the Claude color palette.", + "cssVars": { + "theme": { + "font-sans": "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + "font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "radius": "0.5rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.98 0.01 95.10)", + "foreground": "oklch(0.34 0.03 95.72)", + "card": "oklch(0.98 0.01 95.10)", + "card-foreground": "oklch(0.19 0.00 106.59)", + "popover": "oklch(1.00 0 0)", + "popover-foreground": "oklch(0.27 0.02 98.94)", + "primary": "oklch(0.62 0.14 39.04)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.92 0.01 92.99)", + "secondary-foreground": "oklch(0.43 0.02 98.60)", + "muted": "oklch(0.93 0.02 90.24)", + "muted-foreground": "oklch(0.61 0.01 97.42)", + "accent": "oklch(0.92 0.01 92.99)", + "accent-foreground": "oklch(0.27 0.02 98.94)", + "destructive": "oklch(0.19 0.00 106.59)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.88 0.01 97.36)", + "input": "oklch(0.76 0.02 98.35)", + "ring": "oklch(0.59 0.17 253.06)", + "chart-1": "oklch(0.56 0.13 43.00)", + "chart-2": "oklch(0.69 0.16 290.41)", + "chart-3": "oklch(0.88 0.03 93.13)", + "chart-4": "oklch(0.88 0.04 298.18)", + "chart-5": "oklch(0.56 0.13 42.06)", + "radius": "0.5rem", + "sidebar": "oklch(0.97 0.01 98.88)", + "sidebar-foreground": "oklch(0.36 0.01 106.65)", + "sidebar-primary": "oklch(0.62 0.14 39.04)", + "sidebar-primary-foreground": "oklch(0.99 0 0)", + "sidebar-accent": "oklch(0.92 0.01 92.99)", + "sidebar-accent-foreground": "oklch(0.33 0 0)", + "sidebar-border": "oklch(0.94 0 0)", + "sidebar-ring": "oklch(0.77 0 0)", + "font-sans": "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.1", + "shadow-blur": "3px", + "shadow-spread": "0px", + "shadow-offset-x": "0", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-sm": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow-md": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)", + "shadow-lg": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)", + "shadow-xl": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)", + "shadow-2xl": "0 1px 3px 0px hsl(0 0% 0% / 0.25)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0.27 0.00 106.64)", + "foreground": "oklch(0.81 0.01 93.01)", + "card": "oklch(0.27 0.00 106.64)", + "card-foreground": "oklch(0.98 0.01 95.10)", + "popover": "oklch(0.31 0.00 106.60)", + "popover-foreground": "oklch(0.92 0.00 106.48)", + "primary": "oklch(0.67 0.13 38.76)", + "primary-foreground": "oklch(1.00 0 0)", + "secondary": "oklch(0.98 0.01 95.10)", + "secondary-foreground": "oklch(0.31 0.00 106.60)", + "muted": "oklch(0.22 0.00 106.71)", + "muted-foreground": "oklch(0.77 0.02 99.07)", + "accent": "oklch(0.21 0.01 95.42)", + "accent-foreground": "oklch(0.97 0.01 98.88)", + "destructive": "oklch(0.64 0.21 25.33)", + "destructive-foreground": "oklch(1.00 0 0)", + "border": "oklch(0.36 0.01 106.89)", + "input": "oklch(0.43 0.01 100.22)", + "ring": "oklch(0.59 0.17 253.06)", + "chart-1": "oklch(0.56 0.13 43.00)", + "chart-2": "oklch(0.69 0.16 290.41)", + "chart-3": "oklch(0.21 0.01 95.42)", + "chart-4": "oklch(0.31 0.05 289.32)", + "chart-5": "oklch(0.56 0.13 42.06)", + "radius": "0.5rem", + "sidebar": "oklch(0.24 0.00 67.71)", + "sidebar-foreground": "oklch(0.81 0.01 93.01)", + "sidebar-primary": "oklch(0.33 0 0)", + "sidebar-primary-foreground": "oklch(0.99 0 0)", + "sidebar-accent": "oklch(0.17 0.00 106.62)", + "sidebar-accent-foreground": "oklch(0.81 0.01 93.01)", + "sidebar-border": "oklch(0.94 0 0)", + "sidebar-ring": "oklch(0.77 0 0)", + "font-sans": "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + "font-serif": "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif", + "font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.1", + "shadow-blur": "3px", + "shadow-spread": "0px", + "shadow-offset-x": "0", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-xs": "0 1px 3px 0px hsl(0 0% 0% / 0.05)", + "shadow-sm": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)", + "shadow-md": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)", + "shadow-lg": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)", + "shadow-xl": "0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)", + "shadow-2xl": "0 1px 3px 0px hsl(0 0% 0% / 0.25)" + } + } + }, + "vercel": { + "name": "Vercel", + "description": "A theme based on the Vercel color palette.", + "cssVars": { + "theme": { + "font-sans": "Geist, sans-serif", + "font-mono": "Geist Mono, monospace", + "font-serif": "Georgia, serif", + "radius": "0.5rem", + "tracking-tighter": "calc(var(--tracking-normal) - 0.05em)", + "tracking-tight": "calc(var(--tracking-normal) - 0.025em)", + "tracking-wide": "calc(var(--tracking-normal) + 0.025em)", + "tracking-wider": "calc(var(--tracking-normal) + 0.05em)", + "tracking-widest": "calc(var(--tracking-normal) + 0.1em)" + }, + "light": { + "background": "oklch(0.99 0 0)", + "foreground": "oklch(0 0 0)", + "card": "oklch(1 0 0)", + "card-foreground": "oklch(0 0 0)", + "popover": "oklch(0.99 0 0)", + "popover-foreground": "oklch(0 0 0)", + "primary": "oklch(0 0 0)", + "primary-foreground": "oklch(1 0 0)", + "secondary": "oklch(0.94 0 0)", + "secondary-foreground": "oklch(0 0 0)", + "muted": "oklch(0.97 0 0)", + "muted-foreground": "oklch(0.44 0 0)", + "accent": "oklch(0.94 0 0)", + "accent-foreground": "oklch(0 0 0)", + "destructive": "oklch(0.63 0.19 23.03)", + "destructive-foreground": "oklch(1 0 0)", + "border": "oklch(0.92 0 0)", + "input": "oklch(0.94 0 0)", + "ring": "oklch(0 0 0)", + "chart-1": "oklch(0.81 0.17 75.35)", + "chart-2": "oklch(0.55 0.22 264.53)", + "chart-3": "oklch(0.72 0 0)", + "chart-4": "oklch(0.92 0 0)", + "chart-5": "oklch(0.56 0 0)", + "radius": "0.5rem", + "sidebar": "oklch(0.99 0 0)", + "sidebar-foreground": "oklch(0 0 0)", + "sidebar-primary": "oklch(0 0 0)", + "sidebar-primary-foreground": "oklch(1 0 0)", + "sidebar-accent": "oklch(0.94 0 0)", + "sidebar-accent-foreground": "oklch(0 0 0)", + "sidebar-border": "oklch(0.94 0 0)", + "sidebar-ring": "oklch(0 0 0)", + "font-sans": "Geist, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Geist Mono, monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.18", + "shadow-blur": "2px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 1px 2px 0px hsl(0 0% 0% / 0.09)", + "shadow-xs": "0px 1px 2px 0px hsl(0 0% 0% / 0.09)", + "shadow-sm": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18)", + "shadow": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18)", + "shadow-md": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18)", + "shadow-lg": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18)", + "shadow-xl": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18)", + "shadow-2xl": "0px 1px 2px 0px hsl(0 0% 0% / 0.45)", + "tracking-normal": "0em" + }, + "dark": { + "background": "oklch(0 0 0)", + "foreground": "oklch(1 0 0)", + "card": "oklch(0.14 0 0)", + "card-foreground": "oklch(1 0 0)", + "popover": "oklch(0.18 0 0)", + "popover-foreground": "oklch(1 0 0)", + "primary": "oklch(1 0 0)", + "primary-foreground": "oklch(0 0 0)", + "secondary": "oklch(0.25 0 0)", + "secondary-foreground": "oklch(1 0 0)", + "muted": "oklch(0.23 0 0)", + "muted-foreground": "oklch(0.72 0 0)", + "accent": "oklch(0.32 0 0)", + "accent-foreground": "oklch(1 0 0)", + "destructive": "oklch(0.69 0.20 23.91)", + "destructive-foreground": "oklch(0 0 0)", + "border": "oklch(0.26 0 0)", + "input": "oklch(0.32 0 0)", + "ring": "oklch(0.72 0 0)", + "chart-1": "oklch(0.81 0.17 75.35)", + "chart-2": "oklch(0.58 0.21 260.84)", + "chart-3": "oklch(0.56 0 0)", + "chart-4": "oklch(0.44 0 0)", + "chart-5": "oklch(0.92 0 0)", + "radius": "0.5rem", + "sidebar": "oklch(0.18 0 0)", + "sidebar-foreground": "oklch(1 0 0)", + "sidebar-primary": "oklch(1 0 0)", + "sidebar-primary-foreground": "oklch(0 0 0)", + "sidebar-accent": "oklch(0.32 0 0)", + "sidebar-accent-foreground": "oklch(1 0 0)", + "sidebar-border": "oklch(0.32 0 0)", + "sidebar-ring": "oklch(0.72 0 0)", + "font-sans": "Geist, sans-serif", + "font-serif": "Georgia, serif", + "font-mono": "Geist Mono, monospace", + "shadow-color": "hsl(0 0% 0%)", + "shadow-opacity": "0.18", + "shadow-blur": "2px", + "shadow-spread": "0px", + "shadow-offset-x": "0px", + "shadow-offset-y": "1px", + "letter-spacing": "0em", + "spacing": "0.25rem", + "shadow-2xs": "0px 1px 2px 0px hsl(0 0% 0% / 0.09)", + "shadow-xs": "0px 1px 2px 0px hsl(0 0% 0% / 0.09)", + "shadow-sm": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18)", + "shadow": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18)", + "shadow-md": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18)", + "shadow-lg": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18)", + "shadow-xl": "0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18)", + "shadow-2xl": "0px 1px 2px 0px hsl(0 0% 0% / 0.45)" + } } } } diff --git a/crates/flynt-app/src/app.rs b/crates/flynt-app/src/app.rs index 889e4469..bd50dff0 100644 --- a/crates/flynt-app/src/app.rs +++ b/crates/flynt-app/src/app.rs @@ -33,15 +33,22 @@ pub fn App() -> Element { let current_runtime = ctx.runtime.read().clone(); - let theme = use_context_provider(|| { - Signal::new(ThemeName( - current_runtime.project.config.appearance.theme.clone(), - )) - }); + let operator_settings = current_runtime.omegon.load_operator_settings(); + let initial_theme = if operator_settings.ui_theme.active_theme.trim().is_empty() { + current_runtime.project.config.appearance.theme.clone() + } else { + operator_settings.ui_theme.active_theme.clone() + }; + let theme = use_context_provider(|| Signal::new(ThemeName(initial_theme))); let font_size = use_context_provider(|| Signal::new(current_runtime.project.config.appearance.font_size)); use_context_provider(|| Signal::new(current_runtime.omegon.load_project_profile())); - use_context_provider(|| Signal::new(current_runtime.omegon.load_operator_settings())); + use_context_provider(|| Signal::new(operator_settings.clone())); + use_context_provider(|| { + Signal::new(crate::theme::ThemeLibrary::from_operator( + &operator_settings, + )) + }); use_context_provider(|| Signal::new(None::)); use_context_provider(|| Signal::new(None::)); use_context_provider(|| Signal::new(None::)); @@ -464,6 +471,11 @@ pub fn App() -> Element { ctx.set_runtime(runtime_state_for_project_root(selected_root)); }; + let shell_theme_style = { + let library = use_context::>(); + library.read().active_vars(&theme.read().0) + }; + rsx! { // Prevent flash of unstyled content — hide body until theme loads document::Style { "body {{ opacity: 0; transition: opacity 0.1s; }} body.ready {{ opacity: 1; }}" } @@ -542,6 +554,7 @@ pub fn App() -> Element { div { class: "flynt-shell {font_size.read().css_class()}", "data-theme": "{theme.read().0}", + style: "{shell_theme_style}", tabindex: "0", onkeydown: move |e| { // ⌘P — command palette (command mode) diff --git a/crates/flynt-app/src/lib.rs b/crates/flynt-app/src/lib.rs index 1be40b1e..c9e5b87a 100644 --- a/crates/flynt-app/src/lib.rs +++ b/crates/flynt-app/src/lib.rs @@ -12,5 +12,6 @@ pub mod push_pipeline; pub mod self_update; pub mod state; pub mod sync_prereq; +pub mod theme; pub mod ui_state; pub mod views; diff --git a/crates/flynt-app/src/theme.rs b/crates/flynt-app/src/theme.rs new file mode 100644 index 00000000..5e49c8ce --- /dev/null +++ b/crates/flynt-app/src/theme.rs @@ -0,0 +1,1042 @@ +use flynt_core::models::{FlyntOperatorSettings, ImportedUiTheme}; +use std::collections::{BTreeMap, BTreeSet}; + +const ALPHARIUS_CSS: &str = include_str!("../assets/themes/alpharius.css"); +const TWEAKCN_PRESETS: &str = include_str!("../assets/vendor/tweakcn-presets.json"); +const BUILTIN_THEME_ORDER: &[&str] = &[ + "alpharius", + "light", + "modern-minimal", + "catppuccin", + "graphite", + "cyberpunk", + "perpetuity", + "vercel", + "supabase", + "claude", + "twitter", + "bubblegum", +]; + +#[derive(Clone, Debug, PartialEq)] +pub struct UiTheme { + pub id: String, + pub name: String, + pub description: String, + pub vars: BTreeMap, + pub builtin: bool, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ThemeLibrary { + pub themes: Vec, +} + +impl ThemeLibrary { + pub fn from_operator(settings: &FlyntOperatorSettings) -> Self { + let mut themes = bundled_themes(); + let mut seen: BTreeSet = themes.iter().map(|theme| theme.id.clone()).collect(); + + for imported in settings.ui_theme.imported_themes.iter().cloned() { + if imported.id.trim().is_empty() + || imported.vars.is_empty() + || seen.contains(&imported.id) + { + continue; + } + seen.insert(imported.id.clone()); + themes.push(UiTheme { + id: imported.id, + name: imported.name, + description: imported.description, + vars: complete_vars(imported.vars), + builtin: false, + }); + } + + Self { themes } + } + + pub fn active_vars(&self, active_id: &str) -> String { + self.theme(active_id) + .or_else(|| self.theme("alpharius")) + .map(inline_vars) + .unwrap_or_else(String::new) + } + + pub fn theme(&self, id: &str) -> Option<&UiTheme> { + self.themes.iter().find(|theme| theme.id == id) + } + + pub fn upsert_imported(&mut self, mut theme: UiTheme) -> String { + if self + .theme(&theme.id) + .is_some_and(|existing| existing.builtin) + { + theme.id = self.unique_imported_id(&theme.id); + } + let id = theme.id.clone(); + if let Some(existing) = self + .themes + .iter_mut() + .find(|existing| existing.id == theme.id) + { + *existing = theme; + } else { + self.themes.push(theme); + } + id + } + + pub fn imported_for_settings(&self) -> Vec { + self.themes + .iter() + .filter(|theme| !theme.builtin) + .map(|theme| ImportedUiTheme { + id: theme.id.clone(), + name: theme.name.clone(), + description: theme.description.clone(), + vars: theme.vars.clone(), + }) + .collect() + } + + fn unique_imported_id(&self, base: &str) -> String { + let base = format!("custom-{base}"); + if self.theme(&base).is_none() { + return base; + } + + for suffix in 2.. { + let candidate = format!("{base}-{suffix}"); + if self.theme(&candidate).is_none() { + return candidate; + } + } + unreachable!() + } +} + +pub fn import_tweakcn_theme(content: &str) -> anyhow::Result { + let value: serde_json::Value = serde_json::from_str(content)?; + if let Some(items) = value.get("items").and_then(|items| items.as_array()) { + for item in items { + let id_hint = item + .get("name") + .and_then(|name| name.as_str()) + .unwrap_or("imported"); + if let Some(theme) = parse_theme_value(id_hint, item, false) { + return Ok(theme); + } + } + } + + if let Some(theme) = parse_theme_value("imported", &value, false) { + return Ok(theme); + } + + if let Some(obj) = value.as_object() { + for (id, candidate) in obj { + if let Some(theme) = parse_theme_value(id, candidate, false) { + return Ok(theme); + } + } + } + + anyhow::bail!("No tweak.cn theme vars found"); +} + +pub async fn import_tweakcn_theme_from_locator(locator: &str) -> anyhow::Result { + let candidates = theme_url_candidates(locator)?; + let mut failures = Vec::new(); + + for url in candidates { + match fetch_tweakcn_theme_url(&url).await { + Ok(theme) => return Ok(theme), + Err(err) => failures.push(err), + } + } + + anyhow::bail!( + "No importable tweak.cn theme JSON found. Paste an exported JSON file, a built-in theme slug, or a public /r/themes/{{id}}.json URL. {}", + failures.join("; ") + ) +} + +async fn fetch_tweakcn_theme_url(url: &str) -> Result { + let response = reqwest::get(url) + .await + .map_err(|err| format!("{url}: {err}"))?; + let response = response + .error_for_status() + .map_err(|err| format!("{url}: {}", http_status_message(url, &err)))?; + let content = response + .text() + .await + .map_err(|err| format!("{url}: {err}"))?; + import_tweakcn_theme(&content).map_err(|err| format!("{url}: {err}")) +} + +fn theme_url_candidates(locator: &str) -> anyhow::Result> { + let mut candidates = Vec::new(); + let locator = locator.trim(); + if is_user_locator(locator) { + anyhow::bail!( + "tweak.cn does not expose public profile/user theme import. Paste a specific theme URL, theme ID, registry slug, or exported JSON instead." + ); + } + + if locator.starts_with("http://") || locator.starts_with("https://") { + if let Some(theme_id) = tweakcn_theme_id_from_url(locator) { + candidates.push(format!("https://tweakcn.com/r/themes/{theme_id}.json")); + } + candidates.push(locator.to_string()); + } else if !locator.is_empty() { + let slug = locator + .rsplit('/') + .next() + .unwrap_or(locator) + .trim_end_matches(".json"); + if !slug.trim().is_empty() { + candidates.push(format!( + "https://tweakcn.com/r/themes/{}.json", + sanitize_id(slug) + )); + } + } + + candidates.dedup(); + if candidates.is_empty() { + anyhow::bail!("Enter a tweak.cn JSON URL, public theme URL, built-in slug, or theme ID"); + } + Ok(candidates) +} + +fn http_status_message(url: &str, err: &reqwest::Error) -> String { + let Some(status) = err.status() else { + return err.to_string(); + }; + if status.is_server_error() && url.contains("/r/themes/") { + return format!( + "upstream registry returned {status}. tweak.cn currently does this for some community/private theme IDs; use the exported JSON or a built-in registry slug." + ); + } + format!("upstream returned {status}") +} + +fn is_user_locator(value: &str) -> bool { + value.trim().starts_with('@') + || value.contains("tweakcn.com/@") + || value.contains("tweakcn.com/u/") + || value.contains("tweakcn.com/users/") + || value.contains("tweakcn.com/user/") +} + +fn tweakcn_theme_id_from_url(url: &str) -> Option { + let marker = "/themes/"; + let (_, after) = url.split_once(marker)?; + let id = after + .split(['?', '#', '/']) + .next() + .unwrap_or_default() + .trim_end_matches(".json"); + (!id.is_empty()).then(|| id.to_string()) +} + +fn bundled_themes() -> Vec { + let mut themes = vec![UiTheme { + id: "alpharius".into(), + name: "Alpharius".into(), + description: "Flynt default dark operator theme.".into(), + vars: normalize_vars(base_vars()), + builtin: true, + }]; + + let parsed = match serde_json::from_str::(TWEAKCN_PRESETS) { + Ok(parsed) => parsed, + Err(_) => return themes, + }; + if let Some(obj) = parsed.as_object() { + for (id, value) in obj { + if let Some(theme) = parse_theme_value(id, value, true) { + themes.push(theme); + } + } + } + themes.sort_by_key(|theme| { + BUILTIN_THEME_ORDER + .iter() + .position(|id| *id == theme.id) + .unwrap_or(BUILTIN_THEME_ORDER.len()) + }); + + themes +} + +fn parse_theme_value(id_hint: &str, value: &serde_json::Value, builtin: bool) -> Option { + let vars = vars_object(value)?; + let name = value + .get("title") + .and_then(|name| name.as_str()) + .or_else(|| value.get("name").and_then(|name| name.as_str())) + .filter(|name| !name.trim().is_empty()) + .unwrap_or(id_hint); + let description = value + .get("description") + .and_then(|description| description.as_str()) + .unwrap_or(""); + + let id_source = value + .get("id") + .and_then(|id| id.as_str()) + .unwrap_or(if id_hint == "imported" { name } else { id_hint }); + let id = sanitize_id(id_source); + if id.is_empty() { + return None; + } + + Some(UiTheme { + id, + name: name.trim().to_string(), + description: description.trim().to_string(), + vars: complete_vars(vars), + builtin, + }) +} + +fn vars_object(value: &serde_json::Value) -> Option> { + let container = value + .get("vars") + .or_else(|| value.get("cssVars")) + .or_else(|| value.get("css_vars")) + .or_else(|| value.get("styles")) + .or_else(|| value.get("theme").and_then(|theme| theme.get("cssVars"))) + .or_else(|| value.get("theme").and_then(|theme| theme.get("vars"))) + .unwrap_or(value) + .as_object()?; + + let mut selected = BTreeMap::new(); + if let Some(theme_vars) = container.get("theme").and_then(|mode| mode.as_object()) { + collect_vars(theme_vars, &mut selected); + } + + let mode_vars = container + .get("dark") + .or_else(|| container.get("light")) + .and_then(|mode| mode.as_object()) + .unwrap_or(container); + collect_vars(mode_vars, &mut selected); + + (!selected.is_empty()).then_some(selected) +} + +fn collect_vars( + vars: &serde_json::Map, + out: &mut BTreeMap, +) { + for (key, value) in vars { + let Some(value) = value.as_str() else { + continue; + }; + let name = normalize_var_name(key); + if name.is_empty() || !is_safe_css_value(value) { + continue; + } + let value = normalize_css_value(&name, value); + out.insert(name, value); + } +} + +fn parse_css_vars(css: &str) -> BTreeMap { + let mut vars = BTreeMap::new(); + for line in css.lines() { + let trimmed = line.trim(); + if !trimmed.starts_with("--") { + continue; + } + if let Some((key, value)) = trimmed.trim_end_matches(';').split_once(':') { + let key = normalize_var_name(key); + let value = value.trim(); + if !key.is_empty() && is_safe_css_value(value) { + let value = normalize_css_value(&key, value); + vars.insert(key, value); + } + } + } + vars +} + +fn base_vars() -> BTreeMap { + parse_css_vars(ALPHARIUS_CSS) +} + +fn with_base_vars(vars: BTreeMap) -> BTreeMap { + let mut merged = base_vars(); + merged.extend(vars); + merged +} + +fn complete_vars(vars: BTreeMap) -> BTreeMap { + normalize_vars(with_base_vars(normalize_vars(vars))) +} + +fn normalize_vars(mut vars: BTreeMap) -> BTreeMap { + // Compatibility with current shadcn/tweak.cn sidebar token names. + alias_from_any( + &mut vars, + "--sidebar-bg", + &["--sidebar", "--sidebar-background"], + ); + alias_from_any(&mut vars, "--sidebar-fg", &["--sidebar-foreground"]); + alias_from_any(&mut vars, "--sidebar-border", &["--sidebar-border"]); + alias_from_any(&mut vars, "--sidebar-ring", &["--sidebar-ring", "--ring"]); + alias_from_any(&mut vars, "--sidebar-heading", &["--sidebar-foreground"]); + alias_from_any( + &mut vars, + "--sidebar-item-hover", + &["--sidebar-accent", "--accent", "--muted"], + ); + alias_from_any( + &mut vars, + "--sidebar-item-active", + &["--sidebar-accent", "--sidebar-primary", "--accent"], + ); + alias_from_any( + &mut vars, + "--sidebar-item-active-fg", + &[ + "--sidebar-accent-foreground", + "--sidebar-primary-foreground", + "--accent-foreground", + ], + ); + + // Compatibility with shadcn radius expansions. + alias_from_any(&mut vars, "--radius-sm", &["--radius"]); + alias_from_any(&mut vars, "--radius-md", &["--radius"]); + alias_from_any(&mut vars, "--radius-lg", &["--radius"]); + alias_from_any(&mut vars, "--radius-xl", &["--radius"]); + + // Future-facing Flynt surface vocabulary. Most current CSS still uses the + // legacy names below, but these give new UI work a richer stable contract. + alias_from_any(&mut vars, "--app-bg", &["--background"]); + alias_from_any(&mut vars, "--workspace-bg", &["--background"]); + alias_from_any(&mut vars, "--document-bg", &["--background"]); + alias_from_any(&mut vars, "--document-fg", &["--foreground"]); + alias_from_any( + &mut vars, + "--chrome-bg", + &["--secondary", "--muted", "--card"], + ); + alias_from_any( + &mut vars, + "--chrome-fg", + &["--secondary-foreground", "--foreground"], + ); + alias_from_any(&mut vars, "--chrome-border", &["--border"]); + alias_from_any(&mut vars, "--panel-bg", &["--card", "--popover"]); + alias_from_any( + &mut vars, + "--panel-fg", + &["--card-foreground", "--popover-foreground"], + ); + alias_from_any(&mut vars, "--panel-border", &["--border"]); + alias_from_any(&mut vars, "--panel-muted", &["--muted"]); + alias_from_any(&mut vars, "--elevated-bg", &["--popover", "--card"]); + alias_from_any( + &mut vars, + "--elevated-fg", + &["--popover-foreground", "--card-foreground"], + ); + alias_from_any(&mut vars, "--elevated-border", &["--border"]); + alias_from_any(&mut vars, "--overlay-bg", &["--popover", "--card"]); + alias_from_any( + &mut vars, + "--overlay-fg", + &["--popover-foreground", "--card-foreground"], + ); + alias_from_any(&mut vars, "--overlay-border", &["--border"]); + alias_from_any(&mut vars, "--control-bg", &["--background"]); + alias_from_any(&mut vars, "--control-fg", &["--foreground"]); + alias_from_any( + &mut vars, + "--control-muted-fg", + &["--muted-foreground", "--foreground"], + ); + alias_from_any(&mut vars, "--control-border", &["--input", "--border"]); + alias_from_any(&mut vars, "--control-hover", &["--accent", "--muted"]); + alias_from_any( + &mut vars, + "--control-hover-fg", + &["--accent-foreground", "--foreground"], + ); + alias_from_any(&mut vars, "--control-active", &["--accent", "--primary"]); + alias_from_any( + &mut vars, + "--control-active-fg", + &["--accent-foreground", "--primary-foreground"], + ); + alias_from_any(&mut vars, "--focus", &["--ring", "--primary"]); + alias_from_any(&mut vars, "--selection", &["--accent", "--primary"]); + alias_from_any( + &mut vars, + "--selection-foreground", + &["--accent-foreground", "--primary-foreground"], + ); + alias_from_any(&mut vars, "--link", &["--primary"]); + alias_from_any( + &mut vars, + "--link-hover", + &["--primary-bright", "--primary"], + ); + alias_from_any(&mut vars, "--divider", &["--border"]); + alias_from_any( + &mut vars, + "--scrollbar-thumb", + &["--muted-foreground", "--border"], + ); + alias_from_any( + &mut vars, + "--scrollbar-thumb-hover", + &["--foreground", "--muted-foreground"], + ); + + alias_from_any( + &mut vars, + "--surface", + &["--secondary", "--muted", "--card"], + ); + alias(&mut vars, "--surface-foreground", "--card-foreground"); + alias_from_any(&mut vars, "--surface-0", &["--background"]); + alias_from_any(&mut vars, "--surface-1", &["--surface", "--card"]); + alias_from_any(&mut vars, "--surface-active", &["--selection", "--accent"]); + alias_from_any(&mut vars, "--primary-muted", &["--primary"]); + alias_from_any(&mut vars, "--primary-bright", &["--primary"]); + alias(&mut vars, "--dim", "--muted-foreground"); + alias(&mut vars, "--border-dim", "--border"); + alias(&mut vars, "--success", "--primary"); + alias(&mut vars, "--success-foreground", "--primary-foreground"); + alias(&mut vars, "--warning", "--destructive"); + alias( + &mut vars, + "--warning-foreground", + "--destructive-foreground", + ); + alias(&mut vars, "--error", "--destructive"); + alias(&mut vars, "--error-foreground", "--destructive-foreground"); + alias(&mut vars, "--info", "--primary"); + alias(&mut vars, "--info-foreground", "--primary-foreground"); + alias_from_any( + &mut vars, + "--sidebar-bg", + &["--secondary", "--muted", "--card"], + ); + alias(&mut vars, "--sidebar-fg", "--card-foreground"); + alias(&mut vars, "--sidebar-border", "--border"); + alias(&mut vars, "--sidebar-item-hover", "--muted"); + alias(&mut vars, "--sidebar-item-active", "--accent"); + alias(&mut vars, "--sidebar-item-active-fg", "--accent-foreground"); + alias(&mut vars, "--sidebar-heading", "--muted-foreground"); + alias_from_any( + &mut vars, + "--toolbar-bg", + &["--secondary", "--muted", "--card"], + ); + alias(&mut vars, "--toolbar-border", "--border"); + alias(&mut vars, "--prose-body", "--foreground"); + alias(&mut vars, "--prose-heading", "--foreground"); + alias_from_any(&mut vars, "--prose-link", &["--link", "--primary"]); + alias_from_any( + &mut vars, + "--prose-link-hover", + &["--link-hover", "--primary"], + ); + alias(&mut vars, "--prose-code", "--primary"); + alias(&mut vars, "--prose-code-bg", "--muted"); + alias(&mut vars, "--prose-pre-bg", "--card"); + alias(&mut vars, "--prose-pre-border", "--border"); + alias(&mut vars, "--prose-blockquote", "--muted-foreground"); + alias(&mut vars, "--prose-blockquote-bar", "--primary"); + alias(&mut vars, "--prose-hr", "--border"); + alias(&mut vars, "--prose-table-border", "--border"); + alias(&mut vars, "--prose-table-head-bg", "--muted"); + alias(&mut vars, "--prose-th", "--foreground"); + alias(&mut vars, "--prose-td", "--foreground"); + alias(&mut vars, "--prose-task-check", "--primary"); + alias(&mut vars, "--prose-footnote", "--muted-foreground"); + alias(&mut vars, "--kanban-bg", "--background"); + alias(&mut vars, "--kanban-column-bg", "--card"); + alias(&mut vars, "--kanban-column-border", "--border"); + alias(&mut vars, "--kanban-card-bg", "--surface"); + alias(&mut vars, "--kanban-card-border", "--border"); + alias(&mut vars, "--kanban-card-hover", "--muted"); + alias(&mut vars, "--graph-bg", "--background"); + alias_from_any(&mut vars, "--graph-node", &["--chart-1", "--primary"]); + alias_from_any( + &mut vars, + "--graph-node-active", + &["--chart-2", "--primary"], + ); + alias_from_any( + &mut vars, + "--graph-node-muted", + &["--chart-3", "--muted-foreground"], + ); + alias_from_any(&mut vars, "--graph-edge", &["--chart-4", "--border"]); + alias_from_any( + &mut vars, + "--graph-edge-active", + &["--chart-5", "--primary"], + ); + alias(&mut vars, "--graph-label", "--muted-foreground"); + alias(&mut vars, "--graph-label-active", "--foreground"); + alias_from_any(&mut vars, "--chart-primary", &["--chart-1", "--primary"]); + alias_from_any( + &mut vars, + "--chart-secondary", + &["--chart-2", "--secondary"], + ); + alias_from_any(&mut vars, "--chart-tertiary", &["--chart-3", "--accent"]); + alias_from_any(&mut vars, "--chart-quaternary", &["--chart-4", "--muted"]); + alias_from_any( + &mut vars, + "--chart-quinary", + &["--chart-5", "--destructive"], + ); + alias_from_any(&mut vars, "--priority-low", &["--chart-3", "--muted"]); + alias_from_any(&mut vars, "--priority-low-fg", &["--muted-foreground"]); + alias_from_any( + &mut vars, + "--priority-medium", + &["--chart-2", "--secondary"], + ); + alias_from_any( + &mut vars, + "--priority-medium-fg", + &["--secondary-foreground"], + ); + alias_from_any(&mut vars, "--priority-high", &["--chart-4", "--warning"]); + alias_from_any(&mut vars, "--priority-high-fg", &["--warning-foreground"]); + alias_from_any( + &mut vars, + "--priority-critical", + &["--chart-5", "--destructive"], + ); + alias_from_any( + &mut vars, + "--priority-critical-fg", + &["--destructive-foreground"], + ); + alias(&mut vars, "--bg", "--background"); + alias(&mut vars, "--fg", "--foreground"); + alias(&mut vars, "--text", "--foreground"); + alias(&mut vars, "--text-muted", "--muted-foreground"); + alias(&mut vars, "--text-error", "--error"); + alias(&mut vars, "--bg-canvas", "--background"); + alias_from_any( + &mut vars, + "--bg-elevated", + &["--secondary", "--muted", "--card"], + ); + alias(&mut vars, "--bg-cell", "--card"); + vars +} + +fn alias(vars: &mut BTreeMap, target: &str, source: &str) { + if vars.contains_key(target) { + return; + } + if let Some(value) = vars.get(source).cloned() { + vars.insert(target.to_string(), value); + } +} + +fn alias_from_any(vars: &mut BTreeMap, target: &str, sources: &[&str]) { + if vars.contains_key(target) { + return; + } + for source in sources { + if let Some(value) = vars.get(*source).cloned() { + vars.insert(target.to_string(), value); + return; + } + } +} + +fn inline_vars(theme: &UiTheme) -> String { + theme + .vars + .iter() + .map(|(key, value)| format!("{key}: {value};")) + .collect::>() + .join(" ") +} + +fn normalize_var_name(name: &str) -> String { + let trimmed = name.trim(); + let without_prefix = trimmed.strip_prefix("--").unwrap_or(trimmed); + if without_prefix.is_empty() + || !without_prefix + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') + { + String::new() + } else { + format!("--{without_prefix}") + } +} + +fn sanitize_id(value: &str) -> String { + value + .trim() + .to_ascii_lowercase() + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' }) + .collect::() + .split('-') + .filter(|part| !part.is_empty()) + .collect::>() + .join("-") +} + +fn is_safe_css_value(value: &str) -> bool { + let value = value.trim(); + !value.is_empty() + && !value.contains(';') + && !value.contains('{') + && !value.contains('}') + && !value.to_ascii_lowercase().contains("javascript:") + && !value.to_ascii_lowercase().contains("expression(") + && !value.to_ascii_lowercase().contains("url(") + && !value.to_ascii_lowercase().contains("@import") +} + +fn normalize_css_value(key: &str, value: &str) -> String { + let value = value.trim(); + if key.contains("radius") + || key.contains("font") + || key.contains("space") + || key.contains("duration") + || key.contains("width") + || key.contains("height") + || key.contains("shadow") + || value.starts_with('#') + || value.contains('(') + || value.starts_with("var(") + { + return value.to_string(); + } + + if value.contains('%') + && value + .chars() + .all(|ch| ch.is_ascii_digit() || matches!(ch, '.' | '%' | ' ' | '/' | '-')) + { + return format!("hsl({value})"); + } + + value.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bundled_themes_include_alpharius_and_presets() { + let themes = bundled_themes(); + assert!(themes.iter().any(|theme| theme.id == "alpharius")); + assert!(themes.iter().any(|theme| theme.id == "light")); + assert!(themes.iter().any(|theme| theme.id == "modern-minimal")); + assert!(themes.iter().any(|theme| theme.id == "catppuccin")); + } + + #[test] + fn bundled_themes_follow_picker_order() { + let ids = bundled_themes() + .into_iter() + .take(6) + .map(|theme| theme.id) + .collect::>(); + + assert_eq!( + ids, + vec![ + "alpharius".to_string(), + "light".to_string(), + "modern-minimal".to_string(), + "catppuccin".to_string(), + "graphite".to_string(), + "cyberpunk".to_string() + ] + ); + } + + #[test] + fn imported_tweakcn_theme_normalizes_aliases() { + let theme = import_tweakcn_theme( + r##"{ + "name": "Custom", + "vars": { + "--background": "#111111", + "--foreground": "#eeeeee", + "--card": "#222222", + "--card-foreground": "#eeeeee", + "--primary": "#ff00aa", + "--primary-foreground": "#111111", + "--border": "#333333", + "--muted": "#202020", + "--muted-foreground": "#999999", + "--destructive": "#ff3333", + "--destructive-foreground": "#ffffff", + "--radius": "8px" + } + }"##, + ) + .unwrap(); + + assert_eq!(theme.id, "custom"); + assert_eq!(theme.vars.get("--surface").unwrap(), "#202020"); + assert_eq!(theme.vars.get("--sidebar-bg").unwrap(), "#202020"); + assert!(theme.vars.contains_key("--space-3")); + assert!(theme.vars.contains_key("--toolbar-height")); + } + + #[test] + fn unsafe_css_values_are_dropped() { + let theme = import_tweakcn_theme( + r##"{ + "name": "Custom", + "vars": { + "--background": "#111111", + "--foreground": "red; body { display: none }", + "--card": "url(https://example.com/card.png)" + } + }"##, + ) + .unwrap(); + + assert!(theme.vars.contains_key("--background")); + assert_eq!(theme.vars.get("--foreground").unwrap(), "#c4d8e4"); + assert_eq!(theme.vars.get("--card").unwrap(), "#0e1622"); + } + + #[test] + fn imported_tweakcn_theme_accepts_nested_css_vars() { + let theme = import_tweakcn_theme( + r##"{ + "name": "Nested", + "cssVars": { + "light": { + "background": "#ffffff", + "foreground": "#111111" + }, + "dark": { + "background": "#050505", + "foreground": "#eeeeee", + "card": "#111111" + } + } + }"##, + ) + .unwrap(); + + assert_eq!(theme.id, "nested"); + assert_eq!(theme.vars.get("--background").unwrap(), "#050505"); + assert_eq!(theme.vars.get("--surface").unwrap(), "#111111"); + assert!(theme.vars.contains_key("--font-sans")); + } + + #[test] + fn imported_tweakcn_registry_item_merges_theme_and_mode_vars() { + let theme = import_tweakcn_theme( + r##"{ + "name": "cyberpunk", + "title": "Cyberpunk", + "description": "A theme based on the Cyberpunk color palette.", + "cssVars": { + "theme": { + "font-sans": "Orbitron, sans-serif", + "radius": "0.125rem" + }, + "light": { + "background": "#ffffff", + "foreground": "#111111" + }, + "dark": { + "background": "#050505", + "foreground": "#39ff14", + "card": "#101010" + } + } + }"##, + ) + .unwrap(); + + assert_eq!(theme.id, "cyberpunk"); + assert_eq!(theme.name, "Cyberpunk"); + assert_eq!( + theme.vars.get("--font-sans").unwrap(), + "Orbitron, sans-serif" + ); + assert_eq!(theme.vars.get("--radius").unwrap(), "0.125rem"); + assert_eq!(theme.vars.get("--background").unwrap(), "#050505"); + assert_eq!(theme.vars.get("--foreground").unwrap(), "#39ff14"); + assert_eq!(theme.vars.get("--surface").unwrap(), "#101010"); + } + + #[test] + fn imported_tweakcn_theme_wraps_hsl_channels() { + let theme = import_tweakcn_theme( + r##"{ + "name": "HSL", + "vars": { + "background": "222.2 84% 4.9% / 0.95", + "foreground": "210 40% 98%", + "radius": "0.5rem" + } + }"##, + ) + .unwrap(); + + assert_eq!( + theme.vars.get("--background").unwrap(), + "hsl(222.2 84% 4.9% / 0.95)" + ); + assert_eq!(theme.vars.get("--radius").unwrap(), "0.5rem"); + } + + #[test] + fn imported_theme_ids_do_not_shadow_builtins() { + let settings = FlyntOperatorSettings::default(); + let mut library = ThemeLibrary::from_operator(&settings); + let theme = import_tweakcn_theme( + r##"{ + "id": "light", + "name": "Light", + "vars": { + "background": "#111111", + "foreground": "#eeeeee" + } + }"##, + ) + .unwrap(); + + let id = library.upsert_imported(theme); + + assert_eq!(id, "custom-light"); + assert!(library.theme("light").unwrap().builtin); + assert!(!library.theme("custom-light").unwrap().builtin); + } + + #[test] + fn theme_locator_builds_public_tweakcn_registry_candidates() { + let direct = theme_url_candidates("https://tweakcn.com/themes/cyberpunk").unwrap(); + assert_eq!(direct[0], "https://tweakcn.com/r/themes/cyberpunk.json"); + assert_eq!(direct[1], "https://tweakcn.com/themes/cyberpunk"); + + let slug = theme_url_candidates("catppuccin").unwrap(); + assert_eq!(slug[0], "https://tweakcn.com/r/themes/catppuccin.json"); + assert_eq!(slug.len(), 1); + } + + #[test] + fn theme_locator_does_not_treat_user_handles_as_theme_slugs() { + let err = theme_url_candidates("@cwilson613").unwrap_err(); + assert!(err.to_string().contains("profile/user theme import")); + + let by_id = theme_url_candidates("cmll14cgf000204ky5ms2fdgj").unwrap(); + assert_eq!( + by_id[0], + "https://tweakcn.com/r/themes/cmll14cgf000204ky5ms2fdgj.json" + ); + } + + #[test] + fn bundled_light_theme_separates_chrome_from_document_canvas() { + let library = ThemeLibrary::from_operator(&FlyntOperatorSettings::default()); + let theme = library.theme("light").unwrap(); + + assert_eq!(theme.vars.get("--background").unwrap(), "#ffffff"); + assert_eq!(theme.vars.get("--card").unwrap(), "#ffffff"); + assert_eq!(theme.vars.get("--surface").unwrap(), "#f3f4f6"); + assert_eq!(theme.vars.get("--sidebar-bg").unwrap(), "#f3f4f6"); + assert_eq!(theme.vars.get("--toolbar-bg").unwrap(), "#f3f4f6"); + } + + #[test] + fn imported_theme_maps_broad_tweakcn_surface_contract() { + let theme = import_tweakcn_theme( + r##"{ + "name": "Broad", + "vars": { + "background": "#ffffff", + "foreground": "#111111", + "card": "#fefefe", + "card-foreground": "#111111", + "popover": "#fafafa", + "popover-foreground": "#111111", + "secondary": "#f2f4f7", + "secondary-foreground": "#111827", + "muted": "#eef1f5", + "muted-foreground": "#667085", + "accent": "#e6f0ff", + "accent-foreground": "#12315f", + "border": "#d0d5dd", + "input": "#cbd5e1", + "ring": "#2563eb", + "primary": "#1d4ed8", + "primary-foreground": "#ffffff", + "destructive": "#dc2626", + "destructive-foreground": "#ffffff", + "sidebar": "#f8fafc", + "sidebar-foreground": "#0f172a", + "sidebar-accent": "#e2e8f0", + "sidebar-accent-foreground": "#0f172a", + "sidebar-primary": "#1d4ed8", + "sidebar-primary-foreground": "#ffffff", + "sidebar-border": "#cbd5e1", + "sidebar-ring": "#60a5fa", + "chart-1": "#2563eb", + "chart-2": "#16a34a", + "chart-3": "#f59e0b", + "chart-4": "#8b5cf6", + "chart-5": "#ef4444", + "radius": "0.625rem" + } + }"##, + ) + .unwrap(); + + assert_eq!(theme.vars.get("--sidebar-bg").unwrap(), "#f8fafc"); + assert_eq!(theme.vars.get("--sidebar-fg").unwrap(), "#0f172a"); + assert_eq!(theme.vars.get("--sidebar-item-active").unwrap(), "#e2e8f0"); + assert_eq!(theme.vars.get("--sidebar-ring").unwrap(), "#60a5fa"); + assert_eq!(theme.vars.get("--panel-bg").unwrap(), "#fefefe"); + assert_eq!(theme.vars.get("--overlay-bg").unwrap(), "#fafafa"); + assert_eq!(theme.vars.get("--control-border").unwrap(), "#cbd5e1"); + assert_eq!(theme.vars.get("--focus").unwrap(), "#2563eb"); + assert_eq!(theme.vars.get("--graph-node").unwrap(), "#2563eb"); + assert_eq!(theme.vars.get("--graph-node-active").unwrap(), "#16a34a"); + assert_eq!(theme.vars.get("--graph-edge").unwrap(), "#8b5cf6"); + assert_eq!(theme.vars.get("--priority-critical").unwrap(), "#ef4444"); + assert_eq!(theme.vars.get("--radius-lg").unwrap(), "0.625rem"); + } + + #[test] + fn invalid_variable_names_are_dropped() { + let theme = import_tweakcn_theme( + r##"{ + "name": "Bad Key", + "vars": { + "background; color:red": "#111111", + "foreground": "#eeeeee" + } + }"##, + ) + .unwrap(); + + assert!(!theme.vars.contains_key("--background; color:red")); + assert_eq!(theme.vars.get("--foreground").unwrap(), "#eeeeee"); + } +} diff --git a/crates/flynt-app/src/views/settings.rs b/crates/flynt-app/src/views/settings.rs index e6ab11e1..bb889884 100644 --- a/crates/flynt-app/src/views/settings.rs +++ b/crates/flynt-app/src/views/settings.rs @@ -5,6 +5,7 @@ use crate::{ components::identity_settings::IdentitySettingsSection, components::provider_settings::ProviderSettingsSection, state::{SettingsCategory, SettingsPage, ThemeName}, + theme::{ThemeLibrary, UiTheme, import_tweakcn_theme, import_tweakcn_theme_from_locator}, views::{IndexingScopesEditor, PublicationRulesEditor}, }; use dioxus::prelude::*; @@ -13,32 +14,6 @@ use flynt_core::models::{ OmegonProfile, ProjectConfig, SyncConfig, VisualizationConfig, }; -// ── Theme catalogue ─────────────────────────────────────────────────────────── -// Each entry describes a theme well enough to render a preview card without -// activating it. Hex values here are display-only — component CSS still uses vars. - -#[derive(PartialEq, Eq)] -struct ThemeEntry { - id: &'static str, - label: &'static str, - bg: &'static str, - surface: &'static str, - primary: &'static str, - text: &'static str, -} - -const THEMES: &[ThemeEntry] = &[ - ThemeEntry { - id: "alpharius", - label: "Alpharius", - bg: "#06080e", - surface: "#0e1622", - primary: "#2ab4c8", - text: "#c4d8e4", - }, - // Future themes registered here; CSS file added to app.css @imports. -]; - // ── Settings view ───────────────────────────────────────────────────────────── #[component] @@ -48,6 +23,8 @@ pub fn SettingsView() -> Element { // Appearance — reactive, applied immediately via context signals. let mut theme = use_context::>(); let mut font_sz = use_context::>(); + let mut theme_library = use_context::>(); + let mut operator_settings_state = use_context::>(); // Project + sync — local form state; persisted on explicit Save. let mut project_name = use_signal(|| ctx.project().config.project_name.clone()); @@ -114,8 +91,6 @@ pub fn SettingsView() -> Element { let publication_rules = use_signal(|| ctx.project().config.publication.rules.clone()); let _project_profile_state = use_context::>(); - let _operator_settings_state = use_context::>(); - // Indexing let mut write_frontmatter = use_signal(|| ctx.project().config.indexing.write_frontmatter); let indexing_scopes = use_signal(|| ctx.project().config.indexing.scopes.clone()); @@ -147,6 +122,8 @@ pub fn SettingsView() -> Element { let daemon_config = use_signal(|| ctx.omegon().load_operator_settings().agent_daemon.clone()); let mut save_msg = use_signal(|| Option::<(&'static str, &'static str)>::None); + let mut import_theme_msg = use_signal(|| Option::<(&'static str, String)>::None); + let mut theme_url = use_signal(String::new); let publish_msg = use_signal(|| Option::<(&'static str, String)>::None); let mut active_page = use_context::>(); @@ -154,6 +131,8 @@ pub fn SettingsView() -> Element { let project = ctx.project(); let omegon = ctx.omegon(); let omegon_for_save = omegon.clone(); + let omegon_for_file_theme_import = omegon.clone(); + let omegon_for_remote_theme_import = omegon.clone(); let publish_project = ctx.project(); let mut publish_msg_signal = publish_msg; let publish_preview = @@ -252,11 +231,7 @@ pub fn SettingsView() -> Element { d2_layout: d2_layout.read().clone(), d2_bin: { let bin = d2_bin.read().trim().to_string(); - if bin.is_empty() { - None - } else { - Some(bin) - } + if bin.is_empty() { None } else { Some(bin) } }, }, }; @@ -315,11 +290,14 @@ pub fn SettingsView() -> Element { // Persist daemon config alongside project config let mut operator = omegon_for_save.load_operator_settings(); operator.agent_daemon = daemon_config.read().clone(); + operator.ui_theme.active_theme = theme.read().0.clone(); + operator.ui_theme.imported_themes = theme_library.read().imported_for_settings(); if let Err(e) = omegon_for_save.save_operator_settings(&operator) { tracing::error!("save_operator_settings: {e}"); *save_msg.write() = Some(("err", "Operator settings save failed — check logs.")); return; } + *operator_settings_state.write() = operator; let mut profile = OmegonRuntimeContext::load_launcher_profile(); profile.flynt_update_channel = *flynt_update_channel.read(); @@ -387,16 +365,130 @@ pub fn SettingsView() -> Element { SettingsSection { heading: "Appearance", SettingsRow { label: "Theme", - hint: "Visual theme applied across the sidebar, editor, and rendered preview.", - div { class: "theme-grid", - for entry in THEMES { + hint: "Visual theme applied across the sidebar, editor, canvas, and rendered preview.", + div { class: "theme-stack", + div { class: "theme-actions", + button { + class: "btn btn-ghost", + onclick: move |_| { + let Some(path) = rfd::FileDialog::new() + .add_filter("tweak.cn theme", &["json"]) + .pick_file() + else { + return; + }; + + match std::fs::read_to_string(&path) + .map_err(anyhow::Error::from) + .and_then(|content| import_tweakcn_theme(&content)) + { + Ok(imported) => { + let imported_id = theme_library.write().upsert_imported(imported); + *theme.write() = ThemeName(imported_id.clone()); + + let imported_themes = theme_library.read().imported_for_settings(); + let operator_to_save = { + let mut operator = operator_settings_state.write(); + operator.ui_theme.active_theme = imported_id; + operator.ui_theme.imported_themes = imported_themes; + operator.clone() + }; + match omegon_for_file_theme_import.save_operator_settings(&operator_to_save) { + Ok(()) => { + *import_theme_msg.write() = Some(("ok", format!("Imported {}", path.display()))); + } + Err(err) => { + *import_theme_msg.write() = Some(("err", format!("Theme imported but save failed: {err}"))); + } + } + } + Err(err) => { + *import_theme_msg.write() = Some(("err", format!("Theme import failed: {err}"))); + } + } + }, + "Import tweak.cn JSON" + } + input { + class: "input settings-input theme-import-input", + placeholder: "theme URL, registry slug, or theme ID", + value: "{theme_url}", + oninput: move |e| *theme_url.write() = e.value(), + } + button { + class: "btn btn-ghost", + onclick: move |_| { + let locator = theme_url.read().trim().to_string(); + if locator.is_empty() { + *import_theme_msg.write() = Some(("err", "Enter a theme URL, registry slug, or theme ID.".into())); + return; + } + + let mut theme_library = theme_library; + let mut theme = theme; + let mut operator_settings_state = operator_settings_state; + let omegon = omegon_for_remote_theme_import.clone(); + let mut import_theme_msg = import_theme_msg; + spawn(async move { + *import_theme_msg.write() = Some(("ok", "Fetching theme…".into())); + match import_tweakcn_theme_from_locator(&locator).await { + Ok(imported) => { + let imported_id = theme_library.write().upsert_imported(imported); + *theme.write() = ThemeName(imported_id.clone()); + + let imported_themes = theme_library.read().imported_for_settings(); + let operator_to_save = { + let mut operator = operator_settings_state.write(); + operator.ui_theme.active_theme = imported_id; + operator.ui_theme.imported_themes = imported_themes; + operator.clone() + }; + match omegon.save_operator_settings(&operator_to_save) { + Ok(()) => { + *import_theme_msg.write() = Some(("ok", "Imported remote theme.".into())); + } + Err(err) => { + *import_theme_msg.write() = Some(("err", format!("Theme imported but save failed: {err}"))); + } + } + } + Err(err) => { + *import_theme_msg.write() = Some(("err", format!("Remote theme import failed: {err}"))); + } + } + }); + }, + "Add theme" + } + if let Some((kind, msg)) = import_theme_msg.read().as_ref() { + span { class: "settings-inline-msg {kind}", "{msg}" } + } + } + div { class: "theme-grid", + for entry in theme_library.read().themes.clone() { + { + let active = theme.read().0 == entry.id; + let omegon_for_theme_select = omegon.clone(); + rsx! { ThemeCard { entry, - active: theme.read().0 == entry.id, + active, on_select: move |id: String| { - *theme.write() = ThemeName(id); + *theme.write() = ThemeName(id.clone()); + let operator_to_save = { + let mut operator = operator_settings_state.write(); + operator.ui_theme.active_theme = id; + operator.ui_theme.imported_themes = theme_library.read().imported_for_settings(); + operator.clone() + }; + if let Err(err) = omegon_for_theme_select.save_operator_settings(&operator_to_save) { + *import_theme_msg.write() = Some(("err", format!("Theme save failed: {err}"))); + } }, } + } + } + } } } } @@ -1061,29 +1153,52 @@ fn SettingsRow( } #[component] -fn ThemeCard(entry: &'static ThemeEntry, active: bool, on_select: EventHandler) -> Element { +fn ThemeCard(entry: UiTheme, active: bool, on_select: EventHandler) -> Element { + let bg = entry + .vars + .get("--background") + .map(String::as_str) + .unwrap_or("#06080e"); + let surface = entry.vars.get("--card").map(String::as_str).unwrap_or(bg); + let primary = entry + .vars + .get("--primary") + .map(String::as_str) + .unwrap_or("#2ab4c8"); + let text = entry + .vars + .get("--foreground") + .map(String::as_str) + .unwrap_or("#c4d8e4"); + let badge = if entry.builtin { + "Built-in" + } else { + "Imported" + }; + rsx! { button { class: if active { "theme-card active" } else { "theme-card" }, - onclick: move |_| on_select.call(entry.id.to_string()), + onclick: move |_| on_select.call(entry.id.clone()), div { class: "theme-preview", - style: "background:{entry.bg}; border-color:{entry.primary};", + style: "background:{bg}; border-color:{primary};", div { class: "theme-preview-bar", - style: "background:{entry.surface};", + style: "background:{surface};", } div { class: "theme-preview-dot", - style: "background:{entry.primary};", + style: "background:{primary};", } span { class: "theme-preview-text", - style: "color:{entry.text};", + style: "color:{text};", "Aa" } } - span { class: "theme-name", "{entry.label}" } + span { class: "theme-name", "{entry.name}" } + span { class: "theme-kind", "{badge}" } if active { span { class: "theme-active-badge", "✓" } } diff --git a/crates/flynt-core/src/models.rs b/crates/flynt-core/src/models.rs index a2f18909..5495a235 100644 --- a/crates/flynt-core/src/models.rs +++ b/crates/flynt-core/src/models.rs @@ -778,6 +778,31 @@ pub struct OmegonProfileModel { pub model_id: String, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportedUiTheme { + pub id: String, + pub name: String, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub description: String, + #[serde(default)] + pub vars: std::collections::BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UiThemeSettings { + pub active_theme: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub imported_themes: Vec, +} + +impl Default for UiThemeSettings { + fn default() -> Self { + Self { active_theme: "alpharius".into(), imported_themes: Vec::new() } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FlyntOperatorSettings { @@ -797,6 +822,9 @@ pub struct FlyntOperatorSettings { /// Per-project agent daemon configuration — model, posture, vox channels. #[serde(default)] pub agent_daemon: crate::daemon::AgentDaemonConfig, + /// Operator-selected UI theme plus any imported tweak.cn themes. + #[serde(default)] + pub ui_theme: UiThemeSettings, /// Design canvas settings — default theme, grid, asset bootstrap state. /// Phase 1+2 ship the field with defaults; Phase 4 fills it in. #[serde(default)] @@ -814,6 +842,7 @@ impl Default for FlyntOperatorSettings { vox: VoxSettings::default(), acp_config: std::collections::HashMap::new(), agent_daemon: crate::daemon::AgentDaemonConfig::default(), + ui_theme: UiThemeSettings::default(), canvas: crate::canvas::CanvasSettings::default(), } } diff --git a/docs/onboarding.md b/docs/onboarding.md index 1db250f6..ed7029d5 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -224,7 +224,7 @@ Future flow (with StyreneIdentity): 3. **No crash reporting** — testers should report issues via Slack/GitHub with console logs 4. **No mobile onboarding** — project must be pre-configured 5. **SSH keys (if used) must be in ssh-agent** — passphrase-protected keys need `ssh-add` first. Using HTTPS with a personal access token avoids this entirely. -6. **Single theme** — "alpharius" is the only theme +6. **Theme import is desktop-only** — Flynt ships Alpharius, Light, and bundled upstream tweak.cn presets. Operators can import tweak.cn JSON themes, public theme URLs, registry slugs, or theme IDs from Settings → Appearance. Mobile still uses its own basic stylesheet. 7. **No Vim mode** — CodeMirror 6 without Vim extension 8. **Commit author is "Flynt "** — not yet linked to user identity (StyreneIdentity planned) 9. **iOS is read-heavy** — editing works but is basic (no CM6 on mobile, plain textarea) diff --git a/docs/ui-guide.md b/docs/ui-guide.md index 6cbdf11f..90ffa4ff 100644 --- a/docs/ui-guide.md +++ b/docs/ui-guide.md @@ -248,7 +248,7 @@ to generate raw Excalidraw element arrays. | Section | Field | Type | Notes | |---------|-------|------|-------| -| **Appearance** | Theme | Card grid | Currently: Alpharius only | +| **Appearance** | Theme | Card grid + import | Alpharius, Light, curated upstream tweak.cn presets, and operator-imported tweak.cn themes | | | Font size | Button group | Small / Medium / Large / XLarge | | **Project** | Name | Text input | | | | Location | Read-only path | | @@ -257,6 +257,8 @@ to generate raw Excalidraw element arrays. | | Branch (Git) | Text input | | | | Auto-commit (Git) | Number input | Seconds, minimum 30, 0 = manual only | +**Theme import:** Operators can import a tweak.cn JSON file, a public tweak.cn theme URL, a registry slug, or a theme ID from Appearance. Flynt normalizes the tweak.cn variables into the broader UI token set, applies the theme immediately, and persists theme selection plus imported themes in `.flynt/operator-settings.json`. + **Sync backend change on Save:** Triggers project migration. - **None → iCloud:** Copies all project files to iCloud Drive folder, updates config, switches runtime. **Synchronous — UI blocks during copy, no progress indicator.** Large projects may appear to freeze. - **None → Git:** Stays in current location, initializes git repo + adds remote.