Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 101 additions & 20 deletions apps/docs/src/components/docs/DocsExample.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
// Framework
import { createOverflow, isUndefined, Tabs } from '@vuetify/v0'
import { createOverflow, isUndefined, Popover, Tabs, Theme, useTheme } from '@vuetify/v0'

// Components
import DocsSkeleton from './DocsSkeleton.vue'
Expand All @@ -9,6 +9,7 @@
import { getMultiFileBinUrl } from '@/composables/bin'
import { useExamples } from '@/composables/useExamples'
import { usePlayground } from '@/composables/usePlayground'
import { useThemeToggle } from '@/composables/useThemeToggle'

// Utilities
import { toKebab } from '@/utilities/strings'
Expand Down Expand Up @@ -167,6 +168,36 @@
const url = getMultiFileBinUrl(files, props.title)
window.open(url, '_blank')
}

// Per-example theme override
const theme = useTheme()
const toggle = useThemeToggle()
const committed = shallowRef<string>()
const preview = shallowRef<string>()
const themeOverride = toRef(() => preview.value ?? committed.value)
const themePickerOpen = ref(false)
const hasOverride = toRef(() => !!committed.value)
const themeNames = toRef(() => theme.keys())

function onTheme (name: string) {
committed.value = committed.value === name ? undefined : name
preview.value = undefined
themePickerOpen.value = false
}

function onResetTheme () {
committed.value = undefined
preview.value = undefined
themePickerOpen.value = false
}

function onPreview (name: string) {
preview.value = name
}

function onPreviewReset () {
preview.value = undefined
}
</script>

<template>
Expand All @@ -183,28 +214,78 @@
</DocsExampleDescription>

<!-- Preview -->
<div class="p-6 bg-surface" :class="hasDescription && !descriptionExpanded && 'pt-8'">
<Theme class="p-6 bg-surface" :class="hasDescription && !descriptionExpanded && 'pt-8'" :theme="themeOverride">
<component :is="auto?.component" v-if="auto?.component" />
<slot v-else />
</div>
</Theme>

<!-- Toolbar -->
<div class="border-t border-divider bg-surface-tint">
<div class="flex items-center">
<!-- Theme picker -->
<Popover.Root v-model="themePickerOpen">
<Popover.Activator
aria-label="Override example theme"
class="px-4 py-3 inline-flex items-center gap-1.5 bg-transparent border-none text-sm cursor-pointer transition-colors hover:bg-surface"
:class="hasOverride ? 'text-primary' : 'text-on-surface-variant'"
title="Override theme"
>
<AppIcon :icon="toggle.icon.value" :size="16" />
<span v-if="hasOverride" class="text-xs font-medium capitalize">{{ committed }}</span>
</Popover.Activator>

<Popover.Content
class="p-2 rounded-lg bg-surface border border-divider shadow-xl min-w-44 !mt-1"
position-area="bottom span-right"
position-try="bottom span-right, bottom span-left, top span-right, top span-left"
@mouseleave="onPreviewReset"
>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs font-medium transition-colors"
:class="!hasOverride ? 'bg-primary/15 text-primary' : 'hover:bg-surface-tint text-on-surface'"
type="button"
@click="onResetTheme"
@mouseenter="onPreviewReset"
>
<AppIcon icon="close" :size="12" />
<span>Page default</span>
</button>

<!-- Code toggle button -->
<div v-if="!peek && (resolvedCode || displayFiles?.length)" class="border-t border-divider bg-surface-tint">
<button
:aria-controls="`${uid}-code`"
:aria-expanded="showCode"
class="group w-full px-4 py-3 bg-transparent border-none font-inherit text-sm cursor-pointer flex items-center gap-2 text-on-surface transition-colors hover:bg-surface"
type="button"
@click="toggleCode"
>
<AppLoaderIcon v-if="isLoading" variant="orbit" />
<AppIcon v-else-if="showCode && hasHighlightedCode" icon="chevron-up" :size="16" />
<AppIcon v-else class="transition-colors group-hover:text-primary" icon="code" :size="16" />
<span v-if="hasMultipleFiles" class="ml-auto opacity-60 font-mono text-[0.8125rem]">
{{ displayFiles!.length }} file(s)
</span>
<span v-else-if="fileName" class="ml-auto opacity-60 font-mono text-[0.8125rem]">{{ fileName }}</span>
</button>
<div class="my-1 border-t border-divider" />

<button
v-for="name in themeNames"
:key="name"
class="w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs font-medium transition-colors"
:class="committed === name ? 'bg-primary/15 text-primary' : 'hover:bg-surface-tint text-on-surface'"
type="button"
@click="onTheme(name)"
@mouseenter="onPreview(name)"
>
<AppThemePreview :theme="name" />
<span class="capitalize">{{ name }}</span>
</button>
</Popover.Content>
</Popover.Root>

