@ggui-ai/project-config 0.14.0 → 0.16.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.
package/dist/node.d.ts CHANGED
@@ -121,6 +121,6 @@ export { discoverLocalUis, discoverFromGguiJsonPath, type DiscoveredUi, type Dis
121
121
  export { discoverPrimitives, discoverPrimitivesFromGguiJsonPath, type DiscoveredPrimitiveCatalog, type PrimitiveDiscoveryIssue, type PrimitiveDiscoveryResult, type DiscoverPrimitivesOptions, } from './primitive-discovery.js';
122
122
  export { discoverMcpMounts, GGUI_MCP_MOUNT_FACTORY_EXPORT, type DiscoveredMcpMount, type McpMountDiscoveryIssue, type McpMountDiscoveryResult, type DiscoverMcpMountsOptions, } from './mcp-mount-discovery.js';
123
123
  export { GGUI_PRIMITIVES_JSON_FILENAME, PrimitivesManifestV1, parsePrimitivesManifest, safeParsePrimitivesManifest, type PrimitivesManifest, } from './primitives-manifest.js';
124
- export { ThemeDocumentV1, parseThemeDocument, safeParseThemeDocument, type ThemeDocument, } from './theme.js';
124
+ export { ThemeDocumentV2, normalizeThemeDocument, parseThemeDocument, safeParseThemeDocument, type ThemeDocument, } from './theme.js';
125
125
  export { loadTheme, loadThemeFromGguiJsonPath, type LoadedTheme, type LoadThemeOptions, type LoadThemeResult, type ThemeLoadIssue, } from './theme-loader.js';
126
126
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
package/dist/node.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * upward from `process.cwd()` to find the project-root file.
16
16
  * - Any hosting integration that reads the open manifest to
17
17
  * reconstitute an agent. Hosting layers treat `ggui.json` as
18
- * input-only; per-tenant overlay config lives outside this package.
18
+ * input-only; per-app overlay config lives outside this package.
19
19
  */
20
20
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
21
21
  import { dirname, join, resolve } from 'node:path';
@@ -215,9 +215,9 @@ export { discoverMcpMounts, GGUI_MCP_MOUNT_FACTORY_EXPORT, } from './mcp-mount-d
215
215
  export { GGUI_PRIMITIVES_JSON_FILENAME, PrimitivesManifestV1, parsePrimitivesManifest, safeParsePrimitivesManifest, } from './primitives-manifest.js';
216
216
  // Browser-safe theme document schema + parsers — re-exported on the
217
217
  // Node subpath so `ggui serve` / `ggui dev` get the full theme
218
- // surface from one import path. See `./theme.js` for the v1
218
+ // surface from one import path. See `./theme.js` for the v2
219
219
  // plain-DTCG contract.
220
- export { ThemeDocumentV1, parseThemeDocument, safeParseThemeDocument, } from './theme.js';
220
+ export { ThemeDocumentV2, normalizeThemeDocument, parseThemeDocument, safeParseThemeDocument, } from './theme.js';
221
221
  // Filesystem-backed theme loader — consumes `ggui.json#theme`,
222
222
  // resolves it against the project root, parses + validates, and
223
223
  // returns a `LoadedTheme` ready for the OSS `ggui serve` server.
package/dist/schema.d.ts CHANGED
@@ -156,7 +156,7 @@ declare const GenerationSchema: z.ZodObject<{
156
156
  model: "claude-fable-5-1" | "claude-fable-5" | "claude-opus-5" | "claude-sonnet-5" | "claude-haiku-4-5-20251001" | "claude-opus-4-8" | "claude-sonnet-4-6" | "claude-opus-4-7" | "claude-opus-4-6";
157
157
  } | {
158
158
  provider: "openai";
159
- model: "gpt-5.6" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna" | "gpt-5.5" | "gpt-5.5-2026-04-23" | "gpt-5.5-pro" | "gpt-5.5-pro-2026-04-23" | "gpt-5.4" | "gpt-5.4-2026-03-05" | "gpt-5.4-mini" | "gpt-5.4-mini-2026-03-17" | "gpt-5.4-nano" | "gpt-5.4-nano-2026-03-17" | "gpt-5.3-codex";
159
+ model: "gpt-6-astra" | "gpt-5.6" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna" | "gpt-5.5" | "gpt-5.5-2026-04-23" | "gpt-5.5-pro" | "gpt-5.5-pro-2026-04-23" | "gpt-5.4" | "gpt-5.4-2026-03-05" | "gpt-5.4-mini" | "gpt-5.4-mini-2026-03-17" | "gpt-5.4-nano" | "gpt-5.4-nano-2026-03-17" | "gpt-5.3-codex";
160
160
  } | {
161
161
  provider: "google";
162
162
  model: "gemini-3.7-flash" | "gemini-3.6-flash" | "gemini-3.5-flash" | "gemini-3.5-flash-lite" | "gemini-flash-latest" | "gemini-3.1-flash-lite" | "gemini-3.1-pro-preview" | "gemini-3.1-flash-lite-preview" | "gemini-3-flash-preview";
@@ -265,12 +265,12 @@ export declare const GguiJsonV1: z.ZodObject<{
265
265
  bundleHost: z.ZodOptional<z.ZodString>;
266
266
  bundleSri: z.ZodOptional<z.ZodString>;
267
267
  styleUrl: z.ZodOptional<z.ZodURL>;
268
- connect: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodURL>>>;
269
- requires: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
268
+ connect: z.ZodOptional<z.ZodArray<z.ZodURL>>;
269
+ requires: z.ZodOptional<z.ZodArray<z.ZodString>>;
270
270
  typesUrl: z.ZodOptional<z.ZodURL>;
271
271
  typesSri: z.ZodOptional<z.ZodString>;
272
272
  }, z.core.$strict>>>;
273
- publicEnv: z.ZodOptional<z.ZodReadonly<z.ZodRecord<z.ZodString, z.ZodString>>>;
273
+ publicEnv: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
274
274
  defaultDisplayMode: z.ZodOptional<z.ZodEnum<{
275
275
  inline: "inline";
276
276
  fullscreen: "fullscreen";
@@ -333,7 +333,7 @@ export declare const GguiJsonV1: z.ZodObject<{
333
333
  model: "claude-fable-5-1" | "claude-fable-5" | "claude-opus-5" | "claude-sonnet-5" | "claude-haiku-4-5-20251001" | "claude-opus-4-8" | "claude-sonnet-4-6" | "claude-opus-4-7" | "claude-opus-4-6";
334
334
  } | {
335
335
  provider: "openai";
336
- model: "gpt-5.6" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna" | "gpt-5.5" | "gpt-5.5-2026-04-23" | "gpt-5.5-pro" | "gpt-5.5-pro-2026-04-23" | "gpt-5.4" | "gpt-5.4-2026-03-05" | "gpt-5.4-mini" | "gpt-5.4-mini-2026-03-17" | "gpt-5.4-nano" | "gpt-5.4-nano-2026-03-17" | "gpt-5.3-codex";
336
+ model: "gpt-6-astra" | "gpt-5.6" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna" | "gpt-5.5" | "gpt-5.5-2026-04-23" | "gpt-5.5-pro" | "gpt-5.5-pro-2026-04-23" | "gpt-5.4" | "gpt-5.4-2026-03-05" | "gpt-5.4-mini" | "gpt-5.4-mini-2026-03-17" | "gpt-5.4-nano" | "gpt-5.4-nano-2026-03-17" | "gpt-5.3-codex";
337
337
  } | {
338
338
  provider: "google";
339
339
  model: "gemini-3.7-flash" | "gemini-3.6-flash" | "gemini-3.5-flash" | "gemini-3.5-flash-lite" | "gemini-flash-latest" | "gemini-3.1-flash-lite" | "gemini-3.1-pro-preview" | "gemini-3.1-flash-lite-preview" | "gemini-3-flash-preview";
@@ -1,33 +1,34 @@
1
- import type { DtcgTheme, ThemeMode } from '@ggui-ai/design/themes';
1
+ import type { DtcgTheme, ThemeMode, ThemeVariableMap } from '@ggui-ai/design/themes';
2
2
  import type { GguiJsonV1, ThemeConfig } from './schema.js';
3
- import { type ThemeDocument } from './theme.js';
3
+ /**
4
+ * Both modes' projections of a loaded theme — the `overlays` an app
5
+ * theme carries on the wire (ggui#987 §3), produced by the ONE producer.
6
+ */
7
+ export interface ThemeOverlays {
8
+ readonly light: ThemeVariableMap;
9
+ readonly dark: ThemeVariableMap;
10
+ }
4
11
  /**
5
12
  * One loaded theme — discriminated on `source`. Every variant carries
6
- * the resolved `mode`, the parsed token document, and the pre-rendered
7
- * `--ggui-*` CSS variable block.
13
+ * the resolved `mode` (the default the card paints when no host
14
+ * announces one), the producer-ready document, and both modes'
15
+ * projections (`overlays`).
8
16
  *
9
- * **`document` is shape-discriminated by `source`** (no casts, two
10
- * structurally-similar but separately-validated underlying types):
17
+ * **`document` is the producer's `DtcgTheme` on every variant**: the
18
+ * registry's own document for `default` / `preset`, and for `file` the
19
+ * strict-Zod `ThemeDocument` after `normalizeThemeDocument` (the open
20
+ * `ggui.json#theme` format keeps `motion`/`accessibility`/`zIndex`
21
+ * optional so external tools that emit only a subset still parse; the
22
+ * normaliser fills them from the shipped default).
11
23
  *
12
- * - `default` / `preset` → `DtcgTheme` (the canonical theme shape
13
- * consumed by every registry-side theme — produces full
14
- * `--ggui-*` CSS through `parseTheme`; every group required).
15
- * - `file` → `ThemeDocument` (the strict-Zod schema for the open
16
- * `ggui.json#theme` file format — same `color`/`font`/`spacing`/
17
- * `shape`/`motion`/`accessibility`/`zIndex` vocabulary
18
- * as `DtcgTheme`, but `motion`/`accessibility`/`zIndex`
19
- * are optional so external tools that emit only a subset still
20
- * parse).
21
- *
22
- * Consumers narrow on `source` if they ever need the shape. CSS-variable
23
- * emission happens upstream into `cssVariables`, so most downstream code
24
- * only reads that string.
24
+ * Consumers narrow on `source` only for provenance. The projection
25
+ * happens here, once, into `overlays`; downstream code reads the maps.
25
26
  */
26
27
  export type LoadedTheme = {
27
28
  readonly source: 'default';
28
29
  readonly mode: ThemeMode;
29
30
  readonly document: DtcgTheme;
30
- readonly cssVariables: string;
31
+ readonly overlays: ThemeOverlays;
31
32
  } | {
32
33
  readonly source: 'preset';
33
34
  /** Registry id (`'claudic'`, `'ggui'`, …). */
@@ -36,14 +37,15 @@ export type LoadedTheme = {
36
37
  /** Flat dot-path token overrides applied on top of the preset. */
37
38
  readonly overrides?: Record<string, string>;
38
39
  readonly document: DtcgTheme;
39
- readonly cssVariables: string;
40
+ readonly overlays: ThemeOverlays;
40
41
  } | {
41
42
  readonly source: 'file';
42
43
  /** Absolute filesystem path of the loaded theme file. */
43
44
  readonly path: string;
44
45
  readonly mode: ThemeMode;
45
- readonly document: ThemeDocument;
46
- readonly cssVariables: string;
46
+ /** The parsed file, normalised to the producer's document (see `normalizeThemeDocument`). */
47
+ readonly document: DtcgTheme;
48
+ readonly overlays: ThemeOverlays;
47
49
  };
48
50
  /**
49
51
  * An error surfaced during theme load. Shape matches the issue
@@ -1 +1 @@
1
- {"version":3,"file":"theme-loader.d.ts","sourceRoot":"","sources":["../src/theme-loader.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAwCpE;AAyMD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,UAAU,GACnB,eAAe,CAGjB;AAED;;;;;GAKG;AACH,YAAY,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"theme-loader.d.ts","sourceRoot":"","sources":["../src/theme-loader.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG3D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC;AAUD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAwCpE;AA0MD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,UAAU,GACnB,eAAe,CAGjB;AAED;;;;;GAKG;AACH,YAAY,EAAE,WAAW,EAAE,CAAC"}
@@ -14,16 +14,15 @@
14
14
  * exception. `ggui serve` escalates issues to a fatal exit before
15
15
  * binding a port.
16
16
  *
17
- * ## cssVariables pre-rendering
17
+ * ## Both modes, projected
18
18
  *
19
- * The loader pre-renders the `:root { --ggui-*: value; }` CSS block
20
- * via `@ggui-ai/design/themes`: `parseTheme(id, DtcgTheme)` for the
21
- * preset / default paths (full DTCG with motion tokens) and
22
- * `generateCssVariables(ThemeDocument)` for the file path (the
23
- * duck-typed walker accepts the plain DTCG document without requiring
24
- * the strict {@link DtcgTheme} shape). Downstream consumers (console,
25
- * render endpoint, MCP apps iframe) inject the pre-rendered string
26
- * without re-walking the token tree.
19
+ * Every `LoadedTheme` carries `overlays` — the `--ggui-*` variable map
20
+ * for BOTH modes, produced by `@ggui-ai/design/themes`'
21
+ * `deriveThemeVariables(document, mode)` (the one producer, ggui#987):
22
+ * a preset's light document and its dark twin, or a file's single
23
+ * document projected for each mode. Downstream consumers (the CLI's
24
+ * deploy PATCH, the console picker) ship the projection as the app's
25
+ * theme; nothing re-walks the token tree.
27
26
  *
28
27
  * ## Mode handling
29
28
  *
@@ -35,8 +34,15 @@
35
34
  */
36
35
  import { readFileSync, existsSync } from 'node:fs';
37
36
  import { dirname, isAbsolute, resolve } from 'node:path';
38
- import { generateCssVariables, getRawTheme, getTheme, lightTheme, parseTheme, } from '@ggui-ai/design/themes';
39
- import { parseThemeDocument } from './theme.js';
37
+ import { ThemeDocumentInvalidError, darkTheme, deriveThemeVariables, getRawTheme, lightTheme, } from '@ggui-ai/design/themes';
38
+ import { normalizeThemeDocument, parseThemeDocument } from './theme.js';
39
+ /** Project a light document (and its dark twin when it has one) for both modes. */
40
+ function deriveOverlays(light, dark) {
41
+ return {
42
+ light: deriveThemeVariables(light, 'light'),
43
+ dark: deriveThemeVariables(dark ?? light, 'dark'),
44
+ };
45
+ }
40
46
  /**
41
47
  * Load the theme declared in `ggui.json#theme`, or fall back to the
42
48
  * shipped default when the field is absent.
@@ -59,7 +65,7 @@ export function loadTheme(options) {
59
65
  source: 'default',
60
66
  mode: 'light',
61
67
  document: lightTheme,
62
- cssVariables: parseTheme('default', lightTheme).cssVariables,
68
+ overlays: deriveOverlays(lightTheme, darkTheme),
63
69
  },
64
70
  };
65
71
  }
@@ -86,8 +92,8 @@ export function loadTheme(options) {
86
92
  * manifest parse.
87
93
  */
88
94
  function loadPreset(preset, mode, overrides) {
89
- // Resolve the raw token tree first so we can layer overrides
90
- // before parseTheme walks the leaves.
95
+ // Resolve the raw token tree first (also the registration check)
96
+ // before layering overrides and projecting both modes.
91
97
  const raw = getRawTheme(preset, mode);
92
98
  if (!raw) {
93
99
  return {
@@ -100,45 +106,25 @@ function loadPreset(preset, mode, overrides) {
100
106
  },
101
107
  };
102
108
  }
103
- // No overrides: hand off to the registry's parser directly so we
104
- // share its cache.
105
- if (!overrides || Object.keys(overrides).length === 0) {
106
- const parsed = getTheme(preset, mode);
107
- if (!parsed) {
108
- // Should not happen — getRawTheme succeeded above. Defensive.
109
- return {
110
- ok: false,
111
- issue: {
112
- path: preset,
113
- message: `Theme preset "${preset}" parsed unexpectedly empty.`,
114
- },
115
- };
116
- }
117
- return {
118
- ok: true,
119
- theme: {
120
- source: 'preset',
121
- preset,
122
- mode,
123
- document: raw,
124
- cssVariables: parsed.cssVariables,
125
- },
126
- };
127
- }
128
- // With overrides: clone the raw tree, deep-set each leaf's $value,
129
- // then re-parse. We bypass the registry's cache because the
130
- // override set is per-call and would pollute it.
131
- const mutated = applyOverrides(raw, overrides);
132
- const parsed = parseTheme(preset, mutated);
109
+ // Both modes ride the loaded theme (ggui#987 §3): the preset's light
110
+ // document and its dark twin (a single-mode preset projects the one
111
+ // document for both), overrides applied to each before projection.
112
+ // Overrides clone the raw trees so the registry's cache stays clean.
113
+ const rawLight = getRawTheme(preset, 'light') ?? raw;
114
+ const rawDark = getRawTheme(preset, 'dark');
115
+ const hasOverrides = overrides !== undefined && Object.keys(overrides).length > 0;
116
+ const light = hasOverrides ? applyOverrides(rawLight, overrides) : rawLight;
117
+ const dark = rawDark === undefined ? undefined : hasOverrides ? applyOverrides(rawDark, overrides) : rawDark;
118
+ const document = mode === 'dark' ? (dark ?? light) : light;
133
119
  return {
134
120
  ok: true,
135
121
  theme: {
136
122
  source: 'preset',
137
123
  preset,
138
124
  mode,
139
- overrides,
140
- document: mutated,
141
- cssVariables: parsed.cssVariables,
125
+ ...(hasOverrides ? { overrides } : {}),
126
+ document,
127
+ overlays: deriveOverlays(light, dark),
142
128
  },
143
129
  };
144
130
  }
@@ -203,14 +189,35 @@ function loadFile(filePath, mode, projectRoot) {
203
189
  },
204
190
  };
205
191
  }
192
+ // The producer is the validator of what the schema cannot see (a
193
+ // family with no `500` anchor, an unparseable colour): its refusal is
194
+ // the load issue, named by path.
195
+ const normalized = normalizeThemeDocument(document);
196
+ let overlays;
197
+ try {
198
+ overlays = deriveOverlays(normalized, undefined);
199
+ }
200
+ catch (cause) {
201
+ if (cause instanceof ThemeDocumentInvalidError) {
202
+ return {
203
+ ok: false,
204
+ issue: {
205
+ path: absolutePath,
206
+ message: `Theme file cannot be projected: ${cause.message}`,
207
+ cause,
208
+ },
209
+ };
210
+ }
211
+ throw cause;
212
+ }
206
213
  return {
207
214
  ok: true,
208
215
  theme: {
209
216
  source: 'file',
210
217
  path: absolutePath,
211
218
  mode,
212
- document,
213
- cssVariables: generateCssVariables(document),
219
+ document: normalized,
220
+ overlays,
214
221
  },
215
222
  };
216
223
  }
package/dist/theme.d.ts CHANGED
@@ -1,82 +1,44 @@
1
1
  /**
2
- * `ggui.json#theme` → plain DTCG JSON document.
2
+ * `ggui.json#theme` → plain DTCG JSON document, v2 (ggui#987).
3
3
  *
4
4
  * The theme file is a plain JSON document matching the Design Tokens
5
- * Community Group (DTCG) spec. It is one of the open file-format
6
- * surfaces in the ggui manifest model, alongside
7
- * `ggui.primitives.json` and `ggui.ui.json`.
5
+ * Community Group (DTCG) spec, in the same vocabulary as
6
+ * `@ggui-ai/design`'s canonical {@link DtcgTheme}: the curated registry
7
+ * themes and an authored `theme.json` are the same shape.
8
8
  *
9
- * **Shape parity with the internal `DtcgTheme`.** This v1 schema is
10
- * deliberately the same shape as `@ggui-ai/design`'s canonical
11
- * {@link DtcgTheme} — `color`/`font`/`spacing`/`shape`/`motion`/
12
- * `accessibility`/`zIndex` — so an authored `theme.json`
13
- * uses the same vocabulary the curated registry themes (light/dark/
14
- * premium-*) use. Previously this schema was Tailwind-style flat
15
- * (`typography`/`radius`/`shadow` at root); the v1 schema now mirrors
16
- * the internal shape to remove the divergence.
9
+ * **What is authored, what is derived.** An author states the six
10
+ * layering roles (`ground`/`onGround`, `container`/`onContainer`,
11
+ * `sunken`/`onSunken`), one `500` anchor per family (`primary`,
12
+ * `success`, `warning`, `error`, `info`), the font families and
13
+ * weights, spacing, radii, shadows. Everything else — the ten-stop
14
+ * ramps, every `on*` ink and container, the neutral ladder, outlines,
15
+ * `elevated`, the type scale from the one `font.ramp` — is DERIVED by
16
+ * the one producer (`deriveThemeVariables`), so the retired ladders
17
+ * (`font.size`, `font.lineHeight`, `motion.duration`, `motion.easing`)
18
+ * and `$metadata.fontUrl` are refused, not ignored. Fonts are declared
19
+ * as `typography.faces` (https `src` only); the card installs them.
17
20
  *
18
- * **External-tool compatibility.** External tooling (Figma Tokens,
19
- * Style Dictionary, Tokens Studio) commonly emits only a subset of
20
- * the canonical shape. Only the original required groups (`color`,
21
- * `font`, `spacing`, `shape`) stay required; `motion`,
22
- * `accessibility`, `zIndex`, and the new DTCG metadata fields
23
- * (`$name`, `$description`, `$metadata`) are all OPTIONAL so tools
24
- * that only emit colors + dimensions still parse cleanly.
25
- *
26
- * **BREAKING for old `theme.json` files.** Authors who hand-wrote a
27
- * `theme.json` against the pre-rc schema must rename:
28
- *
29
- * - `typography.fontFamily` → `font.family`
30
- * - `typography.fontSize` → `font.size`
31
- * - `typography.fontWeight` → `font.weight`
32
- * - `typography.lineHeight` → `font.lineHeight`
33
- * - top-level `radius` → `shape.radius`
34
- * - top-level `shadow` → `shape.shadow`
35
- * - top-level `duration` → `motion.duration`
36
- * - top-level `transition` → `motion.transition`
37
- *
38
- * Plus: under `font.family`, `sans` is the only required sub-token
39
- * (was previously freeform); other family slots (`mono`, …) are
40
- * optional. The font-family record itself stays open so additional
41
- * named families parse without a schema change.
21
+ * **External-tool leniency.** `motion`, `accessibility`, `zIndex` and
22
+ * the DTCG metadata fields stay OPTIONAL; {@link normalizeThemeDocument}
23
+ * fills them from the shipped default and flattens the DTCG object
24
+ * forms (structured shadows, transitions, array font stacks) to the CSS
25
+ * strings the producer reads.
42
26
  *
43
27
  * **Ownership boundary:**
44
28
  *
45
29
  * - *Where* the theme lives → `ggui.json#theme` (pointer string).
46
30
  * - *What* the theme file looks like → this module's schema.
47
- * - *How* DTCG tokens become CSS variables → `@ggui-ai/design`'s
48
- * `generateCssVariables` (stays over there; this schema does not
49
- * emit). The duck-typed walker accepts any DTCG-shaped tree, so
50
- * the new nested groups (`font.size`, `shape.radius`, …) walk
51
- * into `--ggui-font-size-md`/`--ggui-shape-radius-md` CSS vars
52
- * without code changes.
31
+ * - *How* the document becomes CSS variables → `@ggui-ai/design`'s
32
+ * `deriveThemeVariables` (the loader calls it for both modes).
53
33
  * - *Built-in default* when `theme` is absent → `@ggui-ai/design`'s
54
- * shipped `lightTheme`.
55
- *
56
- * **Extending rules:**
57
- *
58
- * 1. Additive only within `schema: '1'`. New optional fields must
59
- * default to no-op behaviour so older tooling ignores them.
60
- * 2. Framework-neutral + host-neutral. No vendor names in enum
61
- * values, no build-pipeline fields.
62
- * 3. Root and per-group objects are strict — unknown keys fail
63
- * parse. Same discipline as `ggui.json`, `ggui.ui.json`, and
64
- * `ggui.primitives.json`.
34
+ * shipped `lightTheme` / `darkTheme`.
65
35
  *
66
- * **Strictness tradeoff:**
67
- *
68
- * Individual DTCG token leaves are validated with a discriminated
69
- * union over `$type` (color / dimension / fontFamily / fontWeight /
70
- * shadow / duration / cubicBezier / transition / number). Each
71
- * variant validates its `$value` shape. `$description` is accepted
72
- * everywhere as a free-form docstring. Token leaves under newer
73
- * groups (`accessibility`, `zIndex`) accept a slightly
74
- * wider token vocabulary because external tools sometimes encode
75
- * scalar/array values with permissive `$type` strings (`string`,
76
- * `array`, …); the schema accepts those without enumerating every
77
- * possible spelling.
36
+ * **Extending rules:** additive only within `schema: '1'`; framework-
37
+ * and host-neutral; root and per-group objects are strict — unknown
38
+ * keys fail parse, the same discipline as `ggui.json`.
78
39
  */
79
40
  import { z } from 'zod';
41
+ import type { DtcgTheme } from '@ggui-ai/design/themes';
80
42
  /**
81
43
  * Plain DTCG theme document v1. Required groups (`color`, `font`,
82
44
  * `spacing`, `shape`) map 1:1 to `@ggui-ai/design`'s
@@ -96,25 +58,188 @@ import { z } from 'zod';
96
58
  * allow-listed (DTCG metadata); anything else is a typo. Additive
97
59
  * slices add new fields here under `schema: '1'`.
98
60
  */
99
- export declare const ThemeDocumentV1: z.ZodObject<{
61
+ export declare const ThemeDocumentV2: z.ZodObject<{
100
62
  $schema: z.ZodOptional<z.ZodString>;
101
63
  $version: z.ZodOptional<z.ZodString>;
102
64
  $name: z.ZodOptional<z.ZodString>;
103
65
  $description: z.ZodOptional<z.ZodString>;
104
66
  $metadata: z.ZodOptional<z.ZodObject<{
105
67
  font: z.ZodOptional<z.ZodString>;
106
- fontUrl: z.ZodOptional<z.ZodString>;
107
68
  philosophy: z.ZodOptional<z.ZodString>;
69
+ frameless: z.ZodOptional<z.ZodBoolean>;
108
70
  }, z.core.$strict>>;
109
- color: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
110
- $type: z.ZodLiteral<"color">;
111
- $value: z.ZodString;
112
- $description: z.ZodOptional<z.ZodString>;
113
- }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodObject<{
114
- $type: z.ZodLiteral<"color">;
115
- $value: z.ZodString;
116
- $description: z.ZodOptional<z.ZodString>;
117
- }, z.core.$strict>>]>>;
71
+ color: z.ZodObject<{
72
+ primary: z.ZodRecord<z.ZodString, z.ZodObject<{
73
+ $type: z.ZodLiteral<"color">;
74
+ $value: z.ZodString;
75
+ $description: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strict>>;
77
+ neutral: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
78
+ $type: z.ZodLiteral<"color">;
79
+ $value: z.ZodString;
80
+ $description: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strict>>>;
82
+ success: z.ZodRecord<z.ZodString, z.ZodObject<{
83
+ $type: z.ZodLiteral<"color">;
84
+ $value: z.ZodString;
85
+ $description: z.ZodOptional<z.ZodString>;
86
+ }, z.core.$strict>>;
87
+ warning: z.ZodRecord<z.ZodString, z.ZodObject<{
88
+ $type: z.ZodLiteral<"color">;
89
+ $value: z.ZodString;
90
+ $description: z.ZodOptional<z.ZodString>;
91
+ }, z.core.$strict>>;
92
+ error: z.ZodRecord<z.ZodString, z.ZodObject<{
93
+ $type: z.ZodLiteral<"color">;
94
+ $value: z.ZodString;
95
+ $description: z.ZodOptional<z.ZodString>;
96
+ }, z.core.$strict>>;
97
+ info: z.ZodRecord<z.ZodString, z.ZodObject<{
98
+ $type: z.ZodLiteral<"color">;
99
+ $value: z.ZodString;
100
+ $description: z.ZodOptional<z.ZodString>;
101
+ }, z.core.$strict>>;
102
+ ground: z.ZodObject<{
103
+ $type: z.ZodLiteral<"color">;
104
+ $value: z.ZodString;
105
+ $description: z.ZodOptional<z.ZodString>;
106
+ }, z.core.$strict>;
107
+ onGround: z.ZodObject<{
108
+ $type: z.ZodLiteral<"color">;
109
+ $value: z.ZodString;
110
+ $description: z.ZodOptional<z.ZodString>;
111
+ }, z.core.$strict>;
112
+ container: z.ZodObject<{
113
+ $type: z.ZodLiteral<"color">;
114
+ $value: z.ZodString;
115
+ $description: z.ZodOptional<z.ZodString>;
116
+ }, z.core.$strict>;
117
+ onContainer: z.ZodObject<{
118
+ $type: z.ZodLiteral<"color">;
119
+ $value: z.ZodString;
120
+ $description: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strict>;
122
+ sunken: z.ZodObject<{
123
+ $type: z.ZodLiteral<"color">;
124
+ $value: z.ZodString;
125
+ $description: z.ZodOptional<z.ZodString>;
126
+ }, z.core.$strict>;
127
+ onSunken: z.ZodObject<{
128
+ $type: z.ZodLiteral<"color">;
129
+ $value: z.ZodString;
130
+ $description: z.ZodOptional<z.ZodString>;
131
+ }, z.core.$strict>;
132
+ link: z.ZodOptional<z.ZodObject<{
133
+ $type: z.ZodLiteral<"color">;
134
+ $value: z.ZodString;
135
+ $description: z.ZodOptional<z.ZodString>;
136
+ }, z.core.$strict>>;
137
+ outline: z.ZodOptional<z.ZodObject<{
138
+ $type: z.ZodLiteral<"color">;
139
+ $value: z.ZodString;
140
+ $description: z.ZodOptional<z.ZodString>;
141
+ }, z.core.$strict>>;
142
+ outlineVariant: z.ZodOptional<z.ZodObject<{
143
+ $type: z.ZodLiteral<"color">;
144
+ $value: z.ZodString;
145
+ $description: z.ZodOptional<z.ZodString>;
146
+ }, z.core.$strict>>;
147
+ onPrimary: z.ZodOptional<z.ZodObject<{
148
+ $type: z.ZodLiteral<"color">;
149
+ $value: z.ZodString;
150
+ $description: z.ZodOptional<z.ZodString>;
151
+ }, z.core.$strict>>;
152
+ primaryContainer: z.ZodOptional<z.ZodObject<{
153
+ $type: z.ZodLiteral<"color">;
154
+ $value: z.ZodString;
155
+ $description: z.ZodOptional<z.ZodString>;
156
+ }, z.core.$strict>>;
157
+ onPrimaryContainer: z.ZodOptional<z.ZodObject<{
158
+ $type: z.ZodLiteral<"color">;
159
+ $value: z.ZodString;
160
+ $description: z.ZodOptional<z.ZodString>;
161
+ }, z.core.$strict>>;
162
+ onSuccess: z.ZodOptional<z.ZodObject<{
163
+ $type: z.ZodLiteral<"color">;
164
+ $value: z.ZodString;
165
+ $description: z.ZodOptional<z.ZodString>;
166
+ }, z.core.$strict>>;
167
+ successContainer: z.ZodOptional<z.ZodObject<{
168
+ $type: z.ZodLiteral<"color">;
169
+ $value: z.ZodString;
170
+ $description: z.ZodOptional<z.ZodString>;
171
+ }, z.core.$strict>>;
172
+ onSuccessContainer: z.ZodOptional<z.ZodObject<{
173
+ $type: z.ZodLiteral<"color">;
174
+ $value: z.ZodString;
175
+ $description: z.ZodOptional<z.ZodString>;
176
+ }, z.core.$strict>>;
177
+ onWarning: z.ZodOptional<z.ZodObject<{
178
+ $type: z.ZodLiteral<"color">;
179
+ $value: z.ZodString;
180
+ $description: z.ZodOptional<z.ZodString>;
181
+ }, z.core.$strict>>;
182
+ warningContainer: z.ZodOptional<z.ZodObject<{
183
+ $type: z.ZodLiteral<"color">;
184
+ $value: z.ZodString;
185
+ $description: z.ZodOptional<z.ZodString>;
186
+ }, z.core.$strict>>;
187
+ onWarningContainer: z.ZodOptional<z.ZodObject<{
188
+ $type: z.ZodLiteral<"color">;
189
+ $value: z.ZodString;
190
+ $description: z.ZodOptional<z.ZodString>;
191
+ }, z.core.$strict>>;
192
+ onError: z.ZodOptional<z.ZodObject<{
193
+ $type: z.ZodLiteral<"color">;
194
+ $value: z.ZodString;
195
+ $description: z.ZodOptional<z.ZodString>;
196
+ }, z.core.$strict>>;
197
+ errorContainer: z.ZodOptional<z.ZodObject<{
198
+ $type: z.ZodLiteral<"color">;
199
+ $value: z.ZodString;
200
+ $description: z.ZodOptional<z.ZodString>;
201
+ }, z.core.$strict>>;
202
+ onErrorContainer: z.ZodOptional<z.ZodObject<{
203
+ $type: z.ZodLiteral<"color">;
204
+ $value: z.ZodString;
205
+ $description: z.ZodOptional<z.ZodString>;
206
+ }, z.core.$strict>>;
207
+ onInfo: z.ZodOptional<z.ZodObject<{
208
+ $type: z.ZodLiteral<"color">;
209
+ $value: z.ZodString;
210
+ $description: z.ZodOptional<z.ZodString>;
211
+ }, z.core.$strict>>;
212
+ infoContainer: z.ZodOptional<z.ZodObject<{
213
+ $type: z.ZodLiteral<"color">;
214
+ $value: z.ZodString;
215
+ $description: z.ZodOptional<z.ZodString>;
216
+ }, z.core.$strict>>;
217
+ onInfoContainer: z.ZodOptional<z.ZodObject<{
218
+ $type: z.ZodLiteral<"color">;
219
+ $value: z.ZodString;
220
+ $description: z.ZodOptional<z.ZodString>;
221
+ }, z.core.$strict>>;
222
+ tertiary: z.ZodOptional<z.ZodObject<{
223
+ $type: z.ZodLiteral<"color">;
224
+ $value: z.ZodString;
225
+ $description: z.ZodOptional<z.ZodString>;
226
+ }, z.core.$strict>>;
227
+ onTertiary: z.ZodOptional<z.ZodObject<{
228
+ $type: z.ZodLiteral<"color">;
229
+ $value: z.ZodString;
230
+ $description: z.ZodOptional<z.ZodString>;
231
+ }, z.core.$strict>>;
232
+ tertiaryContainer: z.ZodOptional<z.ZodObject<{
233
+ $type: z.ZodLiteral<"color">;
234
+ $value: z.ZodString;
235
+ $description: z.ZodOptional<z.ZodString>;
236
+ }, z.core.$strict>>;
237
+ onTertiaryContainer: z.ZodOptional<z.ZodObject<{
238
+ $type: z.ZodLiteral<"color">;
239
+ $value: z.ZodString;
240
+ $description: z.ZodOptional<z.ZodString>;
241
+ }, z.core.$strict>>;
242
+ }, z.core.$strict>;
118
243
  spacing: z.ZodRecord<z.ZodString, z.ZodObject<{
119
244
  $type: z.ZodLiteral<"dimension">;
120
245
  $value: z.ZodString;
@@ -132,26 +257,45 @@ export declare const ThemeDocumentV1: z.ZodObject<{
132
257
  $description: z.ZodOptional<z.ZodString>;
133
258
  }, z.core.$strict>;
134
259
  }, z.core.$strip>>;
135
- size: z.ZodRecord<z.ZodString, z.ZodObject<{
136
- $type: z.ZodLiteral<"dimension">;
137
- $value: z.ZodString;
138
- $description: z.ZodOptional<z.ZodString>;
139
- }, z.core.$strict>>;
140
260
  weight: z.ZodRecord<z.ZodString, z.ZodObject<{
141
261
  $type: z.ZodLiteral<"fontWeight">;
142
262
  $value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
143
263
  $description: z.ZodOptional<z.ZodString>;
144
264
  }, z.core.$strict>>;
145
- lineHeight: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
146
- $type: z.ZodLiteral<"number">;
147
- $value: z.ZodNumber;
148
- $description: z.ZodOptional<z.ZodString>;
149
- }, z.core.$strict>, z.ZodObject<{
150
- $type: z.ZodLiteral<"dimension">;
151
- $value: z.ZodString;
152
- $description: z.ZodOptional<z.ZodString>;
153
- }, z.core.$strict>]>>;
265
+ letterSpacing: z.ZodOptional<z.ZodObject<{
266
+ body: z.ZodOptional<z.ZodObject<{
267
+ $type: z.ZodLiteral<"dimension">;
268
+ $value: z.ZodString;
269
+ $description: z.ZodOptional<z.ZodString>;
270
+ }, z.core.$strict>>;
271
+ heading: z.ZodOptional<z.ZodObject<{
272
+ $type: z.ZodLiteral<"dimension">;
273
+ $value: z.ZodString;
274
+ $description: z.ZodOptional<z.ZodString>;
275
+ }, z.core.$strict>>;
276
+ }, z.core.$strict>>;
277
+ ramp: z.ZodOptional<z.ZodObject<{
278
+ base: z.ZodObject<{
279
+ $type: z.ZodLiteral<"dimension">;
280
+ $value: z.ZodString;
281
+ $description: z.ZodOptional<z.ZodString>;
282
+ }, z.core.$strict>;
283
+ ratio: z.ZodObject<{
284
+ $type: z.ZodLiteral<"number">;
285
+ $value: z.ZodNumber;
286
+ $description: z.ZodOptional<z.ZodString>;
287
+ }, z.core.$strict>;
288
+ }, z.core.$strict>>;
154
289
  }, z.core.$strict>;
290
+ typography: z.ZodOptional<z.ZodObject<{
291
+ faces: z.ZodOptional<z.ZodArray<z.ZodObject<{
292
+ family: z.ZodString;
293
+ src: z.ZodString;
294
+ weight: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
295
+ style: z.ZodOptional<z.ZodString>;
296
+ display: z.ZodOptional<z.ZodString>;
297
+ }, z.core.$strict>>>;
298
+ }, z.core.$strict>>;
155
299
  shape: z.ZodObject<{
156
300
  radius: z.ZodRecord<z.ZodString, z.ZodObject<{
157
301
  $type: z.ZodLiteral<"dimension">;
@@ -169,18 +313,20 @@ export declare const ThemeDocumentV1: z.ZodObject<{
169
313
  }, z.core.$strict>]>;
170
314
  $description: z.ZodOptional<z.ZodString>;
171
315
  }, z.core.$strict>>;
316
+ border: z.ZodOptional<z.ZodObject<{
317
+ width: z.ZodOptional<z.ZodObject<{
318
+ $type: z.ZodLiteral<"dimension">;
319
+ $value: z.ZodString;
320
+ $description: z.ZodOptional<z.ZodString>;
321
+ }, z.core.$strict>>;
322
+ style: z.ZodOptional<z.ZodObject<{
323
+ $type: z.ZodString;
324
+ $value: z.ZodString;
325
+ $description: z.ZodOptional<z.ZodString>;
326
+ }, z.core.$strict>>;
327
+ }, z.core.$strict>>;
172
328
  }, z.core.$strict>;
