@ggui-ai/project-config 0.16.0 → 0.17.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/theme.d.ts CHANGED
@@ -326,6 +326,135 @@ export declare const ThemeDocumentV2: z.ZodObject<{
326
326
  }, z.core.$strict>>;
327
327
  }, z.core.$strict>>;
328
328
  }, z.core.$strict>;
329
+ typeScale: z.ZodOptional<z.ZodObject<{
330
+ display: z.ZodOptional<z.ZodObject<{
331
+ size: z.ZodObject<{
332
+ $type: z.ZodLiteral<"dimension">;
333
+ $value: z.ZodString;
334
+ $description: z.ZodOptional<z.ZodString>;
335
+ }, z.core.$strict>;
336
+ weight: z.ZodOptional<z.ZodObject<{
337
+ $type: z.ZodLiteral<"fontWeight">;
338
+ $value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
339
+ $description: z.ZodOptional<z.ZodString>;
340
+ }, z.core.$strict>>;
341
+ tracking: z.ZodOptional<z.ZodObject<{
342
+ $type: z.ZodLiteral<"dimension">;
343
+ $value: z.ZodString;
344
+ $description: z.ZodOptional<z.ZodString>;
345
+ }, z.core.$strict>>;
346
+ leading: z.ZodOptional<z.ZodObject<{
347
+ $type: z.ZodLiteral<"number">;
348
+ $value: z.ZodNumber;
349
+ $description: z.ZodOptional<z.ZodString>;
350
+ }, z.core.$strict>>;
351
+ }, z.core.$strict>>;
352
+ h1: z.ZodOptional<z.ZodObject<{
353
+ size: z.ZodObject<{
354
+ $type: z.ZodLiteral<"dimension">;
355
+ $value: z.ZodString;
356
+ $description: z.ZodOptional<z.ZodString>;
357
+ }, z.core.$strict>;
358
+ weight: z.ZodOptional<z.ZodObject<{
359
+ $type: z.ZodLiteral<"fontWeight">;
360
+ $value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
361
+ $description: z.ZodOptional<z.ZodString>;
362
+ }, z.core.$strict>>;
363
+ tracking: z.ZodOptional<z.ZodObject<{
364
+ $type: z.ZodLiteral<"dimension">;
365
+ $value: z.ZodString;
366
+ $description: z.ZodOptional<z.ZodString>;
367
+ }, z.core.$strict>>;
368
+ leading: z.ZodOptional<z.ZodObject<{
369
+ $type: z.ZodLiteral<"number">;
370
+ $value: z.ZodNumber;
371
+ $description: z.ZodOptional<z.ZodString>;
372
+ }, z.core.$strict>>;
373
+ }, z.core.$strict>>;
374
+ h2: z.ZodOptional<z.ZodObject<{
375
+ size: z.ZodObject<{
376
+ $type: z.ZodLiteral<"dimension">;
377
+ $value: z.ZodString;
378
+ $description: z.ZodOptional<z.ZodString>;
379
+ }, z.core.$strict>;
380
+ weight: z.ZodOptional<z.ZodObject<{
381
+ $type: z.ZodLiteral<"fontWeight">;
382
+ $value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
383
+ $description: z.ZodOptional<z.ZodString>;
384
+ }, z.core.$strict>>;
385
+ tracking: z.ZodOptional<z.ZodObject<{
386
+ $type: z.ZodLiteral<"dimension">;
387
+ $value: z.ZodString;
388
+ $description: z.ZodOptional<z.ZodString>;
389
+ }, z.core.$strict>>;
390
+ leading: z.ZodOptional<z.ZodObject<{
391
+ $type: z.ZodLiteral<"number">;
392
+ $value: z.ZodNumber;
393
+ $description: z.ZodOptional<z.ZodString>;
394
+ }, z.core.$strict>>;
395
+ }, z.core.$strict>>;
396
+ body: z.ZodOptional<z.ZodObject<{
397
+ size: z.ZodObject<{
398
+ $type: z.ZodLiteral<"dimension">;
399
+ $value: z.ZodString;
400
+ $description: z.ZodOptional<z.ZodString>;
401
+ }, z.core.$strict>;
402
+ weight: z.ZodOptional<z.ZodObject<{
403
+ $type: z.ZodLiteral<"fontWeight">;
404
+ $value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
405
+ $description: z.ZodOptional<z.ZodString>;
406
+ }, z.core.$strict>>;
407
+ tracking: z.ZodOptional<z.ZodObject<{
408
+ $type: z.ZodLiteral<"dimension">;
409
+ $value: z.ZodString;
410
+ $description: z.ZodOptional<z.ZodString>;
411
+ }, z.core.$strict>>;
412
+ leading: z.ZodOptional<z.ZodObject<{
413
+ $type: z.ZodLiteral<"number">;
414
+ $value: z.ZodNumber;
415
+ $description: z.ZodOptional<z.ZodString>;
416
+ }, z.core.$strict>>;
417
+ }, z.core.$strict>>;
418
+ label: z.ZodOptional<z.ZodObject<{
419
+ size: z.ZodObject<{
420
+ $type: z.ZodLiteral<"dimension">;
421
+ $value: z.ZodString;
422
+ $description: z.ZodOptional<z.ZodString>;
423
+ }, z.core.$strict>;
424
+ weight: z.ZodOptional<z.ZodObject<{
425
+ $type: z.ZodLiteral<"fontWeight">;
426
+ $value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
427
+ $description: z.ZodOptional<z.ZodString>;
428
+ }, z.core.$strict>>;
429
+ tracking: z.ZodOptional<z.ZodObject<{
430
+ $type: z.ZodLiteral<"dimension">;
431
+ $value: z.ZodString;
432
+ $description: z.ZodOptional<z.ZodString>;
433
+ }, z.core.$strict>>;
434
+ leading: z.ZodOptional<z.ZodObject<{
435
+ $type: z.ZodLiteral<"number">;
436
+ $value: z.ZodNumber;
437
+ $description: z.ZodOptional<z.ZodString>;
438
+ }, z.core.$strict>>;
439
+ }, z.core.$strict>>;
440
+ }, z.core.$strict>>;
441
+ rhythm: z.ZodOptional<z.ZodObject<{
442
+ base: z.ZodObject<{
443
+ $type: z.ZodLiteral<"dimension">;
444
+ $value: z.ZodString;
445
+ $description: z.ZodOptional<z.ZodString>;
446
+ }, z.core.$strict>;
447
+ section: z.ZodOptional<z.ZodObject<{
448
+ $type: z.ZodLiteral<"dimension">;
449
+ $value: z.ZodString;
450
+ $description: z.ZodOptional<z.ZodString>;
451
+ }, z.core.$strict>>;
452
+ inset: z.ZodOptional<z.ZodObject<{
453
+ $type: z.ZodLiteral<"dimension">;
454
+ $value: z.ZodString;
455
+ $description: z.ZodOptional<z.ZodString>;
456
+ }, z.core.$strict>>;
457
+ }, z.core.$strict>>;
329
458
  motion: z.ZodOptional<z.ZodObject<{
330
459
  transition: z.ZodRecord<z.ZodString, z.ZodObject<{
331
460
  $type: z.ZodLiteral<"transition">;
@@ -341,6 +470,64 @@ export declare const ThemeDocumentV2: z.ZodObject<{
341
470
  $value: z.ZodString;
342
471
  $description: z.ZodOptional<z.ZodString>;
343
472
  }, z.core.$strict>>>;
473
+ duration: z.ZodOptional<z.ZodObject<{
474
+ fast: z.ZodOptional<z.ZodObject<{
475
+ $type: z.ZodLiteral<"duration">;
476
+ $value: z.ZodString;
477
+ $description: z.ZodOptional<z.ZodString>;
478
+ }, z.core.$strict>>;
479
+ base: z.ZodOptional<z.ZodObject<{
480
+ $type: z.ZodLiteral<"duration">;
481
+ $value: z.ZodString;
482
+ $description: z.ZodOptional<z.ZodString>;
483
+ }, z.core.$strict>>;
484
+ slow: z.ZodOptional<z.ZodObject<{
485
+ $type: z.ZodLiteral<"duration">;
486
+ $value: z.ZodString;
487
+ $description: z.ZodOptional<z.ZodString>;
488
+ }, z.core.$strict>>;
489
+ }, z.core.$strict>>;
490
+ easing: z.ZodOptional<z.ZodObject<{
491
+ standard: z.ZodOptional<z.ZodObject<{
492
+ $type: z.ZodString;
493
+ $value: z.ZodString;
494
+ $description: z.ZodOptional<z.ZodString>;
495
+ }, z.core.$strict>>;
496
+ emphasized: z.ZodOptional<z.ZodObject<{
497
+ $type: z.ZodString;
498
+ $value: z.ZodString;
499
+ $description: z.ZodOptional<z.ZodString>;
500
+ }, z.core.$strict>>;
501
+ exit: z.ZodOptional<z.ZodObject<{
502
+ $type: z.ZodString;
503
+ $value: z.ZodString;
504
+ $description: z.ZodOptional<z.ZodString>;
505
+ }, z.core.$strict>>;
506
+ }, z.core.$strict>>;
507
+ reduce: z.ZodOptional<z.ZodEnum<{
508
+ ignore: "ignore";
509
+ respect: "respect";
510
+ }>>;
511
+ }, z.core.$strict>>;
512
+ scrim: z.ZodOptional<z.ZodObject<{
513
+ tone: z.ZodUnion<readonly [z.ZodEnum<{
514
+ light: "light";
515
+ dark: "dark";
516
+ }>, z.ZodObject<{
517
+ $type: z.ZodLiteral<"color">;
518
+ $value: z.ZodString;
519
+ $description: z.ZodOptional<z.ZodString>;
520
+ }, z.core.$strict>]>;
521
+ opacity: z.ZodObject<{
522
+ $type: z.ZodLiteral<"number">;
523
+ $value: z.ZodNumber;
524
+ $description: z.ZodOptional<z.ZodString>;
525
+ }, z.core.$strict>;
526
+ blur: z.ZodObject<{
527
+ $type: z.ZodLiteral<"dimension">;
528
+ $value: z.ZodString;
529
+ $description: z.ZodOptional<z.ZodString>;
530
+ }, z.core.$strict>;
344
531
  }, z.core.$strict>>;
345
532
  accessibility: z.ZodOptional<z.ZodObject<{
346
533
  focusRing: z.ZodOptional<z.ZodObject<{
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,wBAAwB,CAAC;AAqYnE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA8D1B,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
@@ -38,6 +38,7 @@
38
38
  * keys fail parse, the same discipline as `ggui.json`.
39
39
  */
40
40
  import { z } from 'zod';
41
+ import { fontFaceDeclarationSchema } from '@ggui-ai/protocol';
41
42
  import { lightTheme } from '@ggui-ai/design/themes';
42
43
  // ─── Token leaves ────────────────────────────────────────────────────
43
44
  /** A CSS color string. `$value` is not structurally validated (hex,
@@ -115,27 +116,6 @@ const StringToken = z.strictObject({
115
116
  $value: z.string().min(1),
116
117
  $description: z.string().optional(),
117
118
  });
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
- });
139
119
  // ─── Groups ──────────────────────────────────────────────────────────
140
120
  /**
141
121
  * A "palette" group — a record of color tokens keyed by scale step
@@ -205,7 +185,8 @@ const FontGroup = z.strictObject({
205
185
  });
206
186
  /** Declared font faces (ggui#987 §5). */
207
187
  const TypographyGroup = z.strictObject({
208
- faces: z.array(FontFaceDeclarationSchema).optional(),
188
+ /** The ONE face grammar — `@ggui-ai/protocol`'s `fontFaceDeclarationSchema` (ggui#1093): the document door and the AppTheme wire's `fonts` validate identically. */
189
+ faces: z.array(fontFaceDeclarationSchema).optional(),
209
190
  });
210
191
  /**
211
192
  * Shape group — `radius` + `shadow`. Moved here from the previous
@@ -222,9 +203,155 @@ const ShapeGroup = z.strictObject({
222
203
  * (optional). Durations and easings are not ladders any more: they ride
223
204
  * each transition's own value.
224
205
  */
206
+ /**
207
+ * The HOST DESIGN LANGUAGE's document members (ggui#1093 P1b, #1075 Track C (a);
208
+ * grammar co-signed with rnd 2026-09-15). These are DOCUMENT tokens, never wire
209
+ * members: they reach the card through `deriveThemeVariables` → the overlay,
210
+ * which stays the ONE projection (`docs/principles/mcp-apps-compliance.md`).
211
+ * Every group is OPTIONAL and absent ⇒ the document parses exactly as before
212
+ * (INVARIANT 1: no declaration, byte-identical output).
213
+ */
214
+ /**
215
+ * One type role. `size` is required; `leading` is a RATIO (`NumberToken`) and
216
+ * never a length — the projection writes `line-height: <ratio>`, which survives
217
+ * a size change, while a length leading breaks the moment the role's size moves.
218
+ */
219
+ const TypeScaleRole = z.strictObject({
220
+ size: DimensionToken,
221
+ weight: FontWeightToken.optional(),
222
+ tracking: DimensionToken.optional(),
223
+ leading: NumberToken.optional(),
224
+ });
225
+ /**
226
+ * PRECEDENCE, and the ONE sentence that governs every member in this group
227
+ * (ggui#1093 R1, ruled with rnd 2026-09-16): **a host-design-language member
228
+ * overrides the base document's corresponding ladder.** A stated `typeScale`
229
+ * role beats the `font.ramp` stop that would have generated it; a stated
230
+ * `rhythm.base` beats the `spacing` stops it re-derives. Those two read as
231
+ * opposite directions (role over scale, scale over rungs) and are not: in
232
+ * both, the member the HOST measured wins over the ladder ggui supplied.
233
+ *
234
+ * The asymmetry that makes the sentence necessary: a `typeScale` role IS a
235
+ * rung, so it wins its own stop and leaves the others to the ramp — a harvest
236
+ * that measured only `h1` and `body` must not blank the rest. A `rhythm.base`
237
+ * is a statement about the SCALE, and every real document already carries
238
+ * spacing stops (the base theme supplies them), so "stated stop wins" would
239
+ * make the member inert on every document a harvest can produce.
240
+ *
241
+ * Nothing is lost by it: a document needing one bespoke step states no
242
+ * rhythm, and an app needing one pinned value has `AppTheme.cssVariables`,
243
+ * which overrides the whole projection at the overlay layer.
244
+ *
245
+ * CONTRACTED AHEAD OF THEIR CONSUMERS — read this before assuming a stated
246
+ * member changes a pixel. A member may be contracted before its consumer
247
+ * exists; a VARIABLE may not enter the consumed-token manifest before its
248
+ * consumer exists (a name nothing reads is coverage debt, not a feature).
249
+ * Today that makes three of these members STATED, STORED and NOT YET
250
+ * VISIBLE: the role-named type families and `scrim` arrive with their
251
+ * consumers (ggui#1075 Track C (b), ggui#1083), and `motion` with
252
+ * **ggui#1106** — the card's motion is build-time constants in the
253
+ * primitives, so no `--ggui-motion-*` variable exists to project onto yet
254
+ * (the manifest carries 122 tokens and none of them is motion). A writer
255
+ * may declare all of them now, which is the point of contracting first;
256
+ * nobody should read the contract as shipping the behaviour.
257
+ */
258
+ /** The five type roles; `display` is the poster-scale role a host page leads with. */
259
+ const TypeScaleGroup = z.strictObject({
260
+ display: TypeScaleRole.optional(),
261
+ h1: TypeScaleRole.optional(),
262
+ h2: TypeScaleRole.optional(),
263
+ body: TypeScaleRole.optional(),
264
+ label: TypeScaleRole.optional(),
265
+ });
266
+ /**
267
+ * The spacing base a rhythm re-derives its scale from: a positive length in
268
+ * `px` (2–16) or `rem` / `em` (0.125–1, the same range at a 16px root). Other
269
+ * units and `calc()` are refused at the door — a base the reader cannot
270
+ * multiply is not a base.
271
+ */
272
+ const RHYTHM_BASE_RE = /^(\d+(?:\.\d+)?)(px|rem|em)$/;
273
+ function isRhythmBase(value) {
274
+ const m = RHYTHM_BASE_RE.exec(value);
275
+ if (m === null)
276
+ return false;
277
+ const n = Number(m[1]);
278
+ return m[2] === 'px' ? n >= 2 && n <= 16 : n >= 0.125 && n <= 1;
279
+ }
280
+ const RhythmBaseToken = DimensionToken.refine((t) => isRhythmBase(t.$value), 'rhythm.base must be a length in px (2–16) or rem/em (0.125–1)');
281
+ /** `base` re-derives the spacing scale; `section` and `inset` are named steps. */
282
+ const RhythmGroup = z.strictObject({
283
+ base: RhythmBaseToken,
284
+ section: DimensionToken.optional(),
285
+ inset: DimensionToken.optional(),
286
+ });
287
+ /** The CSS easing keywords a document may name. */
288
+ const CSS_EASING_KEYWORDS = new Set(['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end']);
289
+ const CUBIC_BEZIER_RE = /^cubic-bezier\(\s*-?\d*\.?\d+\s*(?:,\s*-?\d*\.?\d+\s*){3}\)$/;
290
+ const STEPS_RE = /^steps\(\s*\d+\s*(?:,\s*(?:jump-start|jump-end|jump-none|jump-both|start|end)\s*)?\)$/;
291
+ /**
292
+ * An easing a projection can emit verbatim: a CSS keyword, a well-formed
293
+ * `cubic-bezier(a,b,c,d)`, or `steps(n[, position])`. Validated HERE so no
294
+ * reader has to clamp a string it cannot trust.
295
+ */
296
+ function isCssEasing(value) {
297
+ return CSS_EASING_KEYWORDS.has(value) || CUBIC_BEZIER_RE.test(value) || STEPS_RE.test(value);
298
+ }
299
+ const EasingToken = z.strictObject({
300
+ $type: z.string().min(1),
301
+ $value: z.string().min(1).refine(isCssEasing, 'easing must be a CSS keyword, cubic-bezier(a,b,c,d) or steps(n[, position])'),
302
+ $description: z.string().optional(),
303
+ });
304
+ /**
305
+ * The per-app motion TEMPO OVERRIDE (ggui#1093 P1c; reverses the layer-1
306
+ * retirement of the theming spec §2.3, co-signed with rnd 2026-09-16).
307
+ *
308
+ * Layer-1 keeps the motion SCALE — the durations and easings the design
309
+ * system ships, which every card gets by default and no app may redefine
310
+ * wholesale. What a document may state is a BOUNDED tempo override, for a
311
+ * card that has to sit inside someone else's site: exactly the standing
312
+ * `palette` has (per-app forever, without that making colour "not
313
+ * layer-1"). The retired FREE-MAP ladders stay refused — three duration
314
+ * steps and three easing roles are admitted, and nothing else.
315
+ */
316
+ const MotionDurationGroup = z.strictObject({
317
+ fast: DurationToken.optional(),
318
+ base: DurationToken.optional(),
319
+ slow: DurationToken.optional(),
320
+ });
321
+ const MotionEasingGroup = z.strictObject({
322
+ standard: EasingToken.optional(),
323
+ emphasized: EasingToken.optional(),
324
+ exit: EasingToken.optional(),
325
+ });
326
+ /**
327
+ * The scrim a host page's ground shows through (ggui#1083): `tone` is a word
328
+ * (`light` / `dark`) or a stated colour token, `opacity` a 0..1 ratio, `blur` a
329
+ * length. Absent ⇒ the completion derives it from the ground pair, so a
330
+ * builder who declares nothing still gets a scrim that matches their page.
331
+ */
332
+ const ScrimGroup = z.strictObject({
333
+ tone: z.union([z.enum(['light', 'dark']), ColorToken]),
334
+ opacity: NumberToken.refine((t) => t.$value >= 0 && t.$value <= 1, 'scrim.opacity must be a ratio between 0 and 1'),
335
+ blur: DimensionToken,
336
+ });
225
337
  const MotionGroup = z.strictObject({
226
338
  transition: z.record(z.string(), TransitionToken),
227
339
  keyframes: z.record(z.string(), StringToken).optional(),
340
+ /** Per-app tempo override — three steps, bounded (ggui#1093 P1c). */
341
+ duration: MotionDurationGroup.optional(),
342
+ /** Per-app easing override — three roles, each value validated at the door (ggui#1093 P1c). */
343
+ easing: MotionEasingGroup.optional(),
344
+ /**
345
+ * What the composer does under `prefers-reduced-motion: reduce`.
346
+ *
347
+ * Two defaults that must never be read as disagreeing (rnd, ggui#1093
348
+ * P1c): the DOOR does not stamp a value — absent stays absent, so the
349
+ * document never carries something its author did not write — and the
350
+ * COMPOSER's default is `respect`: it emits the reduced-motion rule
351
+ * unless a document explicitly says `ignore`. Absence therefore means
352
+ * "respect" downstream, without the wire inventing it.
353
+ */
354
+ reduce: z.enum(['respect', 'ignore']).optional(),
228
355
  });
229
356
  const AccessibilityGroup = z.strictObject({
230
357
  focusRing: z
@@ -305,9 +432,23 @@ export const ThemeDocumentV2 = z.strictObject({
305
432
  typography: TypographyGroup.optional(),
306
433
  /** Shape tokens — `radius` + `shadow`. Required. */
307
434
  shape: ShapeGroup,
308
- /** Motion tokens — `duration` + `transition` (required sub-records),
309
- * `easing` + `keyframes` (optional sub-records). Optional. */
435
+ /** The five type ROLES of the host's design language (ggui#1093 P1b):
436
+ * display / h1 / h2 / body / label, each `size` + optional weight,
437
+ * tracking and a RATIO leading. Role-based, beside `font.ramp`'s
438
+ * size-stop ladder — the projection reads a role when it is stated and
439
+ * falls to the ramp when it is not. Optional. */
440
+ typeScale: TypeScaleGroup.optional(),
441
+ /** The host's spacing rhythm (ggui#1093 P1b): `base` re-derives the scale,
442
+ * `section` / `inset` are named steps. Optional. */
443
+ rhythm: RhythmGroup.optional(),
444
+ /** Motion tokens — `transition` (required sub-record), `keyframes`, and
445
+ * the bounded per-app TEMPO OVERRIDE (ggui#1093 P1c): `duration`
446
+ * (fast/base/slow), `easing` (standard/emphasized/exit) and `reduce`.
447
+ * The motion SCALE stays layer-1; the free-map ladders stay refused. */
310
448
  motion: MotionGroup.optional(),
449
+ /** The scrim between the host page's ground and the card (ggui#1093 P1b,
450
+ * ggui#1083). Absent ⇒ derived from the ground pair. Optional. */
451
+ scrim: ScrimGroup.optional(),
311
452
  /** Accessibility tokens (focus ring, reduced motion, high contrast).
312
453
  * Optional. */
313
454
  accessibility: AccessibilityGroup.optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/project-config",
3
- "version": "0.16.0",
3
+ "version": "0.17.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.16.0",
37
- "@ggui-ai/protocol": "0.16.0"
36
+ "@ggui-ai/design": "0.17.0",
37
+ "@ggui-ai/protocol": "0.17.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^24.0.0",