-
+
@@ -50,7 +64,7 @@ provide('navigation', rootNavigation)
-
+
diff --git a/docs/app/assets/css/main.css b/docs/app/assets/css/main.css
index f811d1aee7..0c282889a3 100644
--- a/docs/app/assets/css/main.css
+++ b/docs/app/assets/css/main.css
@@ -12,6 +12,10 @@
--container-8xl: 90rem;
--font-sans: 'Public Sans', sans-serif;
+ /* Tailwind's preflight wires --font-sans and --font-mono itself, nothing
+ consumes --font-serif. Defaulting it to the body stack keeps the heading
+ rule below inert until the studio sets a serif of its own. */
+ --font-serif: var(--font-sans);
--color-green-50: #EFFDF5;
--color-green-100: #D9FBE8;
@@ -30,6 +34,28 @@
--ui-container: var(--container-8xl);
}
+/* The live counterpart of the export's heading rule, until v5 ships
+ --ui-font-heading. In `base` so `font-mono` on an h1 still wins. */
+@layer base {
+ h1, h2, h3, h4, h5, h6 {
+ font-family: var(--font-serif);
+ }
+}
+
+/* The theme studio floats its preview on a canvas recessed BEHIND the card,
+ so the two modes run opposite ways: a step down from white in light, past
+ --ui-bg in dark. Unlayered, to outrank the bg-default utility the Nuxt root
+ carries, and pointed at the neutral ramp so a themed palette reaches it. */
+body.theme-studio,
+body.theme-studio #__nuxt {
+ background-color: var(--ui-color-neutral-100);
+}
+
+.dark body.theme-studio,
+.dark body.theme-studio #__nuxt {
+ background-color: var(--ui-color-neutral-950);
+}
+
html[data-framework="nuxt"] .vue-only,
html[data-framework="vue"] .nuxt-only {
display: none;
diff --git a/docs/app/components/VersionMenu.vue b/docs/app/components/VersionMenu.vue
index 916093e42a..1ebeac9c5e 100644
--- a/docs/app/components/VersionMenu.vue
+++ b/docs/app/components/VersionMenu.vue
@@ -38,7 +38,7 @@ const items = computed(() => {
variant="subtle"
:trailing-icon="appConfig.ui.icons.chevronDown"
size="xs"
- class="-mb-[6px] font-semibold rounded-full truncate"
+ class="-mb-[6px] font-semibold rounded-xl truncate"
:class="[open && 'bg-primary/15']"
:ui="{
trailingIcon: ['transition-transform duration-200', open ? 'rotate-180' : undefined].filter(Boolean).join(' ')
diff --git a/docs/app/components/chat/Chat.vue b/docs/app/components/chat/Chat.vue
index 792a0c5280..a86da8df19 100644
--- a/docs/app/components/chat/Chat.vue
+++ b/docs/app/components/chat/Chat.vue
@@ -14,6 +14,9 @@ const { open, messages } = useChat()
const { open: searchOpen } = useContentSearch()
const { framework } = useFrameworks()
const { resetTheme, applyThemeSettings, hasCSSChanges, hasConfigChanges } = useTheme()
+// A preset is a whole ThemeDoc, so it rides applyDoc (reset, style axis, class
+// bundle) rather than the settings channel applyTheme uses.
+const { presets, applyPreset } = useThemeStudio()
const hasThemeChanges = computed(() => hasCSSChanges.value || hasConfigChanges.value)
@@ -32,6 +35,13 @@ function processThemeToolCalls() {
if (name === 'applyTheme' && part.input) {
_themeApplied.add(part.toolCallId)
applyThemeSettings(part.input as DocsChatTools['applyTheme']['input'])
+ } else if (name === 'applyPreset' && part.input) {
+ _themeApplied.add(part.toolCallId)
+ const { preset: id } = part.input as DocsChatTools['applyPreset']['input']
+ // The model picks from an enum, but a renamed preset in a stale
+ // conversation would still resolve to nothing.
+ const preset = presets.find(entry => entry.id === id)
+ if (preset) applyPreset(preset)
} else if (name === 'resetTheme') {
_themeApplied.add(part.toolCallId)
resetTheme()
@@ -129,6 +139,9 @@ function getToolMessage(state: ToolState, toolName: string, input: Record
preset.id === input.preset)?.name ?? input.preset} preset`,
'resetTheme': `${state === 'output-available' ? 'Reset' : 'Resetting'} theme to defaults`
}[toolName] || `${searchVerb} ${toolName}`
}
@@ -154,6 +167,7 @@ function getToolIcon(part: ToolPart): string {
'getComponentTheme': 'i-lucide-file-text',
'getThemeGuide': 'i-lucide-palette',
'applyTheme': 'i-lucide-palette',
+ 'applyPreset': 'i-lucide-palette',
'resetTheme': 'i-lucide-palette'
}
@@ -242,7 +256,7 @@ defineShortcuts({
icon="i-lucide-rotate-ccw"
color="neutral"
variant="ghost"
- @click="resetTheme"
+ @click="resetTheme()"
/>
diff --git a/docs/app/components/content/IconsTheme.vue b/docs/app/components/content/IconsTheme.vue
index 66d45e4895..3409c83e41 100644
--- a/docs/app/components/content/IconsTheme.vue
+++ b/docs/app/components/content/IconsTheme.vue
@@ -1,6 +1,6 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundAnalytics.vue b/docs/app/components/playground/PlaygroundAnalytics.vue
new file mode 100644
index 0000000000..63e9ce431e
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundAnalytics.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ Visitors
+
+
+ 418.2K
+
+
+
+ +10%
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundAnnouncement.vue b/docs/app/components/playground/PlaygroundAnnouncement.vue
new file mode 100644
index 0000000000..9b1262bb3f
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundAnnouncement.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+ Observability Plus
+
+
+ A faster, natural-language way to explore your data. Monitoring moves to the Pro plan in 2026.
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundAuthForm.vue b/docs/app/components/playground/PlaygroundAuthForm.vue
new file mode 100644
index 0000000000..ebcd15e764
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundAuthForm.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundBadges.vue b/docs/app/components/playground/PlaygroundBadges.vue
new file mode 100644
index 0000000000..d650755a60
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundBadges.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shipped
+
+
+ Pending
+
+
+ v4.9.0
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundButtons.vue b/docs/app/components/playground/PlaygroundButtons.vue
new file mode 100644
index 0000000000..068b5321a6
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundButtons.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundCalendar.vue b/docs/app/components/playground/PlaygroundCalendar.vue
new file mode 100644
index 0000000000..d8e887d915
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundCalendar.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundCard.vue b/docs/app/components/playground/PlaygroundCard.vue
new file mode 100644
index 0000000000..325991c694
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundCard.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundColorPicker.vue b/docs/app/components/playground/PlaygroundColorPicker.vue
new file mode 100644
index 0000000000..7c588ad94d
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundColorPicker.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+ {{ color }}
+
+
+
diff --git a/docs/app/components/playground/PlaygroundCommandPalette.vue b/docs/app/components/playground/PlaygroundCommandPalette.vue
new file mode 100644
index 0000000000..3febf35edd
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundCommandPalette.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundContributors.vue b/docs/app/components/playground/PlaygroundContributors.vue
new file mode 100644
index 0000000000..4e0ea80e32
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundContributors.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+ Contributors
+
+
+ Built by the community.
+
+
+
+ 312
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundDashboard.vue b/docs/app/components/playground/PlaygroundDashboard.vue
new file mode 100644
index 0000000000..156ca0b613
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundDashboard.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+ Total revenue
+
+
+ $48,200
+
+
+
+
+
+ +12.5%
+
+
+
+
+
+
+
+
+
+
+ Monthly goal
+ {{ progress }}%
+
+
+
+
+
+
+
+
+
+
+
+
+ +8 active now
+
+
+
diff --git a/docs/app/components/playground/PlaygroundEmpty.vue b/docs/app/components/playground/PlaygroundEmpty.vue
new file mode 100644
index 0000000000..be0aebc227
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundEmpty.vue
@@ -0,0 +1,9 @@
+
+
+
diff --git a/docs/app/components/playground/PlaygroundGoal.vue b/docs/app/components/playground/PlaygroundGoal.vue
new file mode 100644
index 0000000000..1ced36578e
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundGoal.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Investment goal
+
+
+
+
+ $24k
+ {{ percent }}% of $30k
+
+
+
+
+ Projected finish
+ Oct 2026
+
+
+
diff --git a/docs/app/components/playground/PlaygroundInput.vue b/docs/app/components/playground/PlaygroundInput.vue
new file mode 100644
index 0000000000..02c1a475c9
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundInput.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundInviteTeam.vue b/docs/app/components/playground/PlaygroundInviteTeam.vue
new file mode 100644
index 0000000000..3422cf9e9e
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundInviteTeam.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ Invite your team
+
+
+ Add members to your workspace.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundNavigation.vue b/docs/app/components/playground/PlaygroundNavigation.vue
new file mode 100644
index 0000000000..ee4db9fed6
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundNavigation.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundNotifications.vue b/docs/app/components/playground/PlaygroundNotifications.vue
new file mode 100644
index 0000000000..6dca00a59a
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundNotifications.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+ Notifications
+
+
+ Choose what you want to be notified about.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundPinInput.vue b/docs/app/components/playground/PlaygroundPinInput.vue
new file mode 100644
index 0000000000..56fa920d00
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundPinInput.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+ Verify your account
+
+
+ Enter the 6-digit code we sent you.
+
+
+
+
+
+
+ Didn't get a code?
+
+
+
diff --git a/docs/app/components/playground/PlaygroundPrompt.vue b/docs/app/components/playground/PlaygroundPrompt.vue
new file mode 100644
index 0000000000..047300e1d6
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundPrompt.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activeModel?.label }} {{ effort }}
+
+
+
+ {{ effort }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundReportBug.vue b/docs/app/components/playground/PlaygroundReportBug.vue
new file mode 100644
index 0000000000..42c13d3743
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundReportBug.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+ Report a bug
+
+
+ Help us fix issues faster.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundSavingsTargets.vue b/docs/app/components/playground/PlaygroundSavingsTargets.vue
new file mode 100644
index 0000000000..214e04d92b
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundSavingsTargets.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Savings targets
+
+
+ Active milestones for 2026.
+
+
+
+
+
+ {{ target.label }}
+ {{ target.value }}% achieved
+
+
+ {{ target.amount }}
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundShortcuts.vue b/docs/app/components/playground/PlaygroundShortcuts.vue
new file mode 100644
index 0000000000..c997147de1
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundShortcuts.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ Shortcuts
+
+
+ Work faster with the keyboard.
+
+
+
+
+
+
+
+ {{ shortcut.label }}
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundTabs.vue b/docs/app/components/playground/PlaygroundTabs.vue
new file mode 100644
index 0000000000..9c1b3f7297
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundTabs.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundTransactions.vue b/docs/app/components/playground/PlaygroundTransactions.vue
new file mode 100644
index 0000000000..5cbf87a954
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundTransactions.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ Recent transactions
+
+
+ Your latest account activity.
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ transaction.name }}
+
+
+ {{ transaction.category }}
+
+
+
+ {{ format(transaction.amount) }}
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundTransferFunds.vue b/docs/app/components/playground/PlaygroundTransferFunds.vue
new file mode 100644
index 0000000000..a936f02013
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundTransferFunds.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+ Transfer funds
+
+
+ Move money between your accounts.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Estimated arrival
+ Today
+
+
+ Transaction fee
+ $0.00
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/playground/PlaygroundTypography.vue b/docs/app/components/playground/PlaygroundTypography.vue
new file mode 100644
index 0000000000..ddc6c3fe52
--- /dev/null
+++ b/docs/app/components/playground/PlaygroundTypography.vue
@@ -0,0 +1,31 @@
+
+
+
+ Type scale
+
+
+
+ Designing with rhythm & hierarchy
+
+
+
+ A strong body style keeps long-form content readable and balances the visual weight of headings.
+
+
+
+ Light
+ Regular
+ Medium
+ Semibold
+ Bold
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-picker/ThemePicker.vue b/docs/app/components/theme-picker/ThemePicker.vue
deleted file mode 100644
index badfc7b9f7..0000000000
--- a/docs/app/components/theme-picker/ThemePicker.vue
+++ /dev/null
@@ -1,267 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- Primary
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Neutral
-
-
-
-
-
-
-
-
-
-
-
- Radius
-
-
-
-
-
-
-
-
-
-
-
- Font
-
-
-
-
-
-
-
-
-
-
-
- Icons
-
-
-
-
-
-
-
-
-
-
-
- Color Mode
-
-
-
-
-
-
-
-
-
-
-
- Export
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/app/components/theme-picker/ThemePickerButton.vue b/docs/app/components/theme-picker/ThemePickerButton.vue
deleted file mode 100644
index 8795cf891a..0000000000
--- a/docs/app/components/theme-picker/ThemePickerButton.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/app/components/theme-studio/ThemeStudioActionToggle.vue b/docs/app/components/theme-studio/ThemeStudioActionToggle.vue
new file mode 100644
index 0000000000..1ae76d426c
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioActionToggle.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioColorMenu.vue b/docs/app/components/theme-studio/ThemeStudioColorMenu.vue
new file mode 100644
index 0000000000..2e826f54da
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioColorMenu.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioColorSection.vue b/docs/app/components/theme-studio/ThemeStudioColorSection.vue
new file mode 100644
index 0000000000..b27d789548
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioColorSection.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioControls.vue b/docs/app/components/theme-studio/ThemeStudioControls.vue
new file mode 100644
index 0000000000..df6a75ec1e
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioControls.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioControlsColors.vue b/docs/app/components/theme-studio/ThemeStudioControlsColors.vue
new file mode 100644
index 0000000000..948adb7d2f
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioControlsColors.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioControlsDefaults.vue b/docs/app/components/theme-studio/ThemeStudioControlsDefaults.vue
new file mode 100644
index 0000000000..9e423de1fe
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioControlsDefaults.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ field.items.find(item => item.value === (groupVariants[field.key].value === 'default' ? field.stock : groupVariants[field.key].value))?.label }}
+
+
+
+
+
+
+
+ {{ item.label }} (Default)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioCurveEditor.vue b/docs/app/components/theme-studio/ThemeStudioCurveEditor.vue
new file mode 100644
index 0000000000..d382b5877c
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioCurveEditor.vue
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioDefaultSelect.vue b/docs/app/components/theme-studio/ThemeStudioDefaultSelect.vue
new file mode 100644
index 0000000000..72d3c32cbd
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioDefaultSelect.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+ {{ selected?.label }}
+
+
+
+ {{ asItem(item).label }} (Default)
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioFontOptions.vue b/docs/app/components/theme-studio/ThemeStudioFontOptions.vue
new file mode 100644
index 0000000000..6b262a94ab
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioFontOptions.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioFontPicker.vue b/docs/app/components/theme-studio/ThemeStudioFontPicker.vue
new file mode 100644
index 0000000000..7eb9dc5d95
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioFontPicker.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+ {{ model === 'inherit' ? 'Inherit' : model }}
+
+
+
+ {{ item.label }} (Default)
+
+
+
+ Grumpy wizards make toxic brew
+
+
+
+ {{ empty }}
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioIconOptions.vue b/docs/app/components/theme-studio/ThemeStudioIconOptions.vue
new file mode 100644
index 0000000000..5456814ac8
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioIconOptions.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioPaletteEditor.vue b/docs/app/components/theme-studio/ThemeStudioPaletteEditor.vue
new file mode 100644
index 0000000000..33fb5be1e7
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioPaletteEditor.vue
@@ -0,0 +1,727 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ swatchDetail.shade }}
+ pinned
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioPicker.vue b/docs/app/components/theme-studio/ThemeStudioPicker.vue
new file mode 100644
index 0000000000..54f70c1b24
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioPicker.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioPresetMenu.vue b/docs/app/components/theme-studio/ThemeStudioPresetMenu.vue
new file mode 100644
index 0000000000..0dec323cf8
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioPresetMenu.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.font }}
+
+ ·
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioRadiusOptions.vue b/docs/app/components/theme-studio/ThemeStudioRadiusOptions.vue
new file mode 100644
index 0000000000..ea97a7e728
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioRadiusOptions.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioRow.vue b/docs/app/components/theme-studio/ThemeStudioRow.vue
new file mode 100644
index 0000000000..cd273a49f9
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioRow.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioSection.vue b/docs/app/components/theme-studio/ThemeStudioSection.vue
new file mode 100644
index 0000000000..26dc1a07ba
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioSection.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioShadeGroup.vue b/docs/app/components/theme-studio/ThemeStudioShadeGroup.vue
new file mode 100644
index 0000000000..89649e0d9d
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioShadeGroup.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioShareModal.vue b/docs/app/components/theme-studio/ThemeStudioShareModal.vue
new file mode 100644
index 0000000000..3d8d82efb8
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioShareModal.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pane === 'css' ? 'main.css' : configLabel }}
+
+
+
+
+
+
{{ pane === 'css' ? css : config }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skipped
+
+
{{ skipped.join('\n') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioShuffleButton.vue b/docs/app/components/theme-studio/ThemeStudioShuffleButton.vue
new file mode 100644
index 0000000000..175b3cb126
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioShuffleButton.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioTokenShades.vue b/docs/app/components/theme-studio/ThemeStudioTokenShades.vue
new file mode 100644
index 0000000000..76f438c600
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioTokenShades.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioToolbarField.vue b/docs/app/components/theme-studio/ThemeStudioToolbarField.vue
new file mode 100644
index 0000000000..c7aa6aa86e
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioToolbarField.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioToolbarPopover.vue b/docs/app/components/theme-studio/ThemeStudioToolbarPopover.vue
new file mode 100644
index 0000000000..6543d0e442
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioToolbarPopover.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewA11y.vue b/docs/app/components/theme-studio/ThemeStudioViewA11y.vue
new file mode 100644
index 0000000000..384a328034
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewA11y.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+ Text on surfaces
+
+
+ WCAG 2.x contrast for every text role against every surface token, resolved from the live theme.
+ AA needs 4.5:1 (3:1 for large text), AAA needs 7:1.
+
+
+
+
+
+
+
+
+ {{ bg.label }}
+
+
+
+
+
+
+ {{ fg.label }}
+
+
+
+ Aa
+
+
+ {{ formatRatio(matrix[row]?.[col]?.ratio ?? null) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inverted text on solid surfaces
+
+
+ What solid buttons and badges render: the inverted text token on each semantic color.
+
+
+
+
+ {{ SOLIDS[index]!.label }}
+
+
+ {{ formatRatio(cell.ratio) }}
+
+
+
+
+
+
+
+
+
+ Button & badge variants
+
+
+ Every color × variant as it paints on the page background, tinted variants are measured against the
+ composited pixel, not the raw alias.
+
+
+
+
+
+
+
+
+ {{ variant }}
+
+
+
+
+
+
+ {{ color.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewChangelog.vue b/docs/app/components/theme-studio/ThemeStudioViewChangelog.vue
new file mode 100644
index 0000000000..82955462b9
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewChangelog.vue
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nuxt UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ version.intro }}
+
+
+
+ {{ section.title }}
+
+
+
+
+
+ {{ item.code }}{{ ' ' }}
+ {{ item.text }}
+
+
+
+
+
+
+ Contributors
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewChat.vue b/docs/app/components/theme-studio/ThemeStudioViewChat.vue
new file mode 100644
index 0000000000..5bf5ce9dae
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewChat.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+ Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Building a dashboard with Nuxt UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewDashboard.vue b/docs/app/components/theme-studio/ThemeStudioViewDashboard.vue
new file mode 100644
index 0000000000..1317c79ee0
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewDashboard.vue
@@ -0,0 +1,913 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ mail.from.name }}
+
+
+
+
+
{{ mail.date }}
+
+
+ {{ mail.subject }}
+
+
+ {{ mail.body }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedMail.from.name }}
+
+
+ {{ selectedMail.from.email }}
+
+
+
+
+
+ {{ selectedMail.datetime }}
+
+
+
+
+
+ {{ selectedMail.body }}
+
+
+
+
+
+
+
+
+
+ Reply to {{ selectedMail.from.name }} ({{ selectedMail.from.email }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ stat.value }}
+
+
+
+ {{ stat.variation > 0 ? '+' : '' }}{{ stat.variation }}%
+
+
+
+
+
+
+
+
+
+ Revenue
+
+
+ {{ revenueTotal }}
+
+
+
+
+
+
+
+
+
+
+ {{ tick }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedCustomersCount }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedCustomersCount }} of {{ displayedCustomers.length }} row(s) selected.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewDocs.vue b/docs/app/components/theme-studio/ThemeStudioViewDocs.vue
new file mode 100644
index 0000000000..ef35d5a306
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewDocs.vue
@@ -0,0 +1,322 @@
+
+
+
+
+
+
+
+
+
+ Docs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is only a basic example of what you can achieve with
+ Nuxt UI ,
+ you can tweak it to match your needs. The template uses several Nuxt modules underneath like
+ @nuxt/content for the content and nuxt-og-image
+ for social previews.
+
+
+
+ Learn more on how to take the most out of Nuxt UI!
+
+
+
+ Writing content
+
+
+
+ You can just start writing .md or .yml files in the
+ content/ directory to have your pages updated. The navigation will be
+ automatically generated in the left aside and in the mobile menu. You will also be able to go
+ through your content with full-text search.
+
+
+
+ Each page supports a few frontmatter fields to customize how it appears:
+
+
+
+
+
+ Key
+ Type
+ Description
+
+
+
+
+
+ {{ field.key }}
+
+
+ {{ field.type }}
+
+ {{ field.description }}
+
+
+
+
+
+ App Configuration
+
+
+
+ In addition to @nuxt/ui configuration through the
+ app.config.ts , this template lets you customize the
+ Header , Footer and the
+ Table of contents components.
+
+
+
+ Header
+
+
+
+ {{ appConfigCode }}
+
+
+
+ Footer
+
+
+
+ The footer follows the same pattern: update the bottom left credits, toggle the color mode
+ button and customize the social links, all from a single object in your app config.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Built with Nuxt UI • © 2026
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewEditor.vue b/docs/app/components/theme-studio/ThemeStudioViewEditor.vue
new file mode 100644
index 0000000000..c8bc9b995e
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewEditor.vue
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+ Editor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ e.stopPropagation()
+ const selected = onClick()
+ handlers.suggestion?.execute(editor, { pos: selected?.pos }).run()
+ }"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewLanding.vue b/docs/app/components/theme-studio/ThemeStudioViewLanding.vue
new file mode 100644
index 0000000000..efa153b135
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewLanding.vue
@@ -0,0 +1,492 @@
+
+
+
+
+
+
+
+
+
+ Telemetry
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Observe less.
+
+ Understand more.
+
+
+
+
+
+ Map your entire system in real-time, surface anomalies before they cascade, and eliminate the dashboards you never needed.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ segment.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Capabilities
+
+
+
+ Every signal, one surface.
+
+
+
+ No more tab-switching between metrics, traces, and logs. Correlate everything into a single explorable topology.
+
+
+
+
+
+
+
+
+ By the numbers
+
+
+
+ Built for scale you haven't hit yet.
+
+
+
+ Process billions of events per day across thousands of production environments with an architecture designed for the workloads of 2030.
+
+
+
+
+
+
+
+
+
+
+
+
+ Ready to stop firefighting?
+
+
+
+ Free for up to 5 services. No credit card. Deploys in under a minute.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Built with Nuxt UI • © 2026
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewPortfolio.vue b/docs/app/components/theme-studio/ThemeStudioViewPortfolio.vue
new file mode 100644
index 0000000000..bf65c4502c
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewPortfolio.vue
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.date }}
+
+
+
+ {{ item.position }}
+
+ {{ item.company.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Built with Nuxt UI • © 2026
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewSaas.vue b/docs/app/components/theme-studio/ThemeStudioViewSaas.vue
new file mode 100644
index 0000000000..039e2c0373
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewSaas.vue
@@ -0,0 +1,501 @@
+
+
+
+
+
+
+
+
+
+ SaaS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nuxt UI v4 is out
+
+
+
+
+ Ship your SaaS at light speed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Built with Nuxt UI • © 2026
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/components/theme-studio/ThemeStudioViewSwitcher.vue b/docs/app/components/theme-studio/ThemeStudioViewSwitcher.vue
new file mode 100644
index 0000000000..f19d4d40f9
--- /dev/null
+++ b/docs/app/components/theme-studio/ThemeStudioViewSwitcher.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ section.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/app/composables/useGoogleFonts.ts b/docs/app/composables/useGoogleFonts.ts
new file mode 100644
index 0000000000..2687c6ad1c
--- /dev/null
+++ b/docs/app/composables/useGoogleFonts.ts
@@ -0,0 +1,45 @@
+export interface GoogleFont {
+ name: string
+ category: string
+}
+
+/**
+ * The full Google Fonts catalog (family + category, popularity-ordered),
+ * served by /api/fonts.json, the metadata endpoint itself has no CORS
+ * headers. Fetched lazily the first time a font picker opens and shared
+ * through useState so every picker searches the same copy.
+ */
+export function useGoogleFonts() {
+ const catalog = useState('google-fonts-catalog', () => [])
+ const status = useState<'idle' | 'loading' | 'ready' | 'error'>('google-fonts-status', () => 'idle')
+
+ async function load() {
+ if (status.value === 'ready' || status.value === 'loading') return
+ status.value = 'loading'
+ try {
+ catalog.value = await $fetch('/api/fonts.json')
+ status.value = 'ready'
+ } catch {
+ status.value = 'error'
+ }
+ }
+
+ /** Popularity-ordered matches, prefix matches ahead of substring hits. */
+ function search(query: string, limit = 30): GoogleFont[] {
+ const q = query.trim().toLowerCase()
+ if (!q) return []
+ const starts: GoogleFont[] = []
+ const contains: GoogleFont[] = []
+ for (const font of catalog.value) {
+ const name = font.name.toLowerCase()
+ if (name.startsWith(q)) {
+ if (starts.push(font) >= limit) break
+ } else if (contains.length < limit && name.includes(q)) {
+ contains.push(font)
+ }
+ }
+ return [...starts, ...contains].slice(0, limit)
+ }
+
+ return { catalog, status, load, search }
+}
diff --git a/docs/app/composables/useSearch.ts b/docs/app/composables/useSearch.ts
index 7d6c90fd67..9459f8ef46 100644
--- a/docs/app/composables/useSearch.ts
+++ b/docs/app/composables/useSearch.ts
@@ -3,6 +3,11 @@ export function useSearch() {
const { frameworks } = useFrameworks()
const { track } = useAnalytics()
const { open, messages } = useChat()
+ // The ⌘K link glyphs follow the applied pack where an equivalent exists;
+ // the docs-nav-specific ones (square-play/-code/-function, panels, etc.)
+ // have no pack glyph and stay Lucide.
+ const studioIcons = useStudioIcons()
+ const extra = useStudioExtraIcons()
const searchTerm = ref('')
@@ -22,7 +27,7 @@ export function useSearch() {
const links = computed(() => [{
label: 'Ask AI',
- icon: 'i-lucide-bot-message-square',
+ icon: studioIcons.assistant,
kbds: ['meta', 'i'],
ui: {
itemLeadingIcon: 'group-data-highlighted:not-group-data-disabled:text-primary'
@@ -65,7 +70,7 @@ export function useSearch() {
}, {
label: 'Community',
description: 'Explore community projects and resources.',
- icon: 'i-lucide-globe',
+ icon: extra.globe,
to: '/community'
}, {
label: 'Playground',
@@ -87,7 +92,7 @@ export function useSearch() {
}, {
label: 'Team',
description: 'Meet the team behind the project.',
- icon: 'i-lucide-users',
+ icon: extra.users,
to: '/team'
}, {
label: 'Releases',
@@ -97,7 +102,7 @@ export function useSearch() {
}, {
label: 'GitHub',
description: 'Check out the repository on GitHub.',
- icon: 'i-simple-icons-github',
+ icon: extra.github,
to: 'https://github.com/nuxt/ui/releases',
target: '_blank'
}])
@@ -119,7 +124,7 @@ export function useSearch() {
},
items: [{
label: 'Ask AI',
- icon: 'i-lucide-bot-message-square',
+ icon: studioIcons.assistant,
ui: {
itemLeadingIcon: 'group-data-highlighted:not-group-data-disabled:text-primary'
},
diff --git a/docs/app/composables/useStudioIcons.ts b/docs/app/composables/useStudioIcons.ts
new file mode 100644
index 0000000000..f7d88ad6a7
--- /dev/null
+++ b/docs/app/composables/useStudioIcons.ts
@@ -0,0 +1,31 @@
+import { toReactive } from '@vueuse/core'
+import { studioIcons, STUDIO_EXTRA_DEFAULTS, studioExtraOverrides } from '../utils/theme/icons'
+
+/**
+ * The studio-chrome glyphs (toolbar controls, Ask-AI, theme picker) for the
+ * ACTIVE icon pack, so the studio's own UI skins to the applied theme. Falls
+ * back to Lucide for any unrecognized pack name. Returned as a reactive object
+ * (not a ref), like useStudioExtraIcons, so `studioIcons.undo` resolves
+ * directly in both script and templates.
+ */
+export function useStudioIcons() {
+ // `icon` already reports the stock pack until mounted (see useTheme), so the
+ // chrome inherits that hydration-safety for free.
+ const { icon } = useTheme()
+ return toReactive(computed(() => studioIcons[icon.value as keyof typeof studioIcons] ?? studioIcons.lucide))
+}
+
+/**
+ * The extended functional glyphs the preview demos use (dashboard nav, account
+ * menus, etc.) resolved for the active pack, each pack's override merged over
+ * the Lucide defaults, so an unset pack/key falls back to Lucide. Returned as a
+ * reactive object (not a ref) so `extra.home` resolves directly in both script
+ * arrays and templates, exactly like `appConfig.ui.icons.*`.
+ */
+export function useStudioExtraIcons() {
+ const { icon } = useTheme()
+ return toReactive(computed(() => ({
+ ...STUDIO_EXTRA_DEFAULTS,
+ ...(studioExtraOverrides[icon.value as keyof typeof studioExtraOverrides] ?? {})
+ })))
+}
diff --git a/docs/app/composables/useTheme.ts b/docs/app/composables/useTheme.ts
index 9d643c8c54..02d72e5bbb 100644
--- a/docs/app/composables/useTheme.ts
+++ b/docs/app/composables/useTheme.ts
@@ -1,35 +1,34 @@
import { defu } from 'defu'
-import { useLocalStorage } from '@vueuse/core'
-import { themeIcons, cssVariableDefaults } from '../utils/theme'
+import { THEME_TAG_IDS, THEME_STATE_KEYS } from '../utils/theme/storage'
+import type { FontPrefs } from '../utils/theme/storage'
+import { FONT_WEIGHT_DEFAULTS } from '../utils/theme/studio'
+import { themeIcons } from '../utils/theme/icons'
+import { cssVariableDefaults } from '../utils/theme/tokens'
+import { generateCSS, generateConfig, mergeUi, isDefaultStyle, styleTokens, DEFAULT_COLORS, THEME_DEFAULTS, SEMANTIC_ALIASES, LIBRARY_TOKEN_DEFAULTS } from '../utils/theme/engine'
+import type { ThemeDoc, ThemePalette } from '../utils/theme/engine'
import { omit } from '#ui/utils'
import colors from 'tailwindcss/colors'
-function readLocalStorage(key: string, fallback: T): T {
- if (!import.meta.client) return fallback
- try {
- const raw = window.localStorage.getItem(key)
- return raw ? JSON.parse(raw) : fallback
- } catch {
- return fallback
- }
-}
-
-// AI `applyTheme` output is untrusted and ends up concatenated into