173
329
  motion: z.ZodOptional<z.ZodObject<{
174
- duration: z.ZodRecord<z.ZodString, z.ZodObject<{
175
- $type: z.ZodLiteral<"duration">;
176
- $value: z.ZodString;
177
- $description: z.ZodOptional<z.ZodString>;
178
- }, z.core.$strict>>;
179
- easing: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
180
- $type: z.ZodLiteral<"cubicBezier">;
181
- $value: z.ZodString;
182
- $description: z.ZodOptional<z.ZodString>;
183
- }, z.core.$strict>>>;
184
330
  transition: z.ZodRecord<z.ZodString, z.ZodObject<{
185
331
  $type: z.ZodLiteral<"transition">;
186
332
  $value: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
@@ -192,7 +338,7 @@ export declare const ThemeDocumentV1: z.ZodObject<{
192
338
  }, z.core.$strict>>;
193
339
  keyframes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
194
340
  $type: z.ZodString;
195
- $value: z.ZodUnknown;
341
+ $value: z.ZodString;
196
342
  $description: z.ZodOptional<z.ZodString>;
197
343
  }, z.core.$strict>>>;
198
344
  }, z.core.$strict>>;
@@ -251,9 +397,9 @@ export declare const ThemeDocumentV1: z.ZodObject<{
251
397
  }, z.core.$strict>>>;
