@vuecs/design 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +57 -0
  3. package/assets/animations.css +506 -0
  4. package/assets/index.css +197 -0
  5. package/assets/palettes.css +303 -0
  6. package/assets/standalone.css +37 -0
  7. package/dist/core/color-mode/bind.d.ts +12 -0
  8. package/dist/core/color-mode/bind.d.ts.map +1 -0
  9. package/dist/core/color-mode/composable.d.ts +13 -0
  10. package/dist/core/color-mode/composable.d.ts.map +1 -0
  11. package/dist/core/color-mode/index.d.ts +4 -0
  12. package/dist/core/color-mode/index.d.ts.map +1 -0
  13. package/dist/core/color-mode/types.d.ts +36 -0
  14. package/dist/core/color-mode/types.d.ts.map +1 -0
  15. package/dist/core/color-palette/apply.d.ts +34 -0
  16. package/dist/core/color-palette/apply.d.ts.map +1 -0
  17. package/dist/core/color-palette/bind.d.ts +14 -0
  18. package/dist/core/color-palette/bind.d.ts.map +1 -0
  19. package/dist/core/color-palette/catalog.d.ts +78 -0
  20. package/dist/core/color-palette/catalog.d.ts.map +1 -0
  21. package/dist/core/color-palette/composable.d.ts +34 -0
  22. package/dist/core/color-palette/composable.d.ts.map +1 -0
  23. package/dist/core/color-palette/index.d.ts +7 -0
  24. package/dist/core/color-palette/index.d.ts.map +1 -0
  25. package/dist/core/color-palette/render.d.ts +16 -0
  26. package/dist/core/color-palette/render.d.ts.map +1 -0
  27. package/dist/core/color-palette/types.d.ts +107 -0
  28. package/dist/core/color-palette/types.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/core/theme-runtime/capture.d.ts +32 -0
  32. package/dist/core/theme-runtime/capture.d.ts.map +1 -0
  33. package/dist/core/theme-runtime/composable.d.ts +34 -0
  34. package/dist/core/theme-runtime/composable.d.ts.map +1 -0
  35. package/dist/core/theme-runtime/index.d.ts +4 -0
  36. package/dist/core/theme-runtime/index.d.ts.map +1 -0
  37. package/dist/core/theme-runtime/types.d.ts +42 -0
  38. package/dist/core/theme-runtime/types.d.ts.map +1 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.mjs +467 -0
  42. package/dist/index.mjs.map +1 -0
  43. package/dist/utils/object.d.ts +2 -0
  44. package/dist/utils/object.d.ts.map +1 -0
  45. package/package.json +83 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,467 @@
