Skip to content

Commit 275cd9f

Browse files
committed
feat(palettes): add static palettes and generator adapters
- Core types: PaletteDefinition, PaletteGenerator - Static palettes: md1, md2, material (MD3), tailwind, radix, ant - Generator adapters: material (MCU), ant, leonardo - Docs: palettes guide page with interactive explorer - Playground: palettes moved from inline data to package imports
1 parent 1b19054 commit 275cd9f

21 files changed

Lines changed: 2342 additions & 584 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<script setup lang="ts">
2+
// Framework
3+
import { ant } from '@vuetify/v0/palettes/ant'
4+
import { material } from '@vuetify/v0/palettes/material'
5+
import { md1 } from '@vuetify/v0/palettes/md1'
6+
import { md2 } from '@vuetify/v0/palettes/md2'
7+
import { radix } from '@vuetify/v0/palettes/radix'
8+
import { tailwind } from '@vuetify/v0/palettes/tailwind'
9+
10+
// Composables
11+
import { useClipboard } from '@/composables/useClipboard'
12+
13+
// Utilities
14+
import { computed, ref, shallowRef, watch } from 'vue'
15+
16+
interface PaletteEntry {
17+
label: string
18+
data: Record<string, Record<string, string>>
19+
namespace: string
20+
}
21+
22+
const PALETTES: Record<string, PaletteEntry> = {
23+
tailwind: { label: 'Tailwind', data: tailwind as Record<string, Record<string, string>>, namespace: 'tw' },
24+
md1: { label: 'MD1', data: md1 as Record<string, Record<string, string>>, namespace: 'md1' },
25+
md2: { label: 'MD2', data: md2 as Record<string, Record<string, string>>, namespace: 'md2' },
26+
material: { label: 'Material', data: material as Record<string, Record<string, string>>, namespace: 'md' },
27+
radix: { label: 'Radix', data: radix as Record<string, Record<string, string>>, namespace: 'radix' },
28+
ant: { label: 'Ant Design', data: ant as Record<string, Record<string, string>>, namespace: 'ant' },
29+
}
30+
31+
const selected = shallowRef('tailwind')
32+
const clicks = ref(new Map<string, string>())
33+
const { copied, copy } = useClipboard()
34+
35+
const palette = computed(() => PALETTES[selected.value]!)
36+
37+
// Normalize: extract all unique shade keys across all hues in the selected palette
38+
// This ensures every row has the same number of columns
39+
const columns = computed(() => {
40+
const keys = new Set<string>()
41+
for (const collection of Object.values(palette.value.data)) {
42+
for (const key of Object.keys(collection)) {
43+
keys.add(key)
44+
}
45+
}
46+
// Sort: numeric first (by number), then accent (A-prefixed)
47+
const numeric: string[] = []
48+
const accent: string[] = []
49+
for (const key of keys) {
50+
if (key.startsWith('A')) {
51+
accent.push(key)
52+
} else {
53+
numeric.push(key)
54+
}
55+
}
56+
numeric.sort((a, b) => Number(a) - Number(b))
57+
accent.sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)))
58+
return [...numeric, ...accent]
59+
})
60+
61+
const rows = computed(() => Object.entries(palette.value.data))
62+
63+
// Transpose when there are more shades than hues (e.g., Material: 6 groups × 26 tones)
64+
// This swaps axes so groups become columns and tones become rows
65+
const transposed = computed(() => columns.value.length > rows.value.length * 2)
66+
67+
watch(selected, () => {
68+
clicks.value.clear()
69+
})
70+
71+
function onSwatch (hue: string, shade: string, hex: string) {
72+
const path = `${hue}.${shade}`
73+
copy(path)
74+
clicks.value.set(path, hex)
75+
}
76+
77+
function onExport () {
78+
const p = palette.value
79+
const ns = p.namespace
80+
const name = selected.value
81+
let code = `import { ${name} } from '@vuetify/v0/palettes/${name}'\n\n`
82+
code += `app.use(\n createThemePlugin({\n palette: { ${ns}: ${name} },\n`
83+
code += ` themes: {\n light: {\n colors: {\n`
84+
85+
if (clicks.value.size > 0) {
86+
for (const [path] of clicks.value) {
87+
code += ` // '{palette.${ns}.${path}}',\n`
88+
}
89+
} else {
90+
code += ` // primary: '{palette.${ns}.blue.500}',\n`
91+
code += ` // secondary: '{palette.${ns}.slate.600}',\n`
92+
}
93+
94+
code += ` }\n }\n }\n })\n)`
95+
copy(code)
96+
}
97+
</script>
98+
99+
<template>
100+
<div class="flex flex-col gap-4">
101+
<!-- Header -->
102+
<div class="flex items-center justify-between">
103+
<div class="text-xs uppercase tracking-wider font-semibold op-60">
104+
Browse Palettes
105+
</div>
106+
107+
<select
108+
v-model="selected"
109+
class="h-[30px] px-2 text-xs font-medium bg-surface-tint border border-divider rounded"
110+
>
111+
<option
112+
v-for="(entry, key) in PALETTES"
113+
:key
114+
:value="key"
115+
>
116+
{{ entry.label }}
117+
</option>
118+
</select>
119+
</div>
120+
121+
<!-- Swatch grid — fixed height container, uniform cell sizes -->
122+
<div class="h-[420px] overflow-auto border border-divider rounded-lg">
123+
<!-- Transposed: groups as columns, tones as rows (for Material) -->
124+
<div
125+
v-if="transposed"
126+
class="grid gap-px"
127+
:style="{
128+
gridTemplateColumns: `80px repeat(${rows.length}, minmax(40px, 1fr))`,
129+
}"
130+
>
131+
<!-- Column headers (group names) -->
132+
<div class="sticky top-0 z-1 bg-surface px-2 py-1" />
133+
<div
134+
v-for="([hue]) in rows"
135+
:key="hue"
136+
class="sticky top-0 z-1 bg-surface text-center text-[9px] op-40 py-1 uppercase tracking-wider"
137+
>
138+
{{ hue }}
139+
</div>
140+
141+
<!-- Rows (tone values) -->
142+
<template v-for="tone in columns" :key="tone">
143+
<div class="sticky left-0 z-1 bg-surface flex items-center px-2 text-[10px] font-medium op-50 font-mono">
144+
{{ tone }}
145+
</div>
146+
147+
<button
148+
v-for="([hue, collection]) in rows"
149+
:key="hue"
150+
class="h-7 cursor-pointer border-0 transition-opacity"
151+
:class="collection[tone] ? 'hover:opacity-80' : 'opacity-0 pointer-events-none'"
152+
:style="collection[tone] ? { backgroundColor: collection[tone] } : {}"
153+
:title="collection[tone] ? `${hue}.${tone} · ${collection[tone]}` : ''"
154+
@click="collection[tone] && onSwatch(hue, tone, collection[tone]!)"
155+
/>
156+
</template>
157+
</div>
158+
159+
<!-- Normal: hues as rows, shades as columns -->
160+
<div
161+
v-else
162+
class="grid gap-px min-w-fit"
163+
:style="{
164+
gridTemplateColumns: `80px repeat(${columns.length}, minmax(28px, 1fr))`,
165+
}"
166+
>
167+
<!-- Column headers -->
168+
<div class="sticky top-0 z-1 bg-surface px-2 py-1" />
169+
<div
170+
v-for="col in columns"
171+
:key="col"
172+
class="sticky top-0 z-1 bg-surface text-center text-[9px] op-40 py-1 font-mono"
173+
>
174+
{{ col }}
175+
</div>
176+
177+
<!-- Rows -->
178+
<template v-for="([hue, collection]) in rows" :key="hue">
179+
<!-- Hue label -->
180+
<div class="sticky left-0 z-1 bg-surface flex items-center px-2 text-[10px] uppercase tracking-wider font-medium op-50 truncate">
181+
{{ hue }}
182+
</div>
183+
184+
<!-- Swatch cells -->
185+
<button
186+
v-for="col in columns"
187+
:key="col"
188+
class="h-7 cursor-pointer border-0 transition-opacity"
189+
:class="collection[col] ? 'hover:opacity-80' : 'opacity-0 pointer-events-none'"
190+
:style="collection[col] ? { backgroundColor: collection[col] } : {}"
191+
:title="collection[col] ? `${hue}.${col} · ${collection[col]}` : ''"
192+
@click="collection[col] && onSwatch(hue, col, collection[col]!)"
193+
/>
194+
</template>
195+
</div>
196+
</div>
197+
198+
<!-- Export button with inline copied indicator -->
199+
<div class="flex items-center gap-3">
200+
<button
201+
class="px-3 py-1.5 text-xs font-medium border border-divider rounded bg-transparent text-on-surface cursor-pointer hover:bg-surface-tint"
202+
@click="onExport"
203+
>
204+
Copy Config
205+
</button>
206+
207+
<Transition name="fade">
208+
<span v-if="copied" class="text-xs op-60">
209+
Copied to clipboard
210+
</span>
211+
</Transition>
212+
</div>
213+
</div>
214+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script setup lang="ts">
2+
// Components
3+
import DocsPaletteBrowse from './DocsPaletteBrowse.vue'
4+
</script>
5+
6+
<template>
7+
<div class="my-8">
8+
<DocsPaletteBrowse />
9+
</div>
10+
</template>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<script setup lang="ts">
2+
// Framework
3+
import { debounce, IN_BROWSER } from '@vuetify/v0'
4+
import { ant as generateAnt } from '@vuetify/v0/palettes/ant/generate'
5+
import { leonardo as generateLeonardo } from '@vuetify/v0/palettes/leonardo/generate'
6+
import { material as generateMaterial } from '@vuetify/v0/palettes/material/generate'
7+
8+
// Components
9+
import DocsPalettePreview from './DocsPalettePreview.vue'
10+
11+
// Composables
12+
import { useClipboard } from '@/composables/useClipboard'
13+
14+
// Utilities
15+
import { onMounted, shallowRef, watch } from 'vue'
16+
17+
// Types
18+
import type { PaletteDefinition } from '@vuetify/v0/palettes'
19+
import type { MaterialGenerateOptions } from '@vuetify/v0/palettes/material/generate'
20+
21+
const seed = shallowRef('#6750A4')
22+
const adapter = shallowRef<'material' | 'ant' | 'leonardo'>('material')
23+
const variant = shallowRef<MaterialGenerateOptions['variant']>('tonalSpot')
24+
const result = shallowRef<PaletteDefinition | null>(null)
25+
const { copied, copy } = useClipboard()
26+
27+
const ADAPTERS = [
28+
{ key: 'material' as const, label: 'Material' },
29+
{ key: 'ant' as const, label: 'Ant Design' },
30+
{ key: 'leonardo' as const, label: 'Leonardo' },
31+
]
32+
33+
const VARIANTS: MaterialGenerateOptions['variant'][] = [
34+
'tonalSpot',
35+
'vibrant',
36+
'expressive',
37+
'fidelity',
38+
'monochrome',
39+
'neutral',
40+
]
41+
42+
function run () {
43+
if (!IN_BROWSER) return
44+
45+
try {
46+
if (adapter.value === 'material') {
47+
result.value = generateMaterial(seed.value, { variant: variant.value })
48+
} else if (adapter.value === 'ant') {
49+
result.value = generateAnt(seed.value)
50+
} else {
51+
result.value = generateLeonardo(seed.value)
52+
}
53+
} catch {
54+
// Keep last valid result on error
55+
}
56+
}
57+
58+
const generate = debounce(run, 300)
59+
60+
watch([seed, adapter, variant], () => generate())
61+
onMounted(() => run())
62+
63+
function onExport () {
64+
const name = adapter.value
65+
const importPath = `@vuetify/v0/palettes/${name}/generate`
66+
let code = `import { ${name} } from '${importPath}'\n\n`
67+
code += `const { palette, themes } = ${name}('${seed.value}'`
68+
69+
if (name === 'material') {
70+
code += `, { variant: '${variant.value}' }`
71+
}
72+
73+
code += `)\n\napp.use(createThemePlugin({ palette, themes }))`
74+
copy(code)
75+
}
76+
</script>
77+
78+
<template>
79+
<div class="flex flex-col gap-4">
80+
<!-- Header -->
81+
<div class="text-xs uppercase tracking-wider font-semibold op-60">
82+
Generate from Seed
83+
</div>
84+
85+
<!-- Controls -->
86+
<div class="flex flex-col gap-3">
87+
<!-- Seed color -->
88+
<div class="flex items-center gap-2">
89+
<input
90+
v-model="seed"
91+
class="w-[30px] h-[30px] rounded cursor-pointer border border-divider p-0"
92+
type="color"
93+
>
94+
95+
<input
96+
v-model="seed"
97+
class="font-mono w-24 h-[30px] px-2 text-xs bg-surface-tint border border-divider rounded"
98+
type="text"
99+
>
100+
</div>
101+
102+
<!-- Adapter pills -->
103+
<div class="flex flex-wrap gap-1.5">
104+
<button
105+
v-for="a in ADAPTERS"
106+
:key="a.key"
107+
class="px-3 py-1 text-xs font-medium rounded-full cursor-pointer border border-solid"
108+
:class="adapter === a.key
109+
? 'bg-primary text-on-primary border-primary'
110+
: 'bg-surface-tint border-divider text-on-surface-variant'"
111+
@click="adapter = a.key"
112+
>
113+
{{ a.label }}
114+
</button>
115+
</div>
116+
117+
<!-- Variant pills (material only) -->
118+
<div v-if="adapter === 'material'" class="flex flex-wrap gap-1.5">
119+
<button
120+
v-for="v in VARIANTS"
121+
:key="v"
122+
class="px-2.5 py-1 text-xs font-medium rounded-full cursor-pointer border border-solid"
123+
:class="variant === v
124+
? 'bg-primary text-on-primary border-primary'
125+
: 'bg-surface-tint border-divider text-on-surface-variant'"
126+
@click="variant = v"
127+
>
128+
{{ v }}
129+
</button>
130+
</div>
131+
</div>
132+
133+
<!-- Preview -->
134+
<DocsPalettePreview v-if="result" :definition="result" />
135+
136+
<!-- Export button -->
137+
<div>
138+
<button
139+
class="px-3 py-1.5 text-xs font-medium border border-divider rounded bg-transparent text-on-surface cursor-pointer hover:bg-surface-tint"
140+
@click="onExport"
141+
>
142+
{{ copied ? 'Copied!' : 'Copy Config' }}
143+
</button>
144+
</div>
145+
</div>
146+
</template>

0 commit comments

Comments
 (0)