docs(theme): computed view icons, no remount on a pack swap

This commit is contained in:
Benjamin Canac
2026-09-08 21:17:12 +02:00
parent 7e7cd2fd1f
commit 091eddc979
9 changed files with 54 additions and 65 deletions
@@ -11,7 +11,7 @@ import shiki from '@comark/vue/plugins/shiki'
const appConfig = useAppConfig()
const studioIcons = useStudioIcons()
const introLinks = [{
const introLinks = computed(() => [{
label: 'Documentation',
icon: studioIcons.bookOpen,
color: 'neutral' as const,
@@ -23,7 +23,7 @@ const introLinks = [{
color: 'neutral' as const,
variant: 'ghost' as const,
size: 'md' as const
}]
}])
/**
* Invented releases in the shape the template fetches from GitHub: a tag, a
@@ -51,21 +51,21 @@ function newChat() {
input.value = ''
}
const menuItems: NavigationMenuItem[] = [
const menuItems = computed<NavigationMenuItem[]>(() => [
{ label: 'New chat', icon: appConfig.ui.icons.plus, kbds: ['meta', 'o'], onSelect: () => newChat() },
{ label: 'Search', icon: appConfig.ui.icons.search, kbds: ['meta', 'k'] }
]
])
// The per-chat menu, on the sidebar rows and behind the navbar title.
const chatActions: DropdownMenuItem[] = [
const chatActions = computed<DropdownMenuItem[]>(() => [
{ label: 'Rename', icon: studioIcons.pencil },
{ label: 'Delete', icon: studioIcons.trash, color: 'error' }
]
])
const userItems: DropdownMenuItem[][] = [
const userItems = computed<DropdownMenuItem[][]>(() => [
[{ label: 'Benjamin Canac', avatar: { src: 'https://github.com/benjamincanac.png', alt: 'Benjamin Canac' }, type: 'label' }],
[{ label: 'Settings', icon: studioIcons.settings }, { label: 'Log out', icon: studioIcons.logout }]
]
])
const historyItems: NavigationMenuItem[] = [
{ label: 'Today', type: 'label' },
@@ -88,7 +88,7 @@ const user = {
avatar: { src: 'https://github.com/benjamincanac.png', alt: 'Benjamin Canac' }
}
const userItems: DropdownMenuItem[][] = [[{
const userItems = computed<DropdownMenuItem[][]>(() => [[{
type: 'label',
label: user.name,
avatar: user.avatar
@@ -98,12 +98,12 @@ const userItems: DropdownMenuItem[][] = [[{
{ label: 'Settings', icon: studioIcons.settings }
], [
{ label: 'Log out', icon: studioIcons.logout }
]]
]])
const newItems: DropdownMenuItem[][] = [[
const newItems = computed<DropdownMenuItem[][]>(() => [[
{ label: 'New mail', icon: studioIcons.send },
{ label: 'New customer', icon: studioIcons.userPlus }
]]
]])
// lowercase like the template, capitalized back through the select's `ui`
const periodItems = ['daily', 'weekly', 'monthly']
@@ -153,12 +153,12 @@ function selectDateRange(range: DateRangePreset) {
}
const period = ref('daily')
const stats = [
const stats = computed(() => [
{ title: 'Customers', icon: studioIcons.users, value: '892', variation: 14 },
{ title: 'Conversions', icon: studioIcons.chart, value: '1,436', variation: 8 },
{ title: 'Revenue', icon: studioIcons.dollar, value: '$312,540', variation: 23 },
{ title: 'Orders', icon: studioIcons.cart, value: '254', variation: -5 }
]
])
const revenue = [6200, 7400, 6800, 9100, 8600, 10400, 9800, 11900, 11200, 13000, 12400, 14100, 13600, 15200]
const revenueMin = Math.min(...revenue)
@@ -299,12 +299,12 @@ watch(filteredMails, () => {
}
})
const mailDropdownItems: DropdownMenuItem[][] = [[
const mailDropdownItems = computed<DropdownMenuItem[][]>(() => [[
{ label: 'Mark as unread', icon: appConfig.ui.icons.success },
{ label: 'Mark as important', icon: appConfig.ui.icons.warning }
], [
{ label: 'Star thread', icon: appConfig.ui.icons.star }
]]
]])
const reply = ref('')
@@ -350,14 +350,14 @@ const customerStatusItems = [
{ label: 'Bounced', value: 'bounced' }
]
const customerRowItems: DropdownMenuItem[] = [
const customerRowItems = computed<DropdownMenuItem[]>(() => [
{ type: 'label', label: 'Actions' },
{ label: 'Copy customer ID', icon: appConfig.ui.icons.copy },
{ type: 'separator' },
{ label: 'View customer details', icon: studioIcons.list },
{ type: 'separator' },
{ label: 'Delete customer', icon: studioIcons.trash, color: 'error' }
]
])
const customerColumns: TableColumn<Customer>[] = [{
id: 'select',
@@ -417,7 +417,7 @@ const customerColumns: TableColumn<Customer>[] = [{
cell: () => {
return h('div', { class: 'text-right' }, h(
UDropdownMenu,
{ content: { align: 'end' }, items: customerRowItems },
{ content: { align: 'end' }, items: customerRowItems.value },
() => h(UButton, { icon: appConfig.ui.icons.ellipsis, color: 'neutral', variant: 'ghost', class: 'ml-auto' })
))
}
@@ -10,7 +10,7 @@ const studioIcons = useStudioIcons()
// `title`, and `active` forces the state without a route, so the tree is a
// static array. Paths are empty so nothing navigates out of the studio: the
// type wants the key, and both the mapper and ULink drop a falsy one.
const navLinks: ContentNavigationLink[] = [{
const navLinks = computed<ContentNavigationLink[]>(() => [{
path: '',
title: 'Getting Started',
children: [
@@ -34,7 +34,7 @@ const navLinks: ContentNavigationLink[] = [{
{ path: '', title: 'MCP Server', icon: studioIcons.cpu },
{ path: '', title: 'LLMs Integration', icon: appConfig.ui.icons.file }
]
}]
}])
// The template's prev/next pair, inert for the same reason.
const surround: ContentSurroundLink[] = [{
@@ -49,12 +49,12 @@ const surround: ContentSurroundLink[] = [{
}]
// PageHeaderLinks dropdown, minus the real clipboard / external navigations.
const copyItems: DropdownMenuItem[] = [
const copyItems = computed<DropdownMenuItem[]>(() => [
{ label: 'Copy Markdown link', icon: studioIcons.link },
{ label: 'View as Markdown', icon: 'i-simple-icons:markdown' },
{ label: 'Open in ChatGPT', icon: 'i-simple-icons:openai' },
{ label: 'Open in Claude', icon: 'i-simple-icons:anthropic' }
]
])
// Right column: UContentToc is content-coupled and router-driven, so the
// same sticky layout is hand-rolled with faked active states.
@@ -70,11 +70,11 @@ const tocLinks = [
}
]
const communityLinks: PageLink[] = [
const communityLinks = computed<PageLink[]>(() => [
{ label: 'Edit this page', icon: appConfig.ui.icons.external },
{ label: 'Star on GitHub', icon: appConfig.ui.icons.star },
{ label: 'Nuxt UI docs', icon: studioIcons.bookOpen }
]
])
/**
* The template's `content/1.getting-started/3.usage.md`, verbatim. It renders
@@ -80,7 +80,7 @@ const headingItems = [1, 2, 3, 4].map(level => ({
icon: `i-lucide-heading-${level}`
}))
const blockItems = [{
const blockItems = computed(() => [{
kind: 'bulletList' as const,
label: 'Bullet List',
icon: studioIcons.list
@@ -96,7 +96,7 @@ const blockItems = [{
kind: 'codeBlock' as const,
label: 'Code Block',
icon: 'i-lucide-square-code'
}]
}])
const markItems = [{
kind: 'mark' as const,
@@ -125,7 +125,7 @@ const markItems = [{
tooltip: { text: 'Code' }
}]
const toolbarItems: EditorToolbarItem[][] = [[{
const toolbarItems = computed<EditorToolbarItem[][]>(() => [[{
kind: 'undo',
icon: studioIcons.undo,
tooltip: { text: 'Undo' }
@@ -138,9 +138,9 @@ const toolbarItems: EditorToolbarItem[][] = [[{
tooltip: { text: 'Headings' },
content: { align: 'start' },
items: headingItems
}, ...blockItems.map(({ label, ...item }) => ({ ...item, tooltip: { text: label } }))], markItems]
}, ...blockItems.value.map(({ label, ...item }) => ({ ...item, tooltip: { text: label } }))], markItems])
const bubbleToolbarItems: EditorToolbarItem[][] = [[{
const bubbleToolbarItems = computed<EditorToolbarItem[][]>(() => [[{
label: 'Turn into',
trailingIcon: appConfig.ui.icons.chevronDown,
activeColor: 'neutral',
@@ -155,28 +155,28 @@ const bubbleToolbarItems: EditorToolbarItem[][] = [[{
kind: 'paragraph',
label: 'Paragraph',
icon: studioIcons.text
}, ...headingItems, ...blockItems]
}, ...headingItems, ...blockItems.value]
}], markItems, [{
slot: 'link' as const,
icon: studioIcons.link,
tooltip: { text: 'Link' }
}]]
}]])
const suggestionItems: EditorSuggestionMenuItem[][] = [[{
const suggestionItems = computed<EditorSuggestionMenuItem[][]>(() => [[{
type: 'label',
label: 'Style'
}, {
kind: 'paragraph',
label: 'Paragraph',
icon: studioIcons.text
}, ...headingItems, ...blockItems], [{
}, ...headingItems, ...blockItems.value], [{
type: 'label',
label: 'Insert'
}, {
kind: 'horizontalRule',
label: 'Horizontal Rule',
icon: 'i-lucide-separator-horizontal'
}]]
}]])
const mentionItems: EditorMentionMenuItem[] = [
{ label: 'benjamincanac', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } },
@@ -215,7 +215,7 @@ function dragHandleItems(editor: Editor): DropdownMenuItem[][] {
}, {
label: 'Turn into',
icon: 'i-lucide-repeat-2',
children: [{ kind: 'paragraph', label: 'Paragraph', icon: studioIcons.text }, ...headingItems, ...blockItems]
children: [{ kind: 'paragraph', label: 'Paragraph', icon: studioIcons.text }, ...headingItems, ...blockItems.value]
}], [{
kind: 'duplicate',
pos,
@@ -61,7 +61,7 @@ const logos = [
'i-simple-icons-stripe'
]
const features = [{
const features = computed(() => [{
icon: studioIcons.zap,
title: 'Predictive Alerts',
description: 'ML models trained on your baselines detect anomalies 4 minutes before they hit your SLOs.'
@@ -85,7 +85,7 @@ const features = [{
icon: studioIcons.notebook,
title: 'Team Notebooks',
description: 'Collaborative investigation notebooks that turn incident debugging into reusable runbooks.'
}]
}])
const metrics = [
{ value: '99.99%', label: 'Uptime SLA', class: 'text-success' },
@@ -7,13 +7,13 @@ const studioIcons = useStudioIcons()
// Replicates the Nuxt UI Portfolio template home page: floating pill nav,
// avatar hero with polaroid marquee, about + work experience columns,
// blog list, testimonial carousel, FAQ tabs and footer.
const navItems: NavigationMenuItem[] = [
const navItems = computed<NavigationMenuItem[]>(() => [
{ label: 'Home', icon: studioIcons.home, active: true },
{ label: 'Projects', icon: appConfig.ui.icons.folder },
{ label: 'Blog', icon: appConfig.ui.icons.file },
{ label: 'Speaking', icon: studioIcons.mic },
{ label: 'About', icon: studioIcons.user }
]
])
const socialLinks = [
{ 'icon': 'i-simple-icons-discord', 'aria-label': 'Discord' },
@@ -23,7 +23,7 @@ const socialLinks = [
// The template scrolls travel photos in a marquee; gradient polaroids stand in
// because a tinted surface tracks the theme and a photo does not.
const heroImages = [
const heroImages = computed(() => [
{ icon: studioIcons.mountain, label: 'Alps, 2024' },
{ icon: studioIcons.coffee, label: 'Café sketching' },
{ icon: studioIcons.bike, label: 'Canal ride' },
@@ -33,7 +33,7 @@ const heroImages = [
{ icon: studioIcons.palette, label: 'Color studies' },
{ icon: studioIcons.ferrisWheel, label: 'Rotterdam fair' },
{ icon: studioIcons.waves, label: 'North Sea' }
]
])
const about = {
title: 'About Me',
@@ -41,7 +41,7 @@ const about = {
description: 'I\'m an English designer and developer based in Amsterdam. I sit right between design and engineering, which means both teams assume I\'m the other team\'s problem.'
}
const experience = [{
const experience = computed(() => [{
date: '2025 - Present',
position: 'Public Contributor for',
company: { name: 'Nuxt UI', logo: 'i-simple-icons-nuxtdotjs' }
@@ -57,7 +57,7 @@ const experience = [{
date: '1995 - Present',
position: 'Human person on',
company: { name: 'Earth', logo: studioIcons.globe }
}]
}])
const posts = [{
title: 'Twelve Identical Greys: A Field Guide to Choosing Your Neutral',
@@ -11,7 +11,7 @@ const navItems: NavigationMenuItem[] = [
{ label: 'Changelog' }
]
const heroLinks = [{
const heroLinks = computed(() => [{
label: 'Get started',
trailingIcon: appConfig.ui.icons.arrowRight,
size: 'xl' as const
@@ -21,9 +21,9 @@ const heroLinks = [{
size: 'xl' as const,
color: 'neutral' as const,
variant: 'subtle' as const
}]
}])
const sections = [{
const sections = computed(() => [{
title: 'Powered by Nuxt UI Components',
description: 'Access a complete component library with beautifully styled, accessible and customizable Vue components. Everything you need to build professional SaaS applications.',
reverse: false,
@@ -57,9 +57,9 @@ const sections = [{
description: 'Built-in i18n support for 50+ languages with RTL/LTR layouts and font optimization.',
icon: studioIcons.globe
}]
}]
}])
const features = [{
const features = computed(() => [{
title: 'Beautiful design system',
description: 'Semantic color aliases, comprehensive design tokens and Tailwind Variants for consistent styling.',
icon: studioIcons.palette
@@ -83,7 +83,7 @@ const features = [{
title: 'Infinitely customizable',
description: 'Override any style with the ui prop, customize globally with AppConfig, or use Tailwind classes directly.',
icon: studioIcons.settings
}]
}])
// The template's pricing page: three plans over a billing cycle toggle, no
// badge, and Standard scaled up. Prices and feature lists are content/2.pricing.yml.
@@ -159,14 +159,14 @@ const testimonials = [{
user: { name: 'Lisa Patel', description: 'CEO at AutoScale' }
}]
const ctaLinks = [{
const ctaLinks = computed(() => [{
label: 'Start building',
trailingIcon: appConfig.ui.icons.arrowRight
}, {
label: 'View on GitHub',
icon: 'i-simple-icons-github',
variant: 'subtle' as const
}]
}])
const footerColumns = [{
label: 'Resources',
+2 -13
View File
@@ -4,7 +4,6 @@ import { decodeThemeDoc } from '../utils/theme/link'
import { snapshotStoredTheme, writeStoredTheme } from '../utils/theme/storage'
const { track } = useAnalytics()
const { icon: iconSet } = useTheme()
const { open: chatOpen } = useChat()
@@ -15,7 +14,6 @@ function toggleChat() {
chatOpen.value = !chatOpen.value
}
// The chrome skins to the applied icon pack.
const studioIcons = useStudioIcons()
const { view, views, applyDoc, presets, activePreset } = useThemeStudio()
@@ -100,14 +98,12 @@ onMounted(() => {
if (linkApplied) track('Theme Link Applied', { preset: linkedPreset?.id })
})
// Color mode rides the app-wide `d` binding in app.vue, no page copy needed.
defineShortcuts({
meta_z: undo,
meta_shift_z: redo,
ctrl_y: redo
})
/** The export modal, opened from the header. */
const shareOpen = ref(false)
</script>
@@ -186,9 +182,8 @@ const shareOpen = ref(false)
<div class="flex flex-col bg-default rounded-xl overflow-hidden shadow ring ring-default h-[calc(100dvh-var(--ui-header-height)-0.5rem)] lg:h-[calc(100dvh-var(--ui-header-height)-var(--ui-header-height)-0.5rem)] mx-2">
<!-- [contain:paint]: Chromium won't clip nested composited layers by
an ancestor's overflow alone. Keyed on the icon pack: demo views
resolve icons at setup, so a pack swap remounts to re-resolve. -->
<div :key="iconSet" class="flex-1 min-h-0 overflow-hidden *:contain-[paint]">
an ancestor's overflow alone -->
<div class="flex-1 min-h-0 overflow-hidden *:contain-[paint]">
<Playground v-if="view === 'grid'" />
<LazyThemeStudioViewDashboard v-else-if="view === 'dashboard'" />
<LazyThemeStudioViewChat v-else-if="view === 'chat'" />
@@ -205,10 +200,6 @@ const shareOpen = ref(false)
(min-w-0), so the toolbar inside it scrolls instead of widening the bar -->
<UFooter class="hidden lg:block ring ring-default rounded-xl bg-default mx-2 mt-2" :ui="{ container: 'py-3! px-6!', left: 'mt-0 gap-0 lg:flex-none', center: 'flex-1 min-w-0 justify-start', right: 'mt-0 lg:flex-none' }">
<template #left>
<!-- one cluster: these four move the whole theme, the controls beside
them each change one setting. Framed like the mode tabs' own track
at the other end, a size down on the buttons so both land at the
height of the plain controls between them. -->
<div class="flex items-center gap-0.5 p-0.5 rounded-lg ring ring-default bg-elevated/50">
<UTooltip text="Undo" :kbds="['meta', 'Z']">
<UButton
@@ -234,7 +225,6 @@ const shareOpen = ref(false)
/>
</UTooltip>
<!-- undo/redo step through history, the two beside them rewrite it -->
<USeparator orientation="vertical" class="h-4 mx-0.5" />
<ThemeStudioResetButton size="sm" />
@@ -248,7 +238,6 @@ const shareOpen = ref(false)
<template #right>
<UTooltip text="Switch color mode" :kbds="['d']">
<!-- framed like the history cluster, the bar's two ends match -->
<ThemeStudioColorModeTabs
data-keep-panels
class="shrink-0"