1
+ import { createSharedComposable, usePreferredDark, useStorage } from "@vueuse/core";
2
+ import { computed, inject, ref, watch, watchEffect } from "vue";
3
+ //#region src/core/theme-runtime/composable.ts
4
+ /**
5
+ * Globally-shared `InjectionKey` for the ThemeManager. SSR plugins
6
+ * (Nuxt, future frameworks) that need to look up the manager from
7
+ * outside Vue's component context use this with `app.runWithContext`
8
+ * (or equivalent) + `inject`.
9
+ *
10
+ * Bridge between `@vuecs/design` and `@vuecs/core`'s `ThemeManager`
11
+ * without a hard runtime dep: both packages reference the same
12
+ * `Symbol.for('VCThemeManager')` registry key, so the ThemeManager
13
+ * provided by `installThemeManager(app)` (in `@vuecs/core`) is
14
+ * reachable here. `Symbol.for(...)` is reference-equal across module
15
+ * boundaries, and the `InjectionKey<ThemeRuntimeManager>` cast
16
+ * surfaces the manager type to `inject()` callers without leaking
17
+ * `@vuecs/core` internals.
18
+ *
19
+ * `inject()` returns `undefined` if no ThemeManager is installed —
20
+ * design composables use that as the "no theme dispatch" fallback so
21
+ * they keep working in standalone apps that import `@vuecs/design`
22
+ * without `@vuecs/core`.
23
+ */
24
+ const THEME_RUNTIME_MANAGER_SYMBOL = Symbol.for("VCThemeManager");
25
+ /**
26
+ * Look up the ThemeManager installed by `app.use(vuecs)` (or
27
+ * `installThemeManager(app)`). Returns `undefined` if no manager is
28
+ * provided in the current setup context — design composables fall back
29
+ * to no-dispatch behaviour in that case.
30
+ *
31
+ * Must be called during `setup()` or another composable's setup phase
32
+ * (Vue's `inject()` requirement).
33
+ */
34
+ function useThemeRuntimeManager() {
35
+ return inject(THEME_RUNTIME_MANAGER_SYMBOL, void 0);
36
+ }
37
+ //#endregion
38
+ //#region src/core/color-mode/bind.ts
39
+ /**
40
+ * Wire any reactive `Ref<ColorMode>` into the design system: track
41
+ * system preference, expose the resolved light/dark value, and
42
+ * (optionally) sync the `.dark` / `.light` class on `<html>`.
43
+ *
44
+ * The `syncClass` watcher mirrors what `@vuecs/nuxt`'s SSR plugin
45
+ * writes server-side, so client hydration leaves the class in place.
46
+ */
47
+ function bindColorMode(source, options = {}) {
48
+ const { syncClass = true } = options;
49
+ const preferredDark = usePreferredDark();
50
+ const resolved = computed(() => {
51
+ if (source.value === "dark") return "dark";
52
+ if (source.value === "light") return "light";
53
+ return preferredDark.value ? "dark" : "light";
54
+ });
55
+ const mode = computed({
56
+ get: () => source.value,
57
+ set: (value) => {
58
+ source.value = value;
59
+ }
60
+ });
61
+ const isDark = computed({
62
+ get: () => resolved.value === "dark",
63
+ set: (value) => {
64
+ source.value = value ? "dark" : "light";
65
+ }
66
+ });
67
+ if (syncClass && typeof document !== "undefined") watch(resolved, (value) => {
68
+ document.documentElement.classList.toggle("dark", value === "dark");
69
+ document.documentElement.classList.toggle("light", value === "light");
70
+ }, { immediate: true });
71
+ const manager = useThemeRuntimeManager();
72
+ if (typeof document !== "undefined") watch([resolved, () => manager?.themes], ([value, themes]) => {
73
+ if (!themes) return;
74
+ for (const theme of themes) theme.colorMode?.handle(document, value);
75
+ }, { immediate: true });
76
+ function toggle() {
77
+ source.value = resolved.value === "dark" ? "light" : "dark";
78
+ }
79
+ return {
80
+ mode,
81
+ resolved,
82
+ isDark,
83
+ toggle
84
+ };
85
+ }
86
+ //#endregion
87
+ //#region src/core/color-mode/composable.ts
88
+ const DEFAULT_STORAGE_KEY$1 = "vc-color-mode";
89
+ const COLOR_MODE_SET = new Set([
90
+ "light",
91
+ "dark",
92
+ "system"
93
+ ]);
94
+ const sanitize = (value, fallback) => typeof value === "string" && COLOR_MODE_SET.has(value) ? value : fallback;
95
+ /**
96
+ * Reactive color-mode state with localStorage persistence. Shared
97
+ * across all call sites via `createSharedComposable` so toggling in
98
+ * one component updates every consumer (and the `<html>` class) in
99
+ * lockstep.
100
+ *
101
+ * For SSR-aware cookie-backed storage (Nuxt), the `@vuecs/nuxt` module
102
+ * ships its own `useColorMode()` that calls `bindColorMode()` directly
103
+ * with a cookie-backed ref. Both expose the same return shape.
104
+ */
105
+ const useColorMode = createSharedComposable((options = {}) => {
106
+ const { initial = "system", persist = true, storageKey = DEFAULT_STORAGE_KEY$1, syncClass = true } = options;
107
+ return bindColorMode(persist ? useStorage(storageKey, initial, void 0, { serializer: {
108
+ read: (raw) => sanitize(raw, initial),
109
+ write: (value) => value
110
+ } }) : ref(initial), { syncClass });
111
+ });
112
+ //#endregion
113
+ //#region src/core/color-palette/apply.ts
114
+ /**
115
+ * DOM id used for the runtime palette `<style>` block. Themes that
116
+ * implement palette switching should write into a `<style>` element
117
+ * with this id (via `applyColorPaletteCss()`); SSR plugins use the same
118
+ * id so client hydration replaces the server-rendered block atomically.
119
+ */
120
+ const COLOR_PALETTE_STYLE_ELEMENT_ID = "vc-color-palette";
121
+ /**
122
+ * Apply an arbitrary CSS string as a `<style id="vc-color-palette">` block
123
+ * (client-side only). Idempotent — subsequent calls replace the
124
+ * element's content.
125
+ *
126
+ * Theme-agnostic: accepts whatever CSS string the caller wants. Themes
127
+ * that ship palette switching compose this with their own renderer
128
+ * (e.g. `@vuecs/theme-tailwind`'s `renderColorPaletteStyles()`); other
129
+ * tooling can call it directly with custom CSS.
130
+ *
131
+ * The optional `nonce` parameter wires CSP nonce attribution: when set,
132
+ * the created `<style>` element carries `nonce="..."` so it survives a
133
+ * strict Content-Security-Policy. Subsequent calls update the
134
+ * attribute when the value changes, and clear it when the new value is
135
+ * undefined (so consumers can revoke a stale nonce on policy update).
136
+ * Consumers typically read this via
137
+ * `useConfig('nonce')` (from `@vuecs/core`, augmented by
138
+ * `@vuecs/theme-tailwind`); the per-theme `useColorPalette` wrappers
139
+ * already do this.
140
+ *
141
+ * On the server (`document` undefined) this is a no-op; SSR pre-render
142
+ * paths should serialize the renderer's output into the response head
143
+ * directly (with the nonce wired through framework-specific head
144
+ * APIs), then let the client take over on hydration.
145
+ */
146
+ function applyColorPaletteCss(css, doc = globalThis.document, nonce) {
147
+ if (!doc) return;
148
+ let style = doc.getElementById(COLOR_PALETTE_STYLE_ELEMENT_ID);
149
+ if (!style) {
150
+ style = doc.createElement("style");
151
+ style.id = COLOR_PALETTE_STYLE_ELEMENT_ID;
152
+ if (nonce) style.setAttribute("nonce", nonce);
153
+ doc.head.appendChild(style);
154
+ } else if (nonce) {
155
+ if (style.getAttribute("nonce") !== nonce) style.setAttribute("nonce", nonce);
156
+ } else if (style.hasAttribute("nonce")) style.removeAttribute("nonce");
157
+ style.textContent = css;
158
+ }
159
+ //#endregion
160
+ //#region src/core/color-palette/bind.ts
161
+ /**
162
+ * Wire any reactive `Ref<T>` into the palette runtime: render the
163
+ * current value via the theme-supplied `render` function, apply it via
164
+ * `applyColorPaletteCss`, and re-apply on every change.
165
+ *
166
+ * Generic: each theme defines its own palette shape `T` and renderer.
167
+ * `@vuecs/theme-tailwind` wraps this with its `ColorPaletteConfig` and
168
+ * `renderColorPaletteStyles`; community themes can do the same with their
169
+ * own shapes — including custom merge semantics via `options.extend`.
170
+ */
171
+ function bindColorPalette(source, options) {
172
+ const { render, extend, document = globalThis.document, nonce } = options;
173
+ const resolveNonce = typeof nonce === "function" ? nonce : () => nonce;
174
+ if (document) applyColorPaletteCss(render(source.value), document, resolveNonce());
175
+ watch([source, () => resolveNonce()], () => {
176
+ applyColorPaletteCss(render(source.value), document, resolveNonce());
177
+ }, { deep: true });
178
+ return {
179
+ current: computed(() => source.value),
180
+ set(palette) {
181
+ source.value = palette;
182
+ },
183
+ extend(partial) {
184
+ source.value = extend(source.value, partial);
185
+ }
186
+ };
187
+ }
188
+ //#endregion
189
+ //#region src/core/color-palette/catalog.ts
190
+ /**
191
+ * Canonical palette catalog for the vuecs design system.
192
+ *
193
+ * The names originated in Tailwind v4 (see `@vuecs/design/standalone`'s
194
+ * `palettes.css`, generated from `tailwindcss/theme.css`), but the
195
+ * catalog is now considered design-owned: every supported palette
196
+ * source — Tailwind via `@import "tailwindcss"`, or the
197
+ * standalone subpath — provides the matching `--color-<palette>-<shade>`
198
+ * literals so `setColorPalette()` resolves correctly regardless of
199
+ * whether Tailwind is loaded.
200
+ *
201
+ * Themes typically reuse this catalog verbatim (theme-tailwind and
202
+ * theme-bulma both declare `palette.names: COLOR_PALETTES`). A theme
203
+ * with extra palette names just defines its own local union:
204
+ *
205
+ * type AcmePaletteName = ColorPaletteName | 'acme-blue';
206
+ *
207
+ * — and ships its own `palette.names` array.
208
+ */
209
+ /**
210
+ * The six semantic scales every vuecs theme exposes through the
211
+ * `--vc-color-<scale>-*` variable family. A `setColorPalette({
212
+ * primary: 'green' })` call binds one scale to one palette catalog
213
+ * entry at runtime.
214
+ */
215
+ const SEMANTIC_SCALES = [
216
+ "primary",
217
+ "neutral",
218
+ "success",
219
+ "warning",
220
+ "error",
221
+ "info"
222
+ ];
223
+ /**
224
+ * The 22 catalog palettes shipped with `@vuecs/design` (sourced
225
+ * verbatim from Tailwind v4). Any of these can be assigned to a
226
+ * `SemanticScaleName` via `setColorPalette()`.
227
+ */
228
+ const COLOR_PALETTES = [
229
+ "slate",
230
+ "gray",
231
+ "zinc",
232
+ "neutral",
233
+ "stone",
234
+ "red",
235
+ "orange",
236
+ "amber",
237
+ "yellow",
238
+ "lime",
239
+ "green",
240
+ "emerald",
241
+ "teal",
242
+ "cyan",
243
+ "sky",
244
+ "blue",
245
+ "indigo",
246
+ "violet",
247
+ "purple",
248
+ "fuchsia",
249
+ "pink",
250
+ "rose"
251
+ ];
252
+ /**
253
+ * Tailwind-style 11-stop shade ladder. The same ladder appears in
254
+ * every catalog entry (the standalone subpath's `palettes.css` and
255
+ * Tailwind's own `theme.css` both emit `--color-<palette>-<shade>`
256
+ * for each stop).
257
+ */
258
+ const COLOR_PALETTE_SHADES = [
259
+ "50",
260
+ "100",
261
+ "200",
262
+ "300",
263
+ "400",
264
+ "500",
265
+ "600",
266
+ "700",
267
+ "800",
268
+ "900",
269
+ "950"
270
+ ];
271
+ //#endregion
272
+ //#region src/utils/object.ts
273
+ function isObject(input) {
274
+ return typeof input === "object" && input !== null && !Array.isArray(input);
275
+ }
276
+ //#endregion
277
+ //#region src/core/color-palette/render.ts
278
+ /**
279
+ * Concatenate every installed theme's `palette.handle` output into a
280
+ * single CSS string. SSR plugins emit the result as the
281
+ * `<style id="vc-color-palette">` block so palette-aware themes flow
282
+ * on first paint.
283
+ *
284
+ * Mirrors the client-side concat semantics in `useColorPalette()`:
285
+ * non-overlapping rules from different themes coexist; CSS cascade
286
+ * resolves any incidental overlap with later-rule-wins.
287
+ *
288
+ * Errors thrown by a theme's handler are caught + logged so one
289
+ * broken theme can't crash SSR; other themes still emit.
290
+ */
291
+ function renderColorPaletteFromThemes(themes, palette) {
292
+ const parts = [];
293
+ for (const theme of themes) {
294
+ if (!theme.palette?.handle) continue;
295
+ const handle = theme.palette.handle.bind(theme.palette);
296
+ const input = applyScaleAliases(palette, theme.palette.scaleAliases);
297
+ try {
298
+ const out = handle(input);
299
+ if (out) parts.push(out);
300
+ } catch (e) {
301
+ if (typeof console !== "undefined") console.warn("[vuecs] theme palette.handle failed; skipping:", e);
302
+ }
303
+ }
304
+ return parts.join("\n");
305
+ }
306
+ const FORBIDDEN_KEYS = new Set([
307
+ "__proto__",
308
+ "constructor",
309
+ "prototype"
310
+ ]);
311
+ function applyScaleAliases(palette, aliases) {
312
+ if (!aliases) return palette;
313
+ const out = Object.create(null);
314
+ for (const [k, v] of Object.entries(palette)) {
315
+ const target = aliases[k] ?? k;
316
+ if (FORBIDDEN_KEYS.has(target)) continue;
317
+ out[target] = v;
318
+ }
319
+ return out;
320
+ }
321
+ //#endregion
322
+ //#region src/core/color-palette/composable.ts
323
+ const DEFAULT_STORAGE_KEY = "vc-color-palette";
324
+ const SEMANTIC_SCALE_SET = new Set(SEMANTIC_SCALES);
325
+ const PALETTE_NAME_SET = new Set(COLOR_PALETTES);
326
+ function defaultSanitize(value) {
327
+ if (!isObject(value)) return {};
328
+ const out = {};
329
+ for (const [k, v] of Object.entries(value)) if (SEMANTIC_SCALE_SET.has(k) && typeof v === "string" && PALETTE_NAME_SET.has(v)) out[k] = v;
330
+ return out;
331
+ }
332
+ const shallowMerge = (current, partial) => ({
333
+ ...current,
334
+ ...partial
335
+ });
336
+ /**
337
+ * Theme-aware reactive palette state — un-shared variant.
338
+ *
339
+ * Concatenates every installed theme's `palette.handle` output into the
340
+ * `<style id="vc-color-palette">` element. Walking the installed themes
341
+ * each render means runtime theme swaps via `setThemes()` automatically
342
+ * pick up the new renderer chain.
343
+ *
344
+ * Concat (rather than last-wins) is the doctrinal semantic: when an app
345
+ * stacks multiple palette-aware themes (the docs-site case where
346
+ * Tailwind components and Bulma components share the same picker UI),
347
+ * each theme's renderer emits its own non-overlapping CSS rules —
348
+ * Tailwind rebinds `--vc-color-*`, Bulma writes per-variant HSL channel
349
+ * vars. The CSS cascade resolves any incidental overlap with
350
+ * later-rule-wins semantics, so concat behaves like last-wins for
351
+ * overlapping properties AND emits both themes' unique properties.
352
+ *
353
+ * Production callers should use `useColorPalette` (the shared variant
354
+ * below). This un-shared form is exposed primarily for testing — every
355
+ * call creates a fresh `watchEffect` and palette state.
356
+ */
357
+ function useColorPaletteUnshared(options = {}) {
358
+ const { initial = {}, source, persist = true, storageKey = DEFAULT_STORAGE_KEY, sanitize = defaultSanitize, extend = shallowMerge, nonce } = options;
359
+ const resolveNonce = typeof nonce === "function" ? nonce : () => nonce;
360
+ const manager = useThemeRuntimeManager();
361
+ const storage = source ?? (persist ? useStorage(storageKey, sanitize(initial), void 0, { serializer: {
362
+ read: (raw) => {
363
+ try {
364
+ return sanitize(JSON.parse(raw));
365
+ } catch {
366
+ return sanitize({});
367
+ }
368
+ },
369
+ write: (value) => JSON.stringify(value)
370
+ } }) : ref(sanitize(initial)));
371
+ const renderConcatenated = (palette) => {
372
+ const themes = manager?.themes;
373
+ if (!themes || themes.length === 0) return "";
374
+ return renderColorPaletteFromThemes(themes, sanitize(palette));
375
+ };
376
+ if (typeof document !== "undefined") watchEffect(() => {
377
+ applyColorPaletteCss(renderConcatenated(storage.value), void 0, resolveNonce());
378
+ });
379
+ return {
380
+ current: computed(() => storage.value),
381
+ set(palette) {
382
+ storage.value = palette;
383
+ },
384
+ extend(partial) {
385
+ storage.value = extend(storage.value, partial);
386
+ }
387
+ };
388
+ }
389
+ /**
390
+ * Theme-aware reactive palette state with localStorage persistence
391
+ * (plan 021 slice 2).
392
+ *
393
+ * Wrapped with `createSharedComposable` so every call site shares the
394
+ * same ref + watcher. For SSR-aware cookie-backed storage (Nuxt), the
395
+ * matching Nuxt module ships its own composable that calls
396
+ * `bindColorPalette()` directly with a cookie-backed ref.
397
+ */
398
+ const useColorPalette = createSharedComposable(useColorPaletteUnshared);
399
+ //#endregion
400
+ //#region src/core/theme-runtime/capture.ts
401
+ /**
402
+ * Build a synthetic Document-like whose `documentElement` records
403
+ * `setAttribute` / `removeAttribute` calls into the supplied target
404
+ * record, plus a no-op `classList`. **No other Document or Element
405
+ * APIs are stubbed** — themes that call `doc.createElement`,
406
+ * `doc.head.appendChild`, etc. WILL throw at SSR runtime. Themes
407
+ * needing richer DOM access from `colorMode.handle` should guard their
408
+ * CSR-only logic with `if (typeof window === 'undefined') return;`
409
+ * and split the SSR-flowing bits into declarative `setAttribute` calls.
410
+ *
411
+ * Exposed for advanced consumers; the higher-level
412
+ * `captureColorModeAttrs()` covers the common case and catches errors
413
+ * per theme so a single broken theme can't crash SSR.
414
+ */
415
+ function createCaptureDocument(target) {
416
+ return { documentElement: {
417
+ setAttribute(name, value) {
418
+ target[name] = value;
419
+ },
420
+ removeAttribute(name) {
421
+ delete target[name];
422
+ },
423
+ classList: {
424
+ add() {},
425
+ remove() {},
426
+ toggle() {
427
+ return false;
428
+ },
429
+ contains() {
430
+ return false;
431
+ },
432
+ replace() {}
433
+ }
434
+ } };
435
+ }
436
+ /**
437
+ * Walk every installed theme's `colorMode.handle` against a synthetic
438
+ * Document and capture the resulting attribute mutations. SSR plugins
439
+ * use this to flow `data-bs-theme` / `data-theme` (or any other
440
+ * attribute a theme declares) into `useHead({ htmlAttrs })` before
441
+ * first paint.
442
+ *
443
+ * Each theme's handler runs in install order. If multiple themes set
444
+ * the same attribute, the last one wins — same semantic as the live
445
+ * `document.documentElement.setAttribute` chain on the client.
446
+ *
447
+ * Errors thrown by a theme's handler are caught + logged as a warning
448
+ * so one malformed theme can't crash SSR; other themes still run.
449
+ */
450
+ function captureColorModeAttrs(themes, mode) {
451
+ const attrs = Object.create(null);
452
+ const fakeDoc = createCaptureDocument(attrs);
453
+ for (const theme of themes) {
454
+ if (!theme.colorMode?.handle) continue;
455
+ const handle = theme.colorMode?.handle.bind(theme.colorMode);
456
+ try {
457
+ handle(fakeDoc, mode);
458
+ } catch (e) {
459
+ if (typeof console !== "undefined") console.warn("[vuecs] theme colorMode.handle failed; skipping:", e);
460
+ }
461
+ }
462
+ return attrs;
463
+ }
464
+ //#endregion
465
+ export { COLOR_PALETTES, COLOR_PALETTE_SHADES, COLOR_PALETTE_STYLE_ELEMENT_ID, SEMANTIC_SCALES, THEME_RUNTIME_MANAGER_SYMBOL, applyColorPaletteCss, bindColorMode, bindColorPalette, captureColorModeAttrs, createCaptureDocument, renderColorPaletteFromThemes, useColorMode, useColorPalette, useColorPaletteUnshared, useThemeRuntimeManager };
466
+
467
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["DEFAULT_STORAGE_KEY"],"sources":["../src/core/theme-runtime/composable.ts","../src/core/color-mode/bind.ts","../src/core/color-mode/composable.ts","../src/core/color-palette/apply.ts","../src/core/color-palette/bind.ts","../src/core/color-palette/catalog.ts","../src/utils/object.ts","../src/core/color-palette/render.ts","../src/core/color-palette/composable.ts","../src/core/theme-runtime/capture.ts"],"sourcesContent":["import type { InjectionKey } from 'vue';\nimport { inject } from 'vue';\nimport type { ThemeRuntimeManager } from './types';\n\n/**\n * Globally-shared `InjectionKey` for the ThemeManager. SSR plugins\n * (Nuxt, future frameworks) that need to look up the manager from\n * outside Vue's component context use this with `app.runWithContext`\n * (or equivalent) + `inject`.\n *\n * Bridge between `@vuecs/design` and `@vuecs/core`'s `ThemeManager`\n * without a hard runtime dep: both packages reference the same\n * `Symbol.for('VCThemeManager')` registry key, so the ThemeManager\n * provided by `installThemeManager(app)` (in `@vuecs/core`) is\n * reachable here. `Symbol.for(...)` is reference-equal across module\n * boundaries, and the `InjectionKey<ThemeRuntimeManager>` cast\n * surfaces the manager type to `inject()` callers without leaking\n * `@vuecs/core` internals.\n *\n * `inject()` returns `undefined` if no ThemeManager is installed —\n * design composables use that as the \"no theme dispatch\" fallback so\n * they keep working in standalone apps that import `@vuecs/design`\n * without `@vuecs/core`.\n */\nexport const THEME_RUNTIME_MANAGER_SYMBOL = Symbol.for('VCThemeManager') as InjectionKey<ThemeRuntimeManager>;\n\n/**\n * Look up the ThemeManager installed by `app.use(vuecs)` (or\n * `installThemeManager(app)`). Returns `undefined` if no manager is\n * provided in the current setup context — design composables fall back\n * to no-dispatch behaviour in that case.\n *\n * Must be called during `setup()` or another composable's setup phase\n * (Vue's `inject()` requirement).\n */\nexport function useThemeRuntimeManager(): ThemeRuntimeManager | undefined {\n return inject(THEME_RUNTIME_MANAGER_SYMBOL, undefined);\n}\n","import { usePreferredDark } from '@vueuse/core';\nimport { computed, watch } from 'vue';\nimport type { Ref } from 'vue';\nimport { useThemeRuntimeManager } from '../theme-runtime/composable';\nimport type { ColorMode, UseColorModeOptions, UseColorModeReturn } from './types';\n\n/**\n * Wire any reactive `Ref<ColorMode>` into the design system: track\n * system preference, expose the resolved light/dark value, and\n * (optionally) sync the `.dark` / `.light` class on `<html>`.\n *\n * The `syncClass` watcher mirrors what `@vuecs/nuxt`'s SSR plugin\n * writes server-side, so client hydration leaves the class in place.\n */\nexport function bindColorMode(\n source: Ref<ColorMode>,\n options: Pick<UseColorModeOptions, 'syncClass'> = {},\n): UseColorModeReturn {\n const { syncClass = true } = options;\n const preferredDark = usePreferredDark();\n\n const resolved = computed<'light' | 'dark'>(() => {\n if (source.value === 'dark') return 'dark';\n if (source.value === 'light') return 'light';\n return preferredDark.value ? 'dark' : 'light';\n });\n\n const mode = computed<ColorMode>({\n get: () => source.value,\n set: (value) => {\n source.value = value;\n },\n });\n\n const isDark = computed<boolean>({\n get: () => resolved.value === 'dark',\n set: (value) => {\n source.value = value ? 'dark' : 'light';\n },\n });\n\n if (syncClass && typeof document !== 'undefined') {\n watch(\n resolved,\n (value) => {\n document.documentElement.classList.toggle('dark', value === 'dark');\n document.documentElement.classList.toggle('light', value === 'light');\n },\n { immediate: true },\n );\n }\n\n /*\n * Theme-configurable dispatch (plan 021): each installed theme that\n * declares a `colorMode.handle` hook gets called with the resolved\n * mode. Themes use this to mirror framework-specific dark-mode\n * markers (theme-bootstrap → `data-bs-theme`, theme-bulma →\n * `data-theme`) so framework chrome flips alongside vuecs's own\n * `.dark` class without per-app `watchEffect` mirrors.\n *\n * The watch source is a tuple `[resolved, () => manager?.themes]`\n * because Vue's `watch(source, callback)` only tracks reactive\n * dependencies accessed inside the source — callback reads do NOT\n * subscribe. Including the themes getter in the source means\n * `ThemeManager.setThemes()` (which mutates the underlying\n * `shallowRef`) re-fires the dispatch with the new theme list.\n */\n const manager = useThemeRuntimeManager();\n if (typeof document !== 'undefined') {\n watch(\n [resolved, () => manager?.themes],\n ([value, themes]) => {\n if (!themes) return;\n for (const theme of themes) {\n theme.colorMode?.handle(document, value);\n }\n },\n { immediate: true },\n );\n }\n\n function toggle(): void {\n source.value = resolved.value === 'dark' ? 'light' : 'dark';\n }\n\n return {\n mode,\n resolved,\n isDark,\n toggle,\n };\n}\n","import { createSharedComposable, useStorage } from '@vueuse/core';\nimport { ref } from 'vue';\nimport type { Ref } from 'vue';\nimport { bindColorMode } from './bind';\nimport type { ColorMode, UseColorModeOptions, UseColorModeReturn } from './types';\n\nconst DEFAULT_STORAGE_KEY = 'vc-color-mode';\nconst COLOR_MODES: readonly ColorMode[] = ['light', 'dark', 'system'];\nconst COLOR_MODE_SET = new Set<string>(COLOR_MODES);\n\nconst sanitize = (value: unknown, fallback: ColorMode): ColorMode => (\n typeof value === 'string' && COLOR_MODE_SET.has(value) ?\n (value as ColorMode) :\n fallback\n);\n\n/**\n * Reactive color-mode state with localStorage persistence. Shared\n * across all call sites via `createSharedComposable` so toggling in\n * one component updates every consumer (and the `<html>` class) in\n * lockstep.\n *\n * For SSR-aware cookie-backed storage (Nuxt), the `@vuecs/nuxt` module\n * ships its own `useColorMode()` that calls `bindColorMode()` directly\n * with a cookie-backed ref. Both expose the same return shape.\n */\nexport const useColorMode = createSharedComposable(\n (options: UseColorModeOptions = {}): UseColorModeReturn => {\n const {\n initial = 'system',\n persist = true,\n storageKey = DEFAULT_STORAGE_KEY,\n syncClass = true,\n } = options;\n\n const storage: Ref<ColorMode> = persist ?\n useStorage<ColorMode>(storageKey, initial, undefined, {\n serializer: {\n read: (raw) => sanitize(raw, initial),\n write: (value) => value,\n },\n }) :\n ref<ColorMode>(initial);\n\n return bindColorMode(storage, { syncClass });\n },\n);\n","/**\n * DOM id used for the runtime palette `<style>` block. Themes that\n * implement palette switching should write into a `<style>` element\n * with this id (via `applyColorPaletteCss()`); SSR plugins use the same\n * id so client hydration replaces the server-rendered block atomically.\n */\nexport const COLOR_PALETTE_STYLE_ELEMENT_ID = 'vc-color-palette';\n\n/**\n * Apply an arbitrary CSS string as a `<style id=\"vc-color-palette\">` block\n * (client-side only). Idempotent — subsequent calls replace the\n * element's content.\n *\n * Theme-agnostic: accepts whatever CSS string the caller wants. Themes\n * that ship palette switching compose this with their own renderer\n * (e.g. `@vuecs/theme-tailwind`'s `renderColorPaletteStyles()`); other\n * tooling can call it directly with custom CSS.\n *\n * The optional `nonce` parameter wires CSP nonce attribution: when set,\n * the created `<style>` element carries `nonce=\"...\"` so it survives a\n * strict Content-Security-Policy. Subsequent calls update the\n * attribute when the value changes, and clear it when the new value is\n * undefined (so consumers can revoke a stale nonce on policy update).\n * Consumers typically read this via\n * `useConfig('nonce')` (from `@vuecs/core`, augmented by\n * `@vuecs/theme-tailwind`); the per-theme `useColorPalette` wrappers\n * already do this.\n *\n * On the server (`document` undefined) this is a no-op; SSR pre-render\n * paths should serialize the renderer's output into the response head\n * directly (with the nonce wired through framework-specific head\n * APIs), then let the client take over on hydration.\n */\nexport function applyColorPaletteCss(\n css: string,\n doc: Document | undefined = globalThis.document,\n nonce?: string,\n): void {\n if (!doc) return;\n\n let style = doc.getElementById(COLOR_PALETTE_STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n if (!style) {\n style = doc.createElement('style');\n style.id = COLOR_PALETTE_STYLE_ELEMENT_ID;\n if (nonce) style.setAttribute('nonce', nonce);\n doc.head.appendChild(style);\n } else if (nonce) {\n if (style.getAttribute('nonce') !== nonce) {\n style.setAttribute('nonce', nonce);\n }\n } else if (style.hasAttribute('nonce')) {\n style.removeAttribute('nonce');\n }\n style.textContent = css;\n}\n","import { computed, watch } from 'vue';\nimport type { Ref } from 'vue';\nimport { applyColorPaletteCss } from './apply';\nimport type { BindColorPaletteOptions, UseColorPaletteReturn } from './types';\n\n/**\n * Wire any reactive `Ref<T>` into the palette runtime: render the\n * current value via the theme-supplied `render` function, apply it via\n * `applyColorPaletteCss`, and re-apply on every change.\n *\n * Generic: each theme defines its own palette shape `T` and renderer.\n * `@vuecs/theme-tailwind` wraps this with its `ColorPaletteConfig` and\n * `renderColorPaletteStyles`; community themes can do the same with their\n * own shapes — including custom merge semantics via `options.extend`.\n */\nexport function bindColorPalette<T>(\n source: Ref<T>,\n options: BindColorPaletteOptions<T>,\n): UseColorPaletteReturn<T> {\n const {\n render,\n extend,\n document = globalThis.document,\n nonce,\n } = options;\n\n const resolveNonce: () => string | undefined = typeof nonce === 'function' ?\n nonce :\n () => nonce;\n\n if (document) {\n applyColorPaletteCss(render(source.value), document, resolveNonce());\n }\n /*\n * Watch both the palette source AND the resolved nonce. The nonce\n * getter form (e.g. `() => useConfig('nonce').value`) reads a\n * reactive ref, so a nonce-only rotation (CSP policy update via\n * `setConfig({ nonce })`) re-applies the `<style>` element's\n * attribute without needing a palette mutation. Static nonce\n * strings return the same primitive on every call → the nonce\n * lane of the watcher is silently inert.\n */\n watch(\n [source, () => resolveNonce()] as const,\n () => {\n applyColorPaletteCss(render(source.value), document, resolveNonce());\n },\n { deep: true },\n );\n\n return {\n current: computed(() => source.value),\n set(palette) {\n source.value = palette;\n },\n extend(partial) {\n source.value = extend(source.value, partial);\n },\n };\n}\n","/**\n * Canonical palette catalog for the vuecs design system.\n *\n * The names originated in Tailwind v4 (see `@vuecs/design/standalone`'s\n * `palettes.css`, generated from `tailwindcss/theme.css`), but the\n * catalog is now considered design-owned: every supported palette\n * source — Tailwind via `@import \"tailwindcss\"`, or the\n * standalone subpath — provides the matching `--color-<palette>-<shade>`\n * literals so `setColorPalette()` resolves correctly regardless of\n * whether Tailwind is loaded.\n *\n * Themes typically reuse this catalog verbatim (theme-tailwind and\n * theme-bulma both declare `palette.names: COLOR_PALETTES`). A theme\n * with extra palette names just defines its own local union:\n *\n * type AcmePaletteName = ColorPaletteName | 'acme-blue';\n *\n * — and ships its own `palette.names` array.\n */\n\n/**\n * The six semantic scales every vuecs theme exposes through the\n * `--vc-color-<scale>-*` variable family. A `setColorPalette({\n * primary: 'green' })` call binds one scale to one palette catalog\n * entry at runtime.\n */\nexport const SEMANTIC_SCALES = [\n 'primary',\n 'neutral',\n 'success',\n 'warning',\n 'error',\n 'info',\n] as const;\n\nexport type SemanticScaleName = typeof SEMANTIC_SCALES[number];\n\n/**\n * The 22 catalog palettes shipped with `@vuecs/design` (sourced\n * verbatim from Tailwind v4). Any of these can be assigned to a\n * `SemanticScaleName` via `setColorPalette()`.\n */\nexport const COLOR_PALETTES = [\n 'slate',\n 'gray',\n 'zinc',\n 'neutral',\n 'stone',\n 'red',\n 'orange',\n 'amber',\n 'yellow',\n 'lime',\n 'green',\n 'emerald',\n 'teal',\n 'cyan',\n 'sky',\n 'blue',\n 'indigo',\n 'violet',\n 'purple',\n 'fuchsia',\n 'pink',\n 'rose',\n] as const;\n\n/**\n * Augmentation hook for community themes that extend the catalog with\n * their own palette names. Defaults to empty — `ColorPaletteName` is just\n * the 22-name Tailwind-derived catalog. A theme that ships extra palettes\n * widens the union via declaration merging:\n *\n * declare module '@vuecs/design' {\n * interface ExtraColorPaletteNames {\n * 'acme-blue': true;\n * 'acme-orange': true;\n * }\n * }\n *\n * Both the SPA composables (`@vuecs/theme-tailwind`'s `useColorPalette`,\n * etc.) and `@vuecs/nuxt`'s `colorPalette.value` option pick up the\n * extension automatically because both type against `ColorPaletteName`.\n */\nexport interface ExtraColorPaletteNames {}\n\nexport type ColorPaletteName = typeof COLOR_PALETTES[number] | keyof ExtraColorPaletteNames;\n\n/**\n * Tailwind-style 11-stop shade ladder. The same ladder appears in\n * every catalog entry (the standalone subpath's `palettes.css` and\n * Tailwind's own `theme.css` both emit `--color-<palette>-<shade>`\n * for each stop).\n */\nexport const COLOR_PALETTE_SHADES = [\n '50',\n '100',\n '200',\n '300',\n '400',\n '500',\n '600',\n '700',\n '800',\n '900',\n '950',\n] as const;\n\nexport type ColorPaletteShade = typeof COLOR_PALETTE_SHADES[number];\n\n/**\n * Canonical runtime palette config — a partial mapping of every\n * semantic scale to a catalog palette name. Used by every theme that\n * opts into runtime palette switching (`theme-tailwind`, `theme-bulma`,\n * and any future palette-aware theme).\n *\n * Both keys (`SemanticScaleName`) and values (`ColorPaletteName`)\n * widen automatically via declaration merging: `ExtraColorPaletteNames`\n * adds value-side names; the canonical scale list is fixed at six.\n *\n * Themes whose internal scale names diverge from the canonical six\n * declare a `scaleAliases` map on their `Theme.palette` slot — the\n * dispatcher translates input keys before calling `palette.handle`,\n * so the public-facing config still uses canonical names.\n */\nexport type ColorPaletteConfig = Partial<Record<SemanticScaleName, ColorPaletteName>>;\n","/*\n * Local mirror of `@vuecs/core`'s `isObject` helper. Duplicated (not\n * imported from core) so `@vuecs/design` keeps Layer-0 standing — no\n * internal runtime deps, works standalone with BS / Bulma / no theme.\n *\n * Keep this in sync with `packages/core/src/utils/object.ts` if the\n * semantics ever change.\n */\nexport function isObject(input: unknown): input is Record<string, unknown> {\n return typeof input === 'object' &&\n input !== null &&\n !Array.isArray(input);\n}\n","import type { ThemeRuntimeEntry } from '../theme-runtime/types';\n\n/**\n * Concatenate every installed theme's `palette.handle` output into a\n * single CSS string. SSR plugins emit the result as the\n * `<style id=\"vc-color-palette\">` block so palette-aware themes flow\n * on first paint.\n *\n * Mirrors the client-side concat semantics in `useColorPalette()`:\n * non-overlapping rules from different themes coexist; CSS cascade\n * resolves any incidental overlap with later-rule-wins.\n *\n * Errors thrown by a theme's handler are caught + logged so one\n * broken theme can't crash SSR; other themes still emit.\n */\nexport function renderColorPaletteFromThemes(\n themes: readonly ThemeRuntimeEntry[],\n palette: Record<string, string>,\n): string {\n const parts: string[] = [];\n for (const theme of themes) {\n if (!theme.palette?.handle) {\n continue;\n }\n\n // Preserve `this` so themes whose handle method reads from\n // `theme.palette` state (e.g. caches a renderer instance) keep\n // working when the function is extracted before call.\n const handle = theme.palette.handle.bind(theme.palette);\n\n // Apply per-theme scale aliasing (plan 026). Themes whose\n // internal scale names diverge from the canonical six declare\n // a rename map; the dispatcher rewrites input keys so the\n // theme's renderer sees its own naming while the public-facing\n // palette config stays canonical.\n const input = applyScaleAliases(palette, theme.palette.scaleAliases);\n\n try {\n const out = handle(input);\n if (out) {\n parts.push(out);\n }\n } catch (e) {\n if (typeof console !== 'undefined') {\n // eslint-disable-next-line no-console\n console.warn('[vuecs] theme palette.handle failed; skipping:', e);\n }\n }\n }\n return parts.join('\\n');\n}\n\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nfunction applyScaleAliases(\n palette: Record<string, string>,\n aliases: Record<string, string> | undefined,\n): Record<string, string> {\n if (!aliases) {\n return palette;\n }\n // Use a prototype-free object + explicit deny-list so a theme-provided\n // `scaleAliases` mapping a canonical key to `__proto__` (or any other\n // prototype-touching name) can't reach Object.prototype. Defense in\n // depth: the upstream sanitize already filters input palette keys to\n // SEMANTIC_SCALES, but `aliases` itself is theme-provided and unsanitized.\n const out: Record<string, string> = Object.create(null);\n for (const [k, v] of Object.entries(palette)) {\n const target = aliases[k] ?? k;\n if (FORBIDDEN_KEYS.has(target)) continue;\n out[target] = v;\n }\n return out;\n}\n","import { createSharedComposable, useStorage } from '@vueuse/core';\nimport type { ComputedRef, Ref } from 'vue';\nimport { computed, ref, watchEffect } from 'vue';\nimport { isObject } from '../../utils/object';\nimport { useThemeRuntimeManager } from '../theme-runtime/composable';\nimport { applyColorPaletteCss } from './apply';\nimport { COLOR_PALETTES, SEMANTIC_SCALES } from './catalog';\nimport { renderColorPaletteFromThemes } from './render';\nimport type { UseColorPaletteOptions, UseColorPaletteReturn } from './types';\n\nconst DEFAULT_STORAGE_KEY = 'vc-color-palette';\n\nconst SEMANTIC_SCALE_SET = new Set<string>(SEMANTIC_SCALES);\nconst PALETTE_NAME_SET = new Set<string>(COLOR_PALETTES);\n\nfunction defaultSanitize<T>(value: unknown): T {\n /*\n * Defensive default: reject primitives + arrays, then filter to the\n * canonical catalog (six semantic scales × 22 palette names). Cookies\n * and localStorage can hold anything (older library version,\n * hand-edited DevTools value, payload written under the same key by\n * a different theme). Dropping unknown keys prevents broken CSS\n * downstream — the renderers each defend their inputs as a second\n * line of defense, but applying the filter at the dispatcher means\n * theme `palette.handle` hooks never see junk on the happy path.\n *\n * Themes whose palette config widens past the canonical catalog\n * (`ExtraColorPaletteNames` or a divergent scale set via\n * `scaleAliases`) pass their own `sanitize` to override.\n */\n if (!isObject(value)) {\n return {} as T;\n }\n\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (\n SEMANTIC_SCALE_SET.has(k) &&\n typeof v === 'string' &&\n PALETTE_NAME_SET.has(v)\n ) {\n out[k] = v;\n }\n }\n return out as T;\n}\n\nconst shallowMerge = <T extends Record<string, unknown>>(current: T, partial: Partial<T>): T => ({\n ...current,\n ...partial,\n} as T);\n\n/**\n * Theme-aware reactive palette state — un-shared variant.\n *\n * Concatenates every installed theme's `palette.handle` output into the\n * `<style id=\"vc-color-palette\">` element. Walking the installed themes\n * each render means runtime theme swaps via `setThemes()` automatically\n * pick up the new renderer chain.\n *\n * Concat (rather than last-wins) is the doctrinal semantic: when an app\n * stacks multiple palette-aware themes (the docs-site case where\n * Tailwind components and Bulma components share the same picker UI),\n * each theme's renderer emits its own non-overlapping CSS rules —\n * Tailwind rebinds `--vc-color-*`, Bulma writes per-variant HSL channel\n * vars. The CSS cascade resolves any incidental overlap with\n * later-rule-wins semantics, so concat behaves like last-wins for\n * overlapping properties AND emits both themes' unique properties.\n *\n * Production callers should use `useColorPalette` (the shared variant\n * below). This un-shared form is exposed primarily for testing — every\n * call creates a fresh `watchEffect` and palette state.\n */\nexport function useColorPaletteUnshared<\n T extends Record<string, unknown> = Record<string, string>,\n>(options: UseColorPaletteOptions<T> = {}): UseColorPaletteReturn<T> {\n const {\n initial = {} as T,\n source,\n persist = true,\n storageKey = DEFAULT_STORAGE_KEY,\n sanitize = defaultSanitize<T>,\n extend = shallowMerge,\n nonce,\n } = options;\n\n const resolveNonce: () => string | undefined = typeof nonce === 'function' ?\n nonce :\n () => nonce;\n\n const manager = useThemeRuntimeManager();\n\n /*\n * `source` lets external persistence layers (Nuxt's `useCookie`,\n * custom IndexedDB-backed refs, etc.) replace the default storage\n * without forking the dispatch logic. When provided, `persist` /\n * `storageKey` / `initial` are ignored — the caller is responsible\n * for the initial value and any persistence semantics.\n */\n const storage: Ref<T> = source ??\n (persist ?\n useStorage<T>(storageKey, sanitize(initial), undefined, {\n serializer: {\n read: (raw): T => {\n try {\n return sanitize(JSON.parse(raw));\n } catch {\n return sanitize({});\n }\n },\n write: (value) => JSON.stringify(value),\n },\n }) :\n ref<T>(sanitize(initial)) as Ref<T>);\n\n const renderConcatenated = (palette: T): string => {\n const themes = manager?.themes;\n if (!themes || themes.length === 0) {\n return '';\n }\n\n /*\n * Sanitize at the render boundary so the `source`-provided path\n * (Nuxt cookie, custom IndexedDB, etc.) gets the same defensive\n * filter as the default `useStorage` path (which sanitizes at\n * `serializer.read` time). Theme `palette.handle` hooks should\n * never see primitives, arrays, or other malformed payloads —\n * downstream theme renderers each filter their own input, but\n * applying `sanitize` here keeps the per-theme filter as a\n * second line of defense rather than the only one.\n */\n const sanitized = sanitize(palette);\n return renderColorPaletteFromThemes(themes, sanitized as Record<string, string>);\n };\n\n if (typeof document !== 'undefined') {\n /*\n * Reactive on both storage AND theme swaps: reading\n * `manager.themes` (a shallowRef-backed getter) inside the\n * effect subscribes to `setThemes()`. Reading `storage.value`\n * subscribes to palette changes. Either trigger re-renders the\n * `<style>` block.\n */\n watchEffect(() => {\n applyColorPaletteCss(renderConcatenated(storage.value), undefined, resolveNonce());\n });\n }\n\n return {\n current: computed(() => storage.value) as ComputedRef<T>,\n set(palette) {\n storage.value = palette;\n },\n extend(partial) {\n storage.value = extend(storage.value, partial);\n },\n };\n}\n\n/**\n * Theme-aware reactive palette state with localStorage persistence\n * (plan 021 slice 2).\n *\n * Wrapped with `createSharedComposable` so every call site shares the\n * same ref + watcher. For SSR-aware cookie-backed storage (Nuxt), the\n * matching Nuxt module ships its own composable that calls\n * `bindColorPalette()` directly with a cookie-backed ref.\n */\nexport const useColorPalette = createSharedComposable(useColorPaletteUnshared);\n","import type { ThemeRuntimeEntry } from './types';\n\n/*\n * SSR capture utilities (plan 021 slice 3).\n *\n * On the client, `bindColorMode` and `useColorPalette` walk installed\n * themes and dispatch through their hooks against the live `document`.\n * On the server there is no `document` — but Nuxt SSR plugins still\n * need to flow the same theme-specific markers (`data-bs-theme` /\n * `data-theme` for color mode, `<style id=\"vc-color-palette\">` for\n * palette) into the rendered head before first paint.\n *\n * The pattern: run themes' `colorMode.handle` against a synthetic\n * Document-like object that captures attribute mutations into a plain\n * record. The Nuxt plugin then plumbs the captured record into\n * `useHead({ htmlAttrs })`.\n *\n * Themes that need DOM operations beyond `setAttribute` /\n * `removeAttribute` / `classList` (e.g. inserting child nodes) are not\n * SSR-friendly via this helper; their CSR-only logic should guard with\n * `if (typeof window === 'undefined') return;`.\n */\n\n/**\n * Build a synthetic Document-like whose `documentElement` records\n * `setAttribute` / `removeAttribute` calls into the supplied target\n * record, plus a no-op `classList`. **No other Document or Element\n * APIs are stubbed** — themes that call `doc.createElement`,\n * `doc.head.appendChild`, etc. WILL throw at SSR runtime. Themes\n * needing richer DOM access from `colorMode.handle` should guard their\n * CSR-only logic with `if (typeof window === 'undefined') return;`\n * and split the SSR-flowing bits into declarative `setAttribute` calls.\n *\n * Exposed for advanced consumers; the higher-level\n * `captureColorModeAttrs()` covers the common case and catches errors\n * per theme so a single broken theme can't crash SSR.\n */\nexport function createCaptureDocument(target: Record<string, string>): Document {\n const noopClassList = {\n add() {},\n remove() {},\n toggle(): boolean {\n return false;\n },\n contains(): boolean {\n return false;\n },\n replace() {},\n };\n\n const documentElement = {\n setAttribute(name: string, value: string): void {\n target[name] = value;\n },\n removeAttribute(name: string): void {\n delete target[name];\n },\n classList: noopClassList,\n };\n\n return { documentElement } as unknown as Document;\n}\n\n/**\n * Walk every installed theme's `colorMode.handle` against a synthetic\n * Document and capture the resulting attribute mutations. SSR plugins\n * use this to flow `data-bs-theme` / `data-theme` (or any other\n * attribute a theme declares) into `useHead({ htmlAttrs })` before\n * first paint.\n *\n * Each theme's handler runs in install order. If multiple themes set\n * the same attribute, the last one wins — same semantic as the live\n * `document.documentElement.setAttribute` chain on the client.\n *\n * Errors thrown by a theme's handler are caught + logged as a warning\n * so one malformed theme can't crash SSR; other themes still run.\n */\nexport function captureColorModeAttrs(\n themes: readonly ThemeRuntimeEntry[],\n mode: 'light' | 'dark',\n): Record<string, string> {\n /*\n * Use a prototype-free record: attribute names come from theme\n * code (potentially third-party), and a key like `__proto__` could\n * theoretically poison the result if it later gets merged into\n * another object via `Object.assign` (which uses `[[Set]]` and\n * triggers the prototype setter). `Object.create(null)` avoids the\n * concern entirely without any functional downside.\n */\n const attrs: Record<string, string> = Object.create(null) as Record<string, string>;\n const fakeDoc = createCaptureDocument(attrs);\n\n for (const theme of themes) {\n if (!theme.colorMode?.handle) {\n continue;\n }\n\n const handle = theme.colorMode?.handle.bind(theme.colorMode);\n\n try {\n handle(fakeDoc, mode);\n } catch (e) {\n if (typeof console !== 'undefined') {\n // eslint-disable-next-line no-console\n console.warn('[vuecs] theme colorMode.handle failed; skipping:', e);\n }\n }\n }\n\n return attrs;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function isObject(input: unknown): input is Record<string, unknown>;
2
+ //# sourceMappingURL=object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../../src/utils/object.ts"],"names":[],"mappings":"AAQA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzE"}
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@vuecs/design",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Design tokens (CSS variables) and runtime palette switcher for vuecs components.",
6
+ "exports": {
7
+ "./package.json": "./package.json",
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "style": "./assets/index.css",
11
+ "import": "./dist/index.mjs"
12
+ },
13
+ "./index.css": "./assets/index.css",
14
+ "./assets/index.css": "./assets/index.css",
15
+ "./animations.css": "./assets/animations.css",
16
+ "./assets/animations.css": "./assets/animations.css",
17
+ "./palettes.css": "./assets/palettes.css",
18
+ "./assets/palettes.css": "./assets/palettes.css",
19
+ "./standalone": {
20
+ "style": "./assets/standalone.css",
21
+ "default": "./assets/standalone.css"
22
+ },
23
+ "./standalone.css": "./assets/standalone.css",
24
+ "./assets/standalone.css": "./assets/standalone.css"
25
+ },
26
+ "style": "./assets/index.css",
27
+ "files": [
28
+ "dist",
29
+ "assets"
30
+ ],
31
+ "keywords": [
32
+ "vuecs",
33
+ "design",
34
+ "design-system",
35
+ "design-tokens",
36
+ "css-variables",
37
+ "theme",
38
+ "tailwind",
39
+ "palette"
40
+ ],
41
+ "author": {
42
+ "name": "Peter Placzek",
43
+ "email": "contact@tada5hi.net",
44
+ "url": "https://tada5hi.net"
45
+ },
46
+ "license": "Apache-2.0",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/tada5hi/vuecs.git",
50
+ "directory": "packages/design"
51
+ },
52
+ "sideEffects": [
53
+ "./assets/*.css",
54
+ "./dist/*.css"
55
+ ],
56
+ "scripts": {
57
+ "build:js": "tsdown",
58
+ "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
59
+ "build": "rimraf dist && npm run build:js && npm run build:types",
60
+ "standalone:build": "tsx scripts/build-standalone.ts",
61
+ "standalone:check": "tsx scripts/build-standalone.ts --check",
62
+ "test": "vitest --config test/vitest.config.ts --run",
63
+ "test:coverage": "vitest --config test/vitest.config.ts --run --coverage"
64
+ },
65
+ "devDependencies": {
66
+ "@vuecs/core": "^3.0.0",
67
+ "@vueuse/core": "^14.3.0",
68
+ "jsdom": "^29.1.1",
69
+ "tailwindcss": "^4.0.0",
70
+ "tsx": "^4.22.0",
71
+ "vue": "^3.5.34"
72
+ },
73
+ "peerDependencies": {
74
+ "@vueuse/core": "^13.0.0 || ^14.0.0",
75
+ "vue": "^3.x"
76
+ },
77
+ "engines": {
78
+ "node": ">=22.0.0"
79
+ },
80
+ "publishConfig": {
81
+ "access": "public"
82
+ }
83
+ }