<!-- Code toggle -->
<button
v-if="!peek && (resolvedCode || displayFiles?.length)"
:aria-controls="`${uid}-code`"
:aria-expanded="showCode"
class="group flex-1 px-4 py-3 bg-transparent border-none font-inherit text-sm cursor-pointer flex items-center gap-2 text-on-surface transition-colors hover:bg-surface"
type="button"
@click="toggleCode"
>
<AppLoaderIcon v-if="isLoading" variant="orbit" />
<AppIcon v-else-if="showCode && hasHighlightedCode" icon="chevron-up" :size="16" />
<AppIcon v-else class="transition-colors group-hover:text-primary" icon="code" :size="16" />
<span v-if="hasMultipleFiles" class="ml-auto opacity-60 font-mono text-[0.8125rem]">
{{ displayFiles!.length }} file(s)
</span>
<span v-else-if="fileName" class="ml-auto opacity-60 font-mono text-[0.8125rem]">{{ fileName }}</span>
</button>
</div>
</div>

<!-- Single file code display -->
Expand Down
213 changes: 210 additions & 3 deletions apps/docs/src/components/playground/app/PlaygroundAppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,69 @@
import { IN_BROWSER } from '#v0/constants/globals'

// Framework
import { useBreakpoints, useHotkey, useStorage, useTheme } from '@vuetify/v0'
import { Popover, useBreakpoints, useHotkey, useStorage, useTheme } from '@vuetify/v0'

// Components
import { usePlayground } from './PlaygroundApp.vue'

// Composables
import { usePlaygroundTheme } from '@/composables/usePlaygroundTheme'
import { useThemeToggle, type ThemePreference } from '@/composables/useThemeToggle'

// Utilities
import { computed } from 'vue'
import { computed, ref, shallowRef } from 'vue'
import { RouterLink, useRouter } from 'vue-router'

// Types
import type { ThemeId } from '@/themes'

const router = useRouter()
const theme = useTheme()
const toggle = useThemeToggle()
const playground = usePlayground()
const breakpoints = useBreakpoints()
const storage = useStorage()
const left = storage.get('playground-left-open', true)
const side = storage.get('playground-preview-right', false)
const {
names,
committed,
hasOverride,
onTheme,
onReset,
onPreview,
onPreviewReset,
} = usePlaygroundTheme()

const pickerOpen = ref(false)
const target = shallowRef<'editor' | 'preview'>('editor')

interface ThemeOption {
id: ThemePreference
label: string
icon: string
theme?: ThemeId
}

const modeOptions: ThemeOption[] = [
{ id: 'system', label: 'System', icon: 'theme-system' },
{ id: 'light', label: 'Light', icon: 'theme-light', theme: 'light' },
{ id: 'dark', label: 'Dark', icon: 'theme-dark', theme: 'dark' },
]

const accessibilityOptions: ThemeOption[] = [
{ id: 'high-contrast', label: 'High Contrast', icon: 'theme-high-contrast', theme: 'high-contrast' },
{ id: 'protanopia', label: 'Protanopia', icon: 'theme-protanopia', theme: 'protanopia' },
{ id: 'deuteranopia', label: 'Deuteranopia', icon: 'theme-deuteranopia', theme: 'deuteranopia' },
{ id: 'tritanopia', label: 'Tritanopia', icon: 'theme-tritanopia', theme: 'tritanopia' },
]

const vuetifyOptions: ThemeOption[] = [
{ id: 'blackguard', label: 'Blackguard', icon: 'theme-blackguard', theme: 'blackguard' },
{ id: 'polaris', label: 'Polaris', icon: 'theme-polaris', theme: 'polaris' },
{ id: 'nebula', label: 'Nebula', icon: 'theme-nebula', theme: 'nebula' },
{ id: 'odyssey', label: 'Odyssey', icon: 'theme-odyssey', theme: 'odyssey' },
]

useHotkey('ctrl+b', () => playground.toggle('workspace-left'), { inputs: true })

Expand Down Expand Up @@ -116,7 +163,167 @@
<AppIcon :icon="playground.selected('workspace-right') ? 'editor' : 'eye'" />
</button>

<AppThemeToggle />
<!-- Theme picker -->
<Popover.Root v-model="pickerOpen">
<Popover.Activator
aria-label="Select theme"
class="bg-surface-tint text-on-surface-tint pa-1 inline-flex items-center gap-1.5 rounded hover:bg-surface-variant transition-all cursor-pointer"
:title="toggle.title.value"
>
<AppIcon :icon="toggle.icon.value" />
<span v-if="hasOverride" class="text-[10px] font-semibold text-primary">PREVIEW</span>
</Popover.Activator>

