|
24 | 24 | tailwind: { label: 'Tailwind', data: tailwind as Record<string, Record<string, string>>, namespace: 'tw' }, |
25 | 25 | md1: { label: 'MD1', data: md1 as Record<string, Record<string, string>>, namespace: 'md1' }, |
26 | 26 | md2: { label: 'MD2', data: md2 as Record<string, Record<string, string>>, namespace: 'md2' }, |
27 | | - material: { label: 'Material', data: material as Record<string, Record<string, string>>, namespace: 'md' }, |
| 27 | + material: { label: 'Material 3', data: material as Record<string, Record<string, string>>, namespace: 'md' }, |
28 | 28 | radix: { label: 'Radix', data: radix as Record<string, Record<string, string>>, namespace: 'radix' }, |
29 | 29 | ant: { label: 'Ant Design', data: ant as Record<string, Record<string, string>>, namespace: 'ant' }, |
30 | 30 | } |
31 | 31 |
|
| 32 | + const ROLES = ['primary', 'secondary', 'accent', 'error', 'info', 'success', 'warning'] |
| 33 | +
|
32 | 34 | const selected = shallowRef('tailwind') |
33 | 35 | const clicks = ref(new Map<string, string>()) |
34 | 36 | const { copied, copy } = useClipboard() |
|
65 | 67 | // This swaps axes so groups become columns and tones become rows |
66 | 68 | const transposed = computed(() => columns.value.length > rows.value.length * 2) |
67 | 69 |
|
| 70 | + // Clicked swatches mapped to theme roles, in click order — drives the summary chips and the export |
| 71 | + const selection = computed(() => |
| 72 | + [...clicks.value].map(([path, hex], index) => ({ |
| 73 | + path, |
| 74 | + hex, |
| 75 | + role: ROLES[index] ?? `color${index + 1}`, |
| 76 | + })), |
| 77 | + ) |
| 78 | +
|
| 79 | + // Fast path -> role lookup so each swatch can show whether it is picked |
| 80 | + const picked = computed(() => new Map(selection.value.map(entry => [entry.path, entry.role]))) |
| 81 | +
|
68 | 82 | watch(selected, () => { |
69 | 83 | clicks.value.clear() |
70 | 84 | }) |
|
75 | 89 | clicks.value.set(path, hex) |
76 | 90 | } |
77 | 91 |
|
| 92 | + function onRemove (path: string) { |
| 93 | + clicks.value.delete(path) |
| 94 | + } |
| 95 | +
|
| 96 | + function onClear () { |
| 97 | + clicks.value.clear() |
| 98 | + } |
| 99 | +
|
78 | 100 | function onExport () { |
79 | | - const p = palette.value |
80 | | - const ns = p.namespace |
| 101 | + const ns = palette.value.namespace |
81 | 102 | const name = selected.value |
82 | | - let code = `import { ${name} } from '@vuetify/v0/palettes/${name}'\n\n` |
83 | | - code += `app.use(\n createThemePlugin({\n palette: { ${ns}: ${name} },\n` |
84 | | - code += ` themes: {\n light: {\n colors: {\n` |
85 | 103 |
|
86 | | - if (clicks.value.size > 0) { |
87 | | - for (const [path] of clicks.value) { |
88 | | - code += ` // '{palette.${ns}.${path}}',\n` |
| 104 | + const lines: string[] = [] |
| 105 | +
|
| 106 | + if (selection.value.length > 0) { |
| 107 | + // Each clicked swatch is already mapped to a role |
| 108 | + for (const { path, role } of selection.value) { |
| 109 | + lines.push(` ${role}: '{palette.${ns}.${path}}',`) |
89 | 110 | } |
90 | 111 | } else { |
91 | | - code += ` // primary: '{palette.${ns}.blue.500}',\n` |
92 | | - code += ` // secondary: '{palette.${ns}.slate.600}',\n` |
| 112 | + // No clicks: seed primary/secondary from real hues in this palette so the config resolves |
| 113 | + const shade = columns.value[Math.floor(columns.value.length / 2)] |
| 114 | + for (const [index, [hue, collection]] of rows.value.slice(0, 2).entries()) { |
| 115 | + const key = shade && collection[shade] ? shade : Object.keys(collection)[0]! |
| 116 | + lines.push(` ${ROLES[index]}: '{palette.${ns}.${hue}.${key}}',`) |
| 117 | + } |
93 | 118 | } |
94 | 119 |
|
95 | | - code += ` }\n }\n }\n })\n)` |
| 120 | + let code = `import { ${name} } from '@vuetify/v0/palettes/${name}'\n\n` |
| 121 | + code += `app.use(\n createThemePlugin({\n palette: { ${ns}: ${name} },\n` |
| 122 | + code += ` themes: {\n light: {\n colors: {\n` |
| 123 | + code += `${lines.join('\n')}\n` |
| 124 | + code += ` },\n },\n },\n })\n)` |
96 | 125 | copy(code) |
97 | 126 | } |
98 | 127 | </script> |
|
141 | 170 | </Select.Root> |
142 | 171 | </div> |
143 | 172 |
|
| 173 | + <!-- Hint --> |
| 174 | + <div class="text-xs op-60"> |
| 175 | + Click a swatch to copy its token path and add it to the config. Then use <span class="font-medium">Copy Config</span> for a ready-to-paste theme. |
| 176 | + </div> |
| 177 | + |
144 | 178 | <!-- Swatch grid — fixed height container, uniform cell sizes --> |
145 | 179 | <div class="h-[420px] overflow-auto border border-divider rounded-lg"> |
146 | 180 | <!-- Transposed: groups as columns, tones as rows (for Material) --> |
|
172 | 206 | v-for="([hue, collection]) in rows" |
173 | 207 | :key="hue" |
174 | 208 | class="h-7 cursor-pointer border-0 transition-opacity" |
175 | | - :class="collection[tone] ? 'hover:opacity-80' : 'opacity-0 pointer-events-none'" |
| 209 | + :class="[ |
| 210 | + collection[tone] ? 'hover:opacity-80' : 'opacity-0 pointer-events-none', |
| 211 | + picked.has(`${hue}.${tone}`) && 'outline outline-2 -outline-offset-2 outline-on-surface', |
| 212 | + ]" |
176 | 213 | :style="collection[tone] ? { backgroundColor: collection[tone] } : {}" |
177 | | - :title="collection[tone] ? `${hue}.${tone} · ${collection[tone]}` : ''" |
| 214 | + :title="collection[tone] ? `${hue}.${tone} · ${collection[tone]}${picked.has(`${hue}.${tone}`) ? ` · ${picked.get(`${hue}.${tone}`)}` : ''}` : ''" |
178 | 215 | @click="collection[tone] && onSwatch(hue, tone, collection[tone]!)" |
179 | 216 | /> |
180 | 217 | </template> |
|
211 | 248 | v-for="col in columns" |
212 | 249 | :key="col" |
213 | 250 | class="h-7 cursor-pointer border-0 transition-opacity" |
214 | | - :class="collection[col] ? 'hover:opacity-80' : 'opacity-0 pointer-events-none'" |
| 251 | + :class="[ |
| 252 | + collection[col] ? 'hover:opacity-80' : 'opacity-0 pointer-events-none', |
| 253 | + picked.has(`${hue}.${col}`) && 'outline outline-2 -outline-offset-2 outline-on-surface', |
| 254 | + ]" |
215 | 255 | :style="collection[col] ? { backgroundColor: collection[col] } : {}" |
216 | | - :title="collection[col] ? `${hue}.${col} · ${collection[col]}` : ''" |
| 256 | + :title="collection[col] ? `${hue}.${col} · ${collection[col]}${picked.has(`${hue}.${col}`) ? ` · ${picked.get(`${hue}.${col}`)}` : ''}` : ''" |
217 | 257 | @click="collection[col] && onSwatch(hue, col, collection[col]!)" |
218 | 258 | /> |
219 | 259 | </template> |
220 | 260 | </div> |
221 | 261 | </div> |
222 | 262 |
|
| 263 | + <!-- Selection summary — which swatches are picked and the role each maps to --> |
| 264 | + <div v-if="selection.length > 0" class="flex flex-wrap items-center gap-2"> |
| 265 | + <div |
| 266 | + v-for="item in selection" |
| 267 | + :key="item.path" |
| 268 | + class="inline-flex items-center gap-1.5 h-7 pl-1.5 pr-0.5 text-xs border border-divider rounded-full" |
| 269 | + > |
| 270 | + <span class="w-3 h-3 rounded-full border border-divider" :style="{ backgroundColor: item.hex }" /> |
| 271 | + <span class="font-medium">{{ item.role }}</span> |
| 272 | + <span class="op-50 font-mono">{{ item.path }}</span> |
| 273 | + <AppCloseButton :label="`Remove ${item.role}`" size="sm" @click="onRemove(item.path)" /> |
| 274 | + </div> |
| 275 | + |
| 276 | + <button |
| 277 | + class="text-xs op-60 hover:op-100 underline underline-offset-2 cursor-pointer bg-transparent border-0" |
| 278 | + @click="onClear" |
| 279 | + > |
| 280 | + Clear |
| 281 | + </button> |
| 282 | + </div> |
| 283 | + |
223 | 284 | <!-- Export button with inline copied indicator --> |
224 | 285 | <div class="flex items-center gap-3"> |
225 | 286 | <button |
|
0 commit comments