252
398
  }, z.core.$strict>;
253
399
  /** Static TypeScript type derived from the v1 schema. */
254
- export type ThemeDocumentV1 = z.infer<typeof ThemeDocumentV1>;
400
+ export type ThemeDocumentV2 = z.infer<typeof ThemeDocumentV2>;
255
401
  /** Canonical type alias used everywhere else. */
256
- export type ThemeDocument = ThemeDocumentV1;
402
+ export type ThemeDocument = ThemeDocumentV2;
257
403
  /**
258
404
  * Parse a raw JSON value into a validated {@link ThemeDocument}.
259
405
  * Throws a `ZodError` with human-readable issues on invalid input.
@@ -264,5 +410,23 @@ export declare function parseThemeDocument(raw: unknown): ThemeDocument;
264
410
  * Prefer this inside CLI tooling where you want to render the issue
265
411
  * list without try/catch.
266
412
  */
267
- export declare function safeParseThemeDocument(raw: unknown): ReturnType<typeof ThemeDocumentV1.safeParse>;
413
+ export declare function safeParseThemeDocument(raw: unknown): ReturnType<typeof ThemeDocumentV2.safeParse>;
414
+ /**
415
+ * Turn a parsed document into the EXACT {@link DtcgTheme} the one
416
+ * producer (`deriveThemeVariables`) consumes:
417
+ *
418
+ * - names the document (`$name` ⇒ `'custom'`, `$description` ⇒ `''`);
419
+ * - drops the DTCG envelope fields the producer does not read
420
+ * (`$schema`, `$version`);
421
+ * - flattens the DTCG object forms to the CSS strings the producer
422
+ * reads: array font stacks → `'Inter, system-ui'`, numeric weights →
423
+ * `'400'`, structured shadows → `'0 1px 2px 0 …'`, structured
424
+ * transitions → `'<property ?? all> <duration> <timingFunction>'`;
425
+ * - fills the optional groups (`motion`, `accessibility`, `zIndex` and
426
+ * their sub-records) from the shipped default, so an external tool's
427
+ * colours-and-dimensions export still derives every consumed token.
428
+ *
429
+ * The authored roles, anchors and faces pass through verbatim.
430
+ */
431
+ export declare function normalizeThemeDocument(doc: ThemeDocument): DtcgTheme;
268
432
  //# sourceMappingURL=theme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA4OxB;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0C1B,CAAC;AAEH,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,iDAAiD;AACjD,MAAM,MAAM,aAAa,GAAG,eAAe,CAAC;AAE5C;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,GACX,UAAU,CAAC,OAAO,eAAe,CAAC,SAAS,CAAC,CAE9C"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,wBAAwB,CAAC;AA0PnE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6C1B,CAAC;AAEH,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,iDAAiD;AACjD,MAAM,MAAM,aAAa,GAAG,eAAe,CAAC;AAE5C;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,GACX,UAAU,CAAC,OAAO,eAAe,CAAC,SAAS,CAAC,CAE9C;AAYD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,CAmDpE"}