<Popover.Content
class="p-3 rounded-lg bg-surface border border-divider shadow-xl min-w-56 !mt-1"
position-area="bottom span-left"
position-try="bottom span-left, bottom span-right, top span-left, top span-right"
>
<!-- Header -->
<div class="flex items-center justify-between mb-3 ps-1">
<span class="text-xs font-semibold text-on-surface">Theme</span>
<AppCloseButton size="sm" @click="pickerOpen = false" />
</div>

<!-- Target toggle -->
<div class="flex gap-1 mb-3 p-0.5 rounded bg-surface-tint">
<button
:aria-pressed="target === 'editor'"
class="flex-1 px-3 py-1 rounded text-xs font-medium transition-colors"
:class="target === 'editor'
? 'bg-surface text-on-surface shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'"
type="button"
@click="target = 'editor'"
>
Editor
</button>
<button
:aria-pressed="target === 'preview'"
class="flex-1 px-3 py-1 rounded text-xs font-medium transition-colors"
:class="target === 'preview'
? 'bg-surface text-on-surface shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'"
type="button"
@click="target = 'preview'"
>
Preview
</button>
</div>

<!-- Editor mode -->
<template v-if="target === 'editor'">
<!-- Mode -->
<div class="mb-3">
<div class="text-xs font-medium text-on-surface-variant mb-2 px-1">Mode</div>
<div class="flex gap-1">
<button
v-for="option in modeOptions"
:key="option.id"
:aria-pressed="toggle.preference.value === option.id"
:class="[
'flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 rounded text-xs font-medium transition-colors',
toggle.preference.value === option.id
? 'bg-primary/15 text-primary'
: 'hover:bg-surface-tint text-on-surface',
]"
type="button"
@click="toggle.setPreference(option.id)"
>
<AppIcon :icon="option.icon" size="14" />
<span>{{ option.label }}</span>
</button>
</div>
</div>

<!-- Accessibility -->
<div class="mb-3">
<div class="text-xs font-medium text-on-surface-variant mb-2 px-1">Accessibility</div>
<div class="grid grid-cols-2 gap-1">
<button
v-for="option in accessibilityOptions"
:key="option.id"
:aria-pressed="toggle.preference.value === option.id"
:class="[
'flex flex-col items-start gap-1.5 px-2 py-1.5 rounded text-xs font-medium transition-colors',
toggle.preference.value === option.id
? 'bg-primary/15 text-primary'
: 'hover:bg-surface-tint text-on-surface',
]"
type="button"
@click="toggle.setPreference(option.id)"
>
<div class="flex items-center gap-1.5">
<AppIcon :icon="option.icon" size="14" />
<span>{{ option.label }}</span>
</div>
<AppThemePreview v-if="option.theme" :theme="option.theme" />
</button>
</div>
</div>

<!-- Vuetify Themes -->
<div>
<div class="text-xs font-medium text-on-surface-variant mb-2 px-1">Vuetify</div>
<div class="grid grid-cols-2 gap-1">
<button
v-for="option in vuetifyOptions"
:key="option.id"
:aria-pressed="toggle.preference.value === option.id"
:class="[
'flex flex-col items-start gap-1.5 px-2 py-1.5 rounded text-xs font-medium transition-colors',
toggle.preference.value === option.id
? 'bg-primary/15 text-primary'
: 'hover:bg-surface-tint text-on-surface',
]"
type="button"
@click="toggle.setPreference(option.id)"
>
<div class="flex items-center gap-1.5">
<AppIcon :icon="option.icon" size="14" />
<span>{{ option.label }}</span>
</div>
<AppThemePreview v-if="option.theme" :theme="option.theme" />
</button>
</div>
</div>
</template>

<!-- Preview mode -->
<template v-else>
<button
class="w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs font-medium transition-colors mb-2"
:class="!hasOverride ? 'bg-primary/15 text-primary' : 'hover:bg-surface-tint text-on-surface'"
type="button"
@click="onReset"
@mouseenter="onPreviewReset"
>
<AppIcon icon="close" :size="12" />
<span>Same as editor</span>
</button>

<div
class="grid grid-cols-2 gap-1"
@mouseleave="onPreviewReset"
>
<button
v-for="name in names"
:key="name"
class="flex flex-col items-start gap-1.5 px-2 py-1.5 rounded text-xs font-medium transition-colors"
:class="committed === name ? 'bg-primary/15 text-primary' : 'hover:bg-surface-tint text-on-surface'"
type="button"
@click="onTheme(name)"
@mouseenter="onPreview(name)"
>
<div class="flex items-center gap-1.5">
<AppThemePreview :theme="name" />
<span class="capitalize">{{ name }}</span>
</div>
</button>
</div>
</template>
</Popover.Content>
</Popover.Root>
</div>
</header>
</template>
Loading