package/dist/theme.js CHANGED
@@ -1,82 +1,44 @@
1
1
  /**
2
- * `ggui.json#theme` → plain DTCG JSON document.
2
+ * `ggui.json#theme` → plain DTCG JSON document, v2 (ggui#987).
3
3
  *
4
4
  * The theme file is a plain JSON document matching the Design Tokens
5
- * Community Group (DTCG) spec. It is one of the open file-format
6
- * surfaces in the ggui manifest model, alongside
7
- * `ggui.primitives.json` and `ggui.ui.json`.
5
+ * Community Group (DTCG) spec, in the same vocabulary as
6
+ * `@ggui-ai/design`'s canonical {@link DtcgTheme}: the curated registry
7
+ * themes and an authored `theme.json` are the same shape.
8
8
  *
9
- * **Shape parity with the internal `DtcgTheme`.** This v1 schema is
10
- * deliberately the same shape as `@ggui-ai/design`'s canonical
11
- * {@link DtcgTheme} — `color`/`font`/`spacing`/`shape`/`motion`/
12
- * `accessibility`/`zIndex` — so an authored `theme.json`
13
- * uses the same vocabulary the curated registry themes (light/dark/
14
- * premium-*) use. Previously this schema was Tailwind-style flat
15
- * (`typography`/`radius`/`shadow` at root); the v1 schema now mirrors
16
- * the internal shape to remove the divergence.
9
+ * **What is authored, what is derived.** An author states the six
10
+ * layering roles (`ground`/`onGround`, `container`/`onContainer`,
11
+ * `sunken`/`onSunken`), one `500` anchor per family (`primary`,
12
+ * `success`, `warning`, `error`, `info`), the font families and
13
+ * weights, spacing, radii, shadows. Everything else — the ten-stop
14
+ * ramps, every `on*` ink and container, the neutral ladder, outlines,
15
+ * `elevated`, the type scale from the one `font.ramp` — is DERIVED by
16
+ * the one producer (`deriveThemeVariables`), so the retired ladders
17
+ * (`font.size`, `font.lineHeight`, `motion.duration`, `motion.easing`)
18
+ * and `$metadata.fontUrl` are refused, not ignored. Fonts are declared
19
+ * as `typography.faces` (https `src` only); the card installs them.
17
20
  *
18
- * **External-tool compatibility.** External tooling (Figma Tokens,
19
- * Style Dictionary, Tokens Studio) commonly emits only a subset of
20
- * the canonical shape. Only the original required groups (`color`,
21
- * `font`, `spacing`, `shape`) stay required; `motion`,
22
- * `accessibility`, `zIndex`, and the new DTCG metadata fields
23
- * (`$name`, `$description`, `$metadata`) are all OPTIONAL so tools
24
- * that only emit colors + dimensions still parse cleanly.
25
- *
26
- * **BREAKING for old `theme.json` files.** Authors who hand-wrote a
27
- * `theme.json` against the pre-rc schema must rename:
28
- *
29
- * - `typography.fontFamily` → `font.family`
30
- * - `typography.fontSize` → `font.size`
31
- * - `typography.fontWeight` → `font.weight`
32
- * - `typography.lineHeight` → `font.lineHeight`
33
- * - top-level `radius` → `shape.radius`
34
- * - top-level `shadow` → `shape.shadow`
35
- * - top-level `duration` → `motion.duration`
36
- * - top-level `transition` → `motion.transition`
37
- *
38
- * Plus: under `font.family`, `sans` is the only required sub-token
39
- * (was previously freeform); other family slots (`mono`, …) are
40
- * optional. The font-family record itself stays open so additional
41
- * named families parse without a schema change.
21
+ * **External-tool leniency.** `motion`, `accessibility`, `zIndex` and
22
+ * the DTCG metadata fields stay OPTIONAL; {@link normalizeThemeDocument}
23
+ * fills them from the shipped default and flattens the DTCG object
24
+ * forms (structured shadows, transitions, array font stacks) to the CSS
25
+ * strings the producer reads.
42
26
  *
43
27
  * **Ownership boundary:**
44
28
  *
45
29
  * - *Where* the theme lives → `ggui.json#theme` (pointer string).
46
30
  * - *What* the theme file looks like → this module's schema.
47
- * - *How* DTCG tokens become CSS variables → `@ggui-ai/design`'s
48
- * `generateCssVariables` (stays over there; this schema does not
49
- * emit). The duck-typed walker accepts any DTCG-shaped tree, so
50
- * the new nested groups (`font.size`, `shape.radius`, …) walk
51
- * into `--ggui-font-size-md`/`--ggui-shape-radius-md` CSS vars
52
- * without code changes.
31
+ * - *How* the document becomes CSS variables → `@ggui-ai/design`'s
32
+ * `deriveThemeVariables` (the loader calls it for both modes).
53
33
  * - *Built-in default* when `theme` is absent → `@ggui-ai/design`'s
54
- * shipped `lightTheme`.
55
- *
56
- * **Extending rules:**
34
+ * shipped `lightTheme` / `darkTheme`.
57
35
  *
58
- * 1. Additive only within `schema: '1'`. New optional fields must
59
- * default to no-op behaviour so older tooling ignores them.
60
- * 2. Framework-neutral + host-neutral. No vendor names in enum
61
- * values, no build-pipeline fields.
62
- * 3. Root and per-group objects are strict — unknown keys fail
63
- * parse. Same discipline as `ggui.json`, `ggui.ui.json`, and
64
- * `ggui.primitives.json`.
65
- *
66
- * **Strictness tradeoff:**
67
- *
68
- * Individual DTCG token leaves are validated with a discriminated
69
- * union over `$type` (color / dimension / fontFamily / fontWeight /
70
- * shadow / duration / cubicBezier / transition / number). Each
71
- * variant validates its `$value` shape. `$description` is accepted
72
- * everywhere as a free-form docstring. Token leaves under newer
73
- * groups (`accessibility`, `zIndex`) accept a slightly
74
- * wider token vocabulary because external tools sometimes encode
75
- * scalar/array values with permissive `$type` strings (`string`,
76
- * `array`, …); the schema accepts those without enumerating every
77
- * possible spelling.
36
+ * **Extending rules:** additive only within `schema: '1'`; framework-
37
+ * and host-neutral; root and per-group objects are strict — unknown
38
+ * keys fail parse, the same discipline as `ggui.json`.
78
39
  */
79
40
  import { z } from 'zod';
41
+ import { lightTheme } from '@ggui-ai/design/themes';
80
42
  // ─── Token leaves ────────────────────────────────────────────────────
81
43
  /** A CSS color string. `$value` is not structurally validated (hex,
82
44
  * rgba, color-mix, named, etc. are all legal DTCG colors). */
@@ -112,14 +74,6 @@ const DurationToken = z.strictObject({
112
74
  $value: z.string().min(1),
113
75
  $description: z.string().optional(),
114
76
  });
115
- /** DTCG cubic-bezier easing token. `$value` is a CSS timing function
116
- * string (`"cubic-bezier(0.4, 0, 0.2, 1)"`, keyword aliases like
117
- * `"ease-out"`, etc.). */
118
- const CubicBezierToken = z.strictObject({
119
- $type: z.literal('cubicBezier'),
120
- $value: z.string().min(1),
121
- $description: z.string().optional(),
122
- });
123
77
  /** Structured shadow value — DTCG spec composite shape. */
124
78
  const ShadowValue = z.strictObject({
125
79
  offsetX: z.string().min(1),
@@ -154,32 +108,34 @@ const NumberToken = z.strictObject({
154
108
  $description: z.string().optional(),
155
109
  });
156
110
  /** Line-height token — DTCG allows unit-less number or dimension. */
157
- const LineHeightToken = z.union([
158
- z.strictObject({
159
- $type: z.literal('number'),
160
- $value: z.number(),
161
- $description: z.string().optional(),
162
- }),
163
- z.strictObject({
164
- $type: z.literal('dimension'),
165
- $value: z.string().min(1),
166
- $description: z.string().optional(),
167
- }),
168
- ]);
169
- /**
170
- * Permissive token leaf for the newer optional groups
171
- * (`motion.keyframes`, `zIndex`). DTCG hasn't standardised every
172
- * `$type` value the curated registry themes carry — for example
173
- * easing uses `$type: 'cubicBezier'`. Rather than
174
- * enumerate every spelling, this leaf accepts any `$type` string and
175
- * any JSON-serializable `$value`. Strict per-leaf shape stays in
176
- * force for the original token types via the leaves above.
177
- */
178
- const PermissiveToken = z.strictObject({
111
+ /** A token whose `$value` is a CSS string under any `$type` spelling
112
+ * (keyframes text, border style, …). */
113
+ const StringToken = z.strictObject({
179
114
  $type: z.string().min(1),
180
- $value: z.unknown(),
115
+ $value: z.string().min(1),
181
116
  $description: z.string().optional(),
182
117
  });
118
+ /** A declared font face (ggui#987 §5): `src` MUST be `https:` with a
119
+ * well-formed host — the same rule the design package asserts. */
120
+ const FontFaceDeclarationSchema = z.strictObject({
121
+ family: z.string().min(1).refine((f) => !/[\r\n]/.test(f), 'family must be one line'),
122
+ src: z
123
+ .string()
124
+ .min(1)
125
+ .refine((src) => {
126
+ let url;
127
+ try {
128
+ url = new URL(src);
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ return url.protocol === 'https:' && /^[a-z0-9.-]+$/i.test(url.hostname) && url.hostname.includes('.');
134
+ }, 'src must be an https: URL with a well-formed host'),
135
+ weight: z.union([z.string().min(1), z.number().int().min(1).max(1000)]).optional(),
136
+ style: z.string().min(1).optional(),
137
+ display: z.string().min(1).optional(),
138
+ });
183
139
  // ─── Groups ──────────────────────────────────────────────────────────
184
140
  /**
185
141
  * A "palette" group — a record of color tokens keyed by scale step
@@ -188,42 +144,68 @@ const PermissiveToken = z.strictObject({
188
144
  */
189
145
  const ColorPalette = z.record(z.string(), ColorToken);
190
146
  /**
191
- * Color group — two-tier: `{palette}` records for scales (primary /
192
- * neutral / semantic scales) plus single-token semantic roles
193
- * (surface / onSurface / outline / …) at the top level.
194
- *
195
- * Kept open (`z.record` not `z.strictObject`) so authored themes
196
- * can add brand-specific palettes (`accent`, `brand-green`, …) and
197
- * semantic roles without a schema change. `color` itself is
198
- * required but its internal shape is author-extensible.
199
- *
200
- * Each entry can be EITHER a singleton `ColorToken` (e.g. a Material
201
- * role like `surface`) OR a `ColorPalette` (a scale like `primary`'s
202
- * 50-900 stops). Tools that emit only singleton colors (no scales)
203
- * stay parseable.
147
+ * The colour block: five family anchors (`500` stated; the other stops
148
+ * derived unless stated) + the six layering roles, all REQUIRED; the
149
+ * derived roles (`neutral`, outlines, `on*` inks, containers,
150
+ * `tertiary`, `link`) are optional and win over derivation when stated.
204
151
  */
205
- const ColorGroup = z.record(z.string(), z.union([ColorToken, ColorPalette]));
152
+ const ColorGroup = z.strictObject({
153
+ primary: ColorPalette,
154
+ neutral: ColorPalette.optional(),
155
+ success: ColorPalette,
156
+ warning: ColorPalette,
157
+ error: ColorPalette,
158
+ info: ColorPalette,
159
+ ground: ColorToken,
160
+ onGround: ColorToken,
161
+ container: ColorToken,
162
+ onContainer: ColorToken,
163
+ sunken: ColorToken,
164
+ onSunken: ColorToken,
165
+ link: ColorToken.optional(),
166
+ outline: ColorToken.optional(),
167
+ outlineVariant: ColorToken.optional(),
168
+ onPrimary: ColorToken.optional(),
169
+ primaryContainer: ColorToken.optional(),
170
+ onPrimaryContainer: ColorToken.optional(),
171
+ onSuccess: ColorToken.optional(),
172
+ successContainer: ColorToken.optional(),
173
+ onSuccessContainer: ColorToken.optional(),
174
+ onWarning: ColorToken.optional(),
175
+ warningContainer: ColorToken.optional(),
176
+ onWarningContainer: ColorToken.optional(),
177
+ onError: ColorToken.optional(),
178
+ errorContainer: ColorToken.optional(),
179
+ onErrorContainer: ColorToken.optional(),
180
+ onInfo: ColorToken.optional(),
181
+ infoContainer: ColorToken.optional(),
182
+ onInfoContainer: ColorToken.optional(),
183
+ tertiary: ColorToken.optional(),
184
+ onTertiary: ColorToken.optional(),
185
+ tertiaryContainer: ColorToken.optional(),
186
+ onTertiaryContainer: ColorToken.optional(),
187
+ });
206
188
  const SpacingGroup = z.record(z.string(), DimensionToken);
207
189
  /**
208
- * Font group — `family` / `size` / `weight` / `lineHeight` records.
209
- * Renamed from `typography` (previously: `typography.fontFamily`,
210
- * `typography.fontSize`, …) to mirror the internal `DtcgTheme`
211
- * shape's `font.family` / `font.size` / `font.weight` /
212
- * `font.lineHeight`.
213
- *
214
- * Inside `family`, `sans` is the only required slot — the canonical
215
- * theme always carries a sans-serif default. `mono` and any other
216
- * family slot (e.g. `serif`, `display`) are optional. The record
217
- * stays open so authors can declare additional named families.
190
+ * Font group — `family` (the `sans` slot required; `mono`, `heading`
191
+ * and any other named family optional) and `weight`; optional
192
+ * `letterSpacing` and the ONE `ramp` the type scale is derived from.
218
193
  */
194
+ /** Font families: `sans` required; `mono`, `heading` and any other named family optional. */
219
195
  const FontFamilyGroup = z.record(z.string(), FontFamilyToken).and(z.object({
220
196
  sans: FontFamilyToken,
221
197
  }));
222
198
  const FontGroup = z.strictObject({
223
199
  family: FontFamilyGroup,
224
- size: z.record(z.string(), DimensionToken),
225
200
  weight: z.record(z.string(), FontWeightToken),
226
- lineHeight: z.record(z.string(), LineHeightToken),
201
+ letterSpacing: z
202
+ .strictObject({ body: DimensionToken.optional(), heading: DimensionToken.optional() })
203
+ .optional(),
204
+ ramp: z.strictObject({ base: DimensionToken, ratio: NumberToken }).optional(),
205
+ });
206
+ /** Declared font faces (ggui#987 §5). */
207
+ const TypographyGroup = z.strictObject({
208
+ faces: z.array(FontFaceDeclarationSchema).optional(),
227
209
  });
228
210
  /**
229
211
  * Shape group — `radius` + `shadow`. Moved here from the previous
@@ -233,18 +215,16 @@ const FontGroup = z.strictObject({
233
215
  const ShapeGroup = z.strictObject({
234
216
  radius: z.record(z.string(), DimensionToken),
235
217
  shadow: z.record(z.string(), ShadowToken),
218
+ border: z.strictObject({ width: DimensionToken.optional(), style: StringToken.optional() }).optional(),
236
219
  });
237
220
  /**
238
- * Motion group — `duration` / `easing` / `transition` / `keyframes`.
239
- * Moved here from the previous top-level `duration` and `transition`
240
- * fields. `easing` and `keyframes` are optional because external
241
- * tools (Figma Tokens, Style Dictionary) don't always emit them.
221
+ * Motion group — `transition` (required sub-record) + `keyframes`
222
+ * (optional). Durations and easings are not ladders any more: they ride
223
+ * each transition's own value.
242
224
  */
243
225
  const MotionGroup = z.strictObject({
244
- duration: z.record(z.string(), DurationToken),
245
- easing: z.record(z.string(), CubicBezierToken).optional(),
246
226
  transition: z.record(z.string(), TransitionToken),
247
- keyframes: z.record(z.string(), PermissiveToken).optional(),
227
+ keyframes: z.record(z.string(), StringToken).optional(),
248
228
  });
249
229
  const AccessibilityGroup = z.strictObject({
250
230
  focusRing: z
@@ -276,8 +256,9 @@ const ZIndexGroup = z.record(z.string(), NumberToken);
276
256
  */
277
257
  const MetadataGroup = z.strictObject({
278
258
  font: z.string().min(1).optional(),
279
- fontUrl: z.string().min(1).optional(),
280
259
  philosophy: z.string().min(1).optional(),
260
+ /** The embedding host draws the card silhouette; the renderer suppresses root-children strokes. */
261
+ frameless: z.boolean().optional(),
281
262
  });
282
263
  // ─── Root document ───────────────────────────────────────────────────
283
264
  /**
@@ -299,7 +280,7 @@ const MetadataGroup = z.strictObject({
299
280
  * allow-listed (DTCG metadata); anything else is a typo. Additive
300
281
  * slices add new fields here under `schema: '1'`.
301
282
  */
302
- export const ThemeDocumentV1 = z.strictObject({
283
+ export const ThemeDocumentV2 = z.strictObject({
303
284
  /** Optional DTCG spec URL. Not validated — the spec hasn't frozen
304
285
  * a canonical URL and hand-authored themes often omit it. */
305
286
  $schema: z.string().min(1).optional(),
@@ -320,6 +301,8 @@ export const ThemeDocumentV1 = z.strictObject({
320
301
  spacing: SpacingGroup,
321
302
  /** Font tokens — `family` / `size` / `weight` / `lineHeight`. Required. */
322
303
  font: FontGroup,
304
+ /** Declared font faces (ggui#987 §5) — https `src` only; the card installs them. */
305
+ typography: TypographyGroup.optional(),
323
306
  /** Shape tokens — `radius` + `shadow`. Required. */
324
307
  shape: ShapeGroup,
325
308
  /** Motion tokens — `duration` + `transition` (required sub-records),
@@ -336,7 +319,7 @@ export const ThemeDocumentV1 = z.strictObject({
336
319
  * Throws a `ZodError` with human-readable issues on invalid input.
337
320
  */
338
321
  export function parseThemeDocument(raw) {
339
- return ThemeDocumentV1.parse(raw);
322
+ return ThemeDocumentV2.parse(raw);
340
323
  }
341
324
  /**
342
325
  * Safe-parse variant — returns a discriminated `z.safeParse` result.
@@ -344,5 +327,69 @@ export function parseThemeDocument(raw) {
344
327
  * list without try/catch.
345
328
  */
346
329
  export function safeParseThemeDocument(raw) {
347
- return ThemeDocumentV1.safeParse(raw);
330
+ return ThemeDocumentV2.safeParse(raw);
331
+ }
332
+ // ─── Normalisation → the producer's document ─────────────────────────
333
+ function stringToken(token, value) {
334
+ return { $type: token.$type, $value: value, ...(token.$description !== undefined ? { $description: token.$description } : {}) };
335
+ }
336
+ function mapRecord(record, f) {
337
+ return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, f(v)]));
338
+ }
339
+ /**
340
+ * Turn a parsed document into the EXACT {@link DtcgTheme} the one
341
+ * producer (`deriveThemeVariables`) consumes:
342
+ *
343
+ * - names the document (`$name` ⇒ `'custom'`, `$description` ⇒ `''`);
344
+ * - drops the DTCG envelope fields the producer does not read
345
+ * (`$schema`, `$version`);
346
+ * - flattens the DTCG object forms to the CSS strings the producer
347
+ * reads: array font stacks → `'Inter, system-ui'`, numeric weights →
348
+ * `'400'`, structured shadows → `'0 1px 2px 0 …'`, structured
349
+ * transitions → `'<property ?? all> <duration> <timingFunction>'`;
350
+ * - fills the optional groups (`motion`, `accessibility`, `zIndex` and
351
+ * their sub-records) from the shipped default, so an external tool's
352
+ * colours-and-dimensions export still derives every consumed token.
353
+ *
354
+ * The authored roles, anchors and faces pass through verbatim.
355
+ */
356
+ export function normalizeThemeDocument(doc) {
357
+ const family = doc.font.family;
358
+ const fontFamily = (t) => stringToken(t, Array.isArray(t.$value) ? t.$value.join(', ') : t.$value);
359
+ const accessibility = doc.accessibility;
360
+ return {
361
+ $name: doc.$name ?? 'custom',
362
+ $description: doc.$description ?? '',
363
+ ...(doc.$metadata !== undefined ? { $metadata: doc.$metadata } : {}),
364
+ color: doc.color,
365
+ font: {
366
+ family: { ...mapRecord(family, fontFamily), sans: fontFamily(family.sans) },
367
+ weight: mapRecord(doc.font.weight, (t) => stringToken(t, String(t.$value))),
368
+ ...(doc.font.letterSpacing !== undefined ? { letterSpacing: doc.font.letterSpacing } : {}),
369
+ ...(doc.font.ramp !== undefined ? { ramp: doc.font.ramp } : {}),
370
+ },
371
+ ...(doc.typography !== undefined ? { typography: doc.typography } : {}),
372
+ spacing: doc.spacing,
373
+ shape: {
374
+ radius: doc.shape.radius,
375
+ shadow: mapRecord(doc.shape.shadow, (t) => stringToken(t, typeof t.$value === 'string'
376
+ ? t.$value
377
+ : `${t.$value.offsetX} ${t.$value.offsetY} ${t.$value.blur} ${t.$value.spread} ${t.$value.color}`)),
378
+ ...(doc.shape.border !== undefined ? { border: doc.shape.border } : {}),
379
+ },
380
+ motion: doc.motion === undefined
381
+ ? lightTheme.motion
382
+ : {
383
+ transition: mapRecord(doc.motion.transition, (t) => stringToken(t, typeof t.$value === 'string'
384
+ ? t.$value
385
+ : `${t.$value.property ?? 'all'} ${t.$value.duration} ${t.$value.timingFunction}`)),
386
+ keyframes: doc.motion.keyframes ?? lightTheme.motion.keyframes,
387
+ },
388
+ accessibility: {
389
+ focusRing: accessibility?.focusRing ?? lightTheme.accessibility.focusRing,
390
+ reducedMotion: accessibility?.reducedMotion ?? lightTheme.accessibility.reducedMotion,
391
+ highContrast: accessibility?.highContrast ?? lightTheme.accessibility.highContrast,
392
+ },
393
+ zIndex: doc.zIndex ?? lightTheme.zIndex,
394
+ };
348
395
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/project-config",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Schema and loader for `ggui.json` — the portable app/agent manifest for the ggui protocol. Vendor-neutral. Consumed by the `ggui` CLI and any host that reconstitutes an agent from the manifest.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -33,8 +33,8 @@
33
33
  "dependencies": {
34
34
  "tinyglobby": "^0.2.16",
35
35
  "zod": "^4.3.6",
36
- "@ggui-ai/design": "0.14.0",
37
- "@ggui-ai/protocol": "0.14.0"
36
+ "@ggui-ai/design": "0.16.0",
37
+ "@ggui-ai/protocol": "0.16.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^24.0.0",