@yahoo/uds-create-config 5.27.1 → 5.29.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 (33) hide show
  1. package/dist/configs/react-native-system.d.ts +1 -0
  2. package/dist/configs/system.js +4 -0
  3. package/dist/entities/native/NativeSettings.d.ts +1 -0
  4. package/dist/entities/system/Font.d.ts +20 -1
  5. package/dist/entities/system/Font.js +50 -4
  6. package/dist/entities/system/LinkedSystem.d.ts +12 -0
  7. package/dist/entities/system/LinkedSystem.js +114 -18
  8. package/dist/entities/system/Settings.d.ts +3 -0
  9. package/dist/entities/system/Settings.js +7 -0
  10. package/dist/entities/system/link-declaration.d.ts +43 -2
  11. package/dist/entities/system/link-declaration.js +110 -15
  12. package/dist/entities/system/link-plan.js +109 -4
  13. package/dist/framework/Config.d.ts +6 -1
  14. package/dist/framework/Config.js +28 -6
  15. package/dist/framework/defineEntity.d.ts +4 -3
  16. package/dist/framework/defineEntity.js +1 -0
  17. package/dist/framework/registry.d.ts +5 -0
  18. package/dist/framework/registry.js +2 -1
  19. package/dist/framework/rejection.d.ts +4 -2
  20. package/dist/framework/schema-version.d.ts +1 -1
  21. package/dist/framework/schema-version.js +5 -5
  22. package/dist/framework/source-integrity.d.ts +9 -2
  23. package/dist/framework/source-integrity.js +21 -2
  24. package/dist/framework/utils/refs.d.ts +14 -2
  25. package/dist/framework/utils/refs.js +22 -3
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +4 -4
  28. package/dist/migrations/20261013182150_link_exclude_op.d.ts +13 -0
  29. package/dist/migrations/20261013182150_link_exclude_op.js +10 -0
  30. package/dist/migrations/20261013201548_font_faces.d.ts +9 -0
  31. package/dist/migrations/20261013201548_font_faces.js +10 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -2
@@ -48,6 +48,7 @@ declare const ReactNativeSystemDefinition: ConfigClass<{
48
48
  }, _$zod_v4_core0.$strict>]>>;
49
49
  scope: _$zod.ZodOptional<_$zod.ZodString>;
50
50
  preflight: _$zod.ZodOptional<_$zod.ZodBoolean>;
51
+ fontFaces: _$zod.ZodOptional<_$zod.ZodBoolean>;
51
52
  rem: _$zod.ZodOptional<_$zod.ZodString>;
52
53
  content: _$zod.ZodOptional<_$zod.ZodArray<_$zod.ZodString>>;
53
54
  safelist: _$zod.ZodOptional<_$zod.ZodType<SafelistEntry[], readonly SafelistEntry[], _$zod_v4_core0.$ZodTypeInternals<SafelistEntry[], readonly SafelistEntry[]>>>;
@@ -68,6 +68,8 @@ import { linkOverrideRefMigration } from "../migrations/20261009193004_link_over
68
68
  import { linkedComponentModulesMigration } from "../migrations/20261012200606_linked_component_modules.js";
69
69
  import { componentStatesMigration } from "../migrations/20261013154037_component_states.js";
70
70
  import { tokenOverridesListMigration } from "../migrations/20261013164300_token_overrides_list.js";
71
+ import { linkExcludeOpMigration } from "../migrations/20261013182150_link_exclude_op.js";
72
+ import { fontFacesMigration } from "../migrations/20261013201548_font_faces.js";
71
73
  const System = defineConfig({
72
74
  name: WEB_CONFIG_NAME,
73
75
  description: "A design system: the values, primitives and compositions an app is built from. A ref is written `{ \"__ref\": \"<kind>:<path>\" }`, so a token is `{ \"__ref\": \"token:<group>/<name>\" }` and a token group is `{ \"__ref\": \"token:<group>\" }`. Build it in this order. Tokens first, in groups by purpose (colors, spacing, radii, type sizes), since everything else names them. Then style properties, each with `values` holding one ref to the token group whose members it accepts (`{ \"__ref\": \"token:color\" }`), so a prop takes token names rather than raw CSS; add a string keyword only for a value no token holds. A style rule's bag holds a prop's values (`\"padding\": \"4\"`, `\"bg\": \"brand\"`), never raw CSS. Then a few primitive components, one layer each, whose props are style properties (a box for layout and surface, a text for type, a pressable for interaction). Then every other component composed from those primitives: its layers name a primitive or an element, its anatomy places them (a node renders a layer, a tag or `Slot` where a slot prop's content lands; `children` is the edge list; `visible` is `{ \"$state\": \"<prop>\" }`; a prop shown in a nested layer is a `forward` route, and `$state` never sits in a node's props), and its style rules set style-property values per layer, keyed by prop values when a variant changes them. Modes (a dark mode is `{ type: \"selector\", value: \".dark\" }`) change token values through a token's `overrides`, so a component never restyles for a mode; modifiers (hover, focus, disabled) name a state, and a style rule applies under one as a `_hover` block inside a layer bag. Composites bundle style values that recur. Last, register canvas roles: the canvas draws with them, one entity per tool (`frame` for the layout primitive, `text` for the text primitive, `image`) whose entries name the components that tool inserts, and a tool whose role is unregistered is disabled. Give each component a `preview` entry under `*` so it shows content, and never write a CSS property name where a style-property name belongs.",
@@ -147,5 +149,7 @@ if (!registeredSchemaMigrations().has(20261009193004)) registerSchemaMigrations(
147
149
  if (!registeredSchemaMigrations().has(20261012200606)) registerSchemaMigrations(linkedComponentModulesMigration);
148
150
  if (!registeredSchemaMigrations().has(20261013154037)) registerSchemaMigrations(componentStatesMigration);
149
151
  if (!registeredSchemaMigrations().has(20261013164300)) registerSchemaMigrations(tokenOverridesListMigration);
152
+ if (!registeredSchemaMigrations().has(20261013182150)) registerSchemaMigrations(linkExcludeOpMigration);
153
+ if (!registeredSchemaMigrations().has(20261013201548)) registerSchemaMigrations(fontFacesMigration);
150
154
  //#endregion
151
155
  export { System };
@@ -39,6 +39,7 @@ declare const NativeSettings: EntityClass<z.ZodObject<Omit<{
39
39
  }, z.core.$strict>]>>;
40
40
  scope: z.ZodOptional<z.ZodString>;
41
41
  preflight: z.ZodOptional<z.ZodBoolean>;
42
+ fontFaces: z.ZodOptional<z.ZodBoolean>;
42
43
  rem: z.ZodOptional<z.ZodString>;
43
44
  content: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
45
  safelist: z.ZodOptional<z.ZodType<SafelistEntry[], readonly SafelistEntry[], z.core.$ZodTypeInternals<SafelistEntry[], readonly SafelistEntry[]>>>;
@@ -14,6 +14,9 @@ import { z } from "zod";
14
14
  * addresses the FAMILY and lets the browser pick by weight/style — but a native platform references
15
15
  * a file directly, and it can't be derived from the family name. Optional because nothing that feeds
16
16
  * a config records one today; it's author/upload-supplied.
17
+ *
18
+ * `weight` and `style` are checked as invariants rather than shapes, so an edit with a value
19
+ * `@font-face` doesn't accept is refused while a stored config holding one still loads.
17
20
  */
18
21
  declare const FontFileDefinition: SubEntityClass<z.ZodObject<{
19
22
  src: z.ZodString;
@@ -22,6 +25,22 @@ declare const FontFileDefinition: SubEntityClass<z.ZodObject<{
22
25
  style: z.ZodOptional<z.ZodString>;
23
26
  postscriptName: z.ZodOptional<z.ZodString>;
24
27
  }, z.core.$strip>>;
28
+ /** Whether a value is one the `@font-face` `font-weight` descriptor accepts. `bolder` and `lighter`
29
+ * are relative to a parent and mean nothing on a face. */
30
+ declare function isFontFaceWeight(value: number | string): boolean;
31
+ /** Whether a value is one the `@font-face` `font-style` descriptor accepts. */
32
+ declare function isFontFaceStyle(value: string): boolean;
33
+ /**
34
+ * A file's weight and style as `@font-face` descriptor values. A stored config can still hold a value
35
+ * the schema now refuses, so one the descriptor doesn't accept is left out rather than written.
36
+ */
37
+ declare function fontFileDescriptors(file: {
38
+ readonly weight?: number | string;
39
+ readonly style?: string;
40
+ }): {
41
+ readonly weight?: string;
42
+ readonly style?: string;
43
+ };
25
44
  interface FontFileClass extends Named<typeof FontFileDefinition> {}
26
45
  declare const FontFile: FontFileClass;
27
46
  /**
@@ -74,4 +93,4 @@ declare function fontFamilyStack(font: {
74
93
  readonly variable?: string | undefined;
75
94
  }): string;
76
95
  //#endregion
77
- export { Font, FontClass, FontFile, FontFileClass, cssString, fontFamilyStack };
96
+ export { Font, FontClass, FontFile, FontFileClass, cssString, fontFamilyStack, fontFileDescriptors, isFontFaceStyle, isFontFaceWeight };
@@ -1,7 +1,22 @@
1
1
  import { defineEntity } from "../../framework/defineEntity.js";
2
2
  import { defineSubEntity } from "../../framework/defineSubEntity.js";
3
3
  import { z } from "zod";
4
- const FontFile = defineSubEntity({
4
+ //#region src/entities/system/Font.ts
5
+ /**
6
+ * One FILE of a font family — the unit `@font-face` needs a rule for. Named `files` rather than
7
+ * `weights` because the two don't line up: a roman and an italic at 400 are two files sharing one
8
+ * weight, and a variable font is one file covering many (`weight: '100 900'`). `weightCount` is the
9
+ * derivation over these, not their count.
10
+ *
11
+ * `postscriptName` names this exact file (`YahooSans-BoldItalic`). CSS never needs it — a stylesheet
12
+ * addresses the FAMILY and lets the browser pick by weight/style — but a native platform references
13
+ * a file directly, and it can't be derived from the family name. Optional because nothing that feeds
14
+ * a config records one today; it's author/upload-supplied.
15
+ *
16
+ * `weight` and `style` are checked as invariants rather than shapes, so an edit with a value
17
+ * `@font-face` doesn't accept is refused while a stored config holding one still loads.
18
+ */
19
+ const FontFileDefinition = defineSubEntity({
5
20
  name: "file",
6
21
  label: "Font file",
7
22
  labelPlural: "Files",
@@ -9,11 +24,42 @@ const FontFile = defineSubEntity({
9
24
  src: z.string(),
10
25
  format: z.string().optional(),
11
26
  /** `400`, or a RANGE (`'100 900'`) for a variable font's single file. */
12
- weight: z.union([z.number(), z.string()]).optional(),
13
- style: z.string().optional(),
27
+ weight: z.union([z.number(), z.string()]).refine(isFontFaceWeight, { error: "an `@font-face` font-weight: `auto`, `normal`, `bold`, or a number from 1 to 1000, or two of those as a range (`100 900`)." }).optional(),
28
+ style: z.string().refine(isFontFaceStyle, { error: "an `@font-face` font-style: `auto`, `normal`, `italic`, or `oblique` with up to two angles (`oblique 10deg`)." }).optional(),
14
29
  postscriptName: z.string().optional()
15
30
  })
16
31
  });
32
+ const FONT_WEIGHT_PART = /^(normal|bold|\d+(\.\d+)?)$/i;
33
+ const FONT_STYLE = /^(auto|normal|italic|oblique(\s+-?\d+(\.\d+)?deg){0,2})$/i;
34
+ /** Whether a value is one the `@font-face` `font-weight` descriptor accepts. `bolder` and `lighter`
35
+ * are relative to a parent and mean nothing on a face. */
36
+ function isFontFaceWeight(value) {
37
+ const text = String(value).trim();
38
+ if (/^auto$/i.test(text)) return true;
39
+ const parts = text.split(/\s+/);
40
+ return parts.length <= 2 && parts.every((part) => {
41
+ if (!FONT_WEIGHT_PART.test(part)) return false;
42
+ const number = Number(part);
43
+ return Number.isNaN(number) || number >= 1 && number <= 1e3;
44
+ });
45
+ }
46
+ /** Whether a value is one the `@font-face` `font-style` descriptor accepts. */
47
+ function isFontFaceStyle(value) {
48
+ return FONT_STYLE.test(value.trim());
49
+ }
50
+ /**
51
+ * A file's weight and style as `@font-face` descriptor values. A stored config can still hold a value
52
+ * the schema now refuses, so one the descriptor doesn't accept is left out rather than written.
53
+ */
54
+ function fontFileDescriptors(file) {
55
+ const weight = file.weight === void 0 ? void 0 : String(file.weight).trim();
56
+ const style = file.style?.trim();
57
+ return {
58
+ ...weight !== void 0 && isFontFaceWeight(weight) ? { weight } : {},
59
+ ...style !== void 0 && isFontFaceStyle(style) ? { style } : {}
60
+ };
61
+ }
62
+ const FontFile = FontFileDefinition;
17
63
  const Font = defineEntity({
18
64
  kind: "font",
19
65
  label: "Font",
@@ -73,4 +119,4 @@ function fontFamilyStack(font) {
73
119
  return font.variable ? `var(${font.variable}, ${named})` : named;
74
120
  }
75
121
  //#endregion
76
- export { Font, FontFile, cssString, fontFamilyStack };
122
+ export { Font, FontFile, cssString, fontFamilyStack, fontFileDescriptors, isFontFaceStyle, isFontFaceWeight };
@@ -143,6 +143,18 @@ declare const LinkedSystemDefinition: EntityClass<z.ZodObject<{
143
143
  data: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodArray<z.ZodString>]>>]>>;
144
144
  }, z.core.$strip>;
145
145
  };
146
+ readonly exclude: {
147
+ readonly input: z.ZodObject<{
148
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
149
+ data: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
150
+ }, z.core.$strip>;
151
+ };
152
+ readonly 'exclude-plan': {
153
+ readonly input: z.ZodObject<{
154
+ path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
155
+ data: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
156
+ }, z.core.$strip>;
157
+ };
146
158
  readonly break: {
147
159
  readonly input: z.ZodObject<{
148
160
  path: z.ZodType<string, string | undefined, z.core.$ZodTypeInternals<string, string | undefined>>;
@@ -6,8 +6,9 @@ import { kindBorrowable } from "../../framework/registry.js";
6
6
  import { defineEntity } from "../../framework/defineEntity.js";
7
7
  import { storedStyleRules } from "./Component.js";
8
8
  import { sourceUnreachable } from "../../framework/sources.js";
9
+ import { covers, excludes } from "./link-declaration.js";
9
10
  import { planCopy } from "./copy-plan.js";
10
- import { planBreak, planLink, planUnlink, sourceRowStates } from "./link-plan.js";
11
+ import { planBreak, planExclude, planLink, planUnlink, sourceRowStates } from "./link-plan.js";
11
12
  import { z } from "zod";
12
13
  //#region src/entities/system/LinkedSystem.ts
13
14
  /**
@@ -177,6 +178,12 @@ const unlinkInput = z.object({
177
178
  /** What to stop drawing, in `link`'s shape; everything when omitted. */
178
179
  data: z.union([z.literal("*"), z.record(z.string(), copyPaths)]).optional()
179
180
  });
181
+ const excludeInput = z.object({
182
+ path: pathInput("linked-system"),
183
+ /** What to leave out, keyed by accessor the way the pin's `link` is: the source's paths, where a
184
+ * folder leaves out its members. */
185
+ data: z.record(z.string(), z.array(z.string().min(1)).min(1))
186
+ });
180
187
  const breakInput = z.object({
181
188
  path: pathInput("linked-system"),
182
189
  /** Whether every value drawn through the pin becomes this config's own first. */
@@ -197,6 +204,13 @@ function copyInputOf(input) {
197
204
  ...input.onConflict === void 0 ? {} : { onConflict: input.onConflict }
198
205
  };
199
206
  }
207
+ /** The parsed `exclude` input as the plan takes it. */
208
+ function excludePlanInputOf(input) {
209
+ return {
210
+ path: String(input.path),
211
+ data: input.data
212
+ };
213
+ }
200
214
  /** The parsed `link` input as the plan takes it. */
201
215
  function linkPlanInputOf(input) {
202
216
  return {
@@ -270,7 +284,11 @@ const LinkedSystemDefinition = defineEntity({
270
284
  kind: "linked-system",
271
285
  label: "Linked system",
272
286
  labelPlural: "Linked systems",
273
- invariants: (config) => [...config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`), ...linkRedirects(config).filter((redirect) => redirect.problem !== void 0 && redirect.problem !== "missing").map(redirectSentence)],
287
+ invariants: (config) => [
288
+ ...config.list("linked-system").filter(({ path }) => !isLinkSlug(path)).map(({ path }) => `Link "${path}": ${LINK_SLUG_MESSAGE}`),
289
+ ...linkRedirects(config).filter((redirect) => redirect.problem !== void 0 && redirect.problem !== "missing").map(redirectSentence),
290
+ ...excludedDirectives(config)
291
+ ],
274
292
  schemas: {
275
293
  create: { example: ({ config }) => {
276
294
  const path = freeExampleSlug(config);
@@ -372,6 +390,45 @@ const LinkedSystemDefinition = defineEntity({
372
390
  }
373
391
  },
374
392
  /**
393
+ * Leave part of a linked system out for good: a `!` entry per path in the pin's `link`. Unlike
394
+ * `unlink`, nothing derives there however the link reaches it, and the linked tokens' overrides
395
+ * under an excluded modifier are left out of the build. `link` with the same path lifts it.
396
+ */
397
+ exclude: {
398
+ input: excludeInput,
399
+ readOnly: false,
400
+ label: "Exclude from",
401
+ description: "Leave paths of a linked system out, keyed by accessor the way `link` is: `{ modifiers: ['text'] }` leaves out the `text` folder's members. Stronger than `unlink`: nothing derives at an excluded path however the link reaches it, even when a linked token's override names it, and the source's token overrides under an excluded modifier are left out of the build. The pin's `options` entries at those paths go with them. `exclude-plan` names what goes; `link` with the same path brings it back.",
402
+ scope: "item",
403
+ approval: "reference-change",
404
+ title: (entity) => `Exclude something from ${entity}`,
405
+ example: linkExample,
406
+ handler: (input, config) => {
407
+ const { patches, blocked } = planExclude({
408
+ config,
409
+ input: excludePlanInputOf(input)
410
+ });
411
+ if (blocked !== void 0) rejectData(blocked);
412
+ return config.applyAll(patches);
413
+ }
414
+ },
415
+ "exclude-plan": {
416
+ input: excludeInput,
417
+ readOnly: true,
418
+ label: "Plan an exclusion",
419
+ description: "What `exclude` with the same input leaves out — the minted rows that stop deriving, the linked tokens' overrides under an excluded modifier (`leftOut`), and the pin's `options` entries that go with the paths (`dropped`) — and why the config would refuse it. Writes nothing.",
420
+ scope: "item",
421
+ title: (entity) => `Plan an exclusion from ${entity}`,
422
+ example: linkExample,
423
+ handler: (input, config) => {
424
+ const { patches: _patches, ...plan } = planExclude({
425
+ config,
426
+ input: excludePlanInputOf(input)
427
+ });
428
+ return plan;
429
+ }
430
+ },
431
+ /**
375
432
  * Break the link: the pin goes, and everything drawn through it either becomes this config's own
376
433
  * or goes with it.
377
434
  */
@@ -541,6 +598,7 @@ const LinkedSystemDefinition = defineEntity({
541
598
  * link: { tokens: '*', icons: '*' } // those kinds whole
542
599
  * link: { tokens: ['color/brand', 'color/paper'] } // those entities
543
600
  * link: { components: ['actions'] } // a folder: its members
601
+ * link: { '*': '*', modifiers: ['*', '!text'] } // everything but one folder
544
602
  * ```
545
603
  * `Config.withDerivedRows` reads it and makes a pointer row per entity it covers — on every build,
546
604
  * and inside the apply of any patch to this pin — so a link's cost in authored source is this one
@@ -566,14 +624,38 @@ const LinkedSystemDefinition = defineEntity({
566
624
  * build would make it again on the next pass, and a delete that comes back is worse than one that
567
625
  * was refused. To own one of their entities, `copy` it; to stop drawing it, `unlink` it.
568
626
  *
569
- * The gestures that write this are the kind's own ops — `link`, `unlink`, `break` — each with a
570
- * `-plan` read beside it, so a surface never assembles the declaration by hand.
627
+ * The gestures that write this are the kind's own ops — `link`, `unlink`, `exclude`, `break` —
628
+ * each with a `-plan` read beside it, so a surface never assembles the declaration by hand.
571
629
  *
572
630
  * `'*'` means every kind that may be drawn on, which is every kind that may be borrowed — each
573
631
  * kind says which it is (`EntityKind.linkable`). Nothing is subtracted: a pin and a `file` aren't
574
632
  * borrowable, so no borrow of one exists for this to cover.
633
+ *
634
+ * **Leaving part of a kind out.** A record's `'*'` key stands for every accessor it doesn't name,
635
+ * and a list entry `'*'` for every path, so everything but one folder is spelled
636
+ * ```ts
637
+ * link: { '*': '*', modifiers: ['*', '!text'] }
638
+ * ```
639
+ * A `!` entry leaves that path out, or a folder's members, whatever else in the list reaches it.
640
+ * Unlike a path a list doesn't name, an excluded path derives no row however the link reaches it —
641
+ * a linked token whose override is under a `text` modifier names that modifier, and it still
642
+ * stays out — so the name is free for this config to author, and the source's entity stays
643
+ * reachable qualified. A token's overrides under an excluded modifier are left out of the build
644
+ * rather than refused (`unstatableConditions`), which is what a consumer without one of the
645
+ * source's mode axes means. A linked component's rule keyed on one still reaches the source's
646
+ * modifier, as a rule on any modifier the link doesn't take does. See `covers` and `excludes`.
647
+ *
648
+ * Exclusions win wherever they sit, so `['!text', '*']` is `['*', '!text']`, and a list of them
649
+ * alone (`['!text']`) takes nothing by name and still keeps `text` out. An `options` entry at an
650
+ * excluded path is refused (an invariant), since no row derives there for it to adjust. `exclude`
651
+ * writes one (and drops such entries with it), and `link` lifts one over what it takes. `unlink`
652
+ * keeps them and writes none, because a `!` also drops the overrides under the path, which
653
+ * unlinking never has.
654
+ *
655
+ * No entity path can be misread here: `entityPathProblem` refuses a segment that starts with `!`,
656
+ * a segment that is `*`, and an empty segment, wherever a path becomes an identity.
575
657
  */
576
- link: z.union([z.literal("*"), z.record(z.string(), z.union([z.literal("*"), z.array(z.string().min(1)).min(1)]))]).optional().describe("What this system draws from the source, standing: `'*'` for everything, or a record by kind."),
658
+ link: z.union([z.literal("*"), z.record(z.string(), z.union([z.literal("*"), z.array(z.string().min(1)).min(1)]))]).optional().describe("What this system draws from the source, standing: `'*'` for everything, or a record by kind (`'*'` keys every kind it doesn't name). A list's `'!path'` entry leaves that path out."),
577
659
  /**
578
660
  * What this system does to individual entities of the source, keyed by accessor, then by the path
579
661
  * the source spells, then by verb.
@@ -715,20 +797,22 @@ displayName: ({ member, path }) => typeof member.name === "string" && member.nam
715
797
  * question and the delete gate's, answered here because `link` is this kind's field and its shape
716
798
  * is this kind's to interpret.
717
799
  *
718
- * `'*'` at either level covers everything under it; a list covers the paths it names, as the source
719
- * spells them, and a named folder covers its members. Nothing excludes a kind here — a kind a
720
- * whole-source take does not reach is one no borrow of it exists for (`EntityKind.linkable`), so
721
- * the claim is unreachable rather than filtered.
800
+ * `'*'` at either level covers everything under it, and a record's `'*'` key stands for every
801
+ * accessor it doesn't name; a list covers the paths it names, as the source spells them, and a named
802
+ * folder covers its members, less what its `!` entries leave out. Nothing else excludes a kind here
803
+ * — a kind a whole-source take does not reach is one no borrow of it exists for
804
+ * (`EntityKind.linkable`), so the claim is unreachable rather than filtered.
722
805
  */
723
- bulkAdopts: ({ body, kind, accessor, path }) => {
724
- const declared = body.link;
725
- if (declared === void 0) return false;
726
- if (declared === "*") return true;
727
- const paths = declared[accessor ?? kind];
728
- if (paths === void 0) return false;
729
- if (paths === "*") return true;
730
- return paths.some((declaredPath) => declaredPath === path || path.startsWith(`${declaredPath}/`));
731
- },
806
+ bulkAdopts: ({ body, kind, accessor, path }) => covers({
807
+ declared: body.link,
808
+ accessor: accessor ?? kind,
809
+ path
810
+ }),
811
+ bulkExcludes: ({ body, kind, accessor, path }) => excludes({
812
+ declared: body.link,
813
+ accessor: accessor ?? kind,
814
+ path
815
+ }),
732
816
  bulkOverlay: ({ body, kind, accessor, path }) => {
733
817
  const directive = directiveAt(body, accessor ?? kind, path);
734
818
  if (!directive) return void 0;
@@ -794,6 +878,18 @@ function linkedSystemItem(def) {
794
878
  };
795
879
  }
796
880
  const LinkedSystem = Object.assign(LinkedSystemDefinition, { item: linkedSystemItem });
881
+ /** Each `options` entry at a path its pin's `link` excludes, which derives no row for it to adjust. */
882
+ function excludedDirectives(config) {
883
+ return config.list("linked-system").flatMap(({ path: slug }) => {
884
+ const body = config.storedBody("linked-system", slug);
885
+ if (!body?.options) return [];
886
+ return Object.entries(body.options).flatMap(([accessor, entries]) => isPlainObject(entries) ? Object.keys(entries).filter((path) => excludes({
887
+ declared: body.link,
888
+ accessor,
889
+ path
890
+ })).map((path) => `Link "${slug}": options.${accessor}["${path}"] adjusts a path its \`link\` excludes, which derives no row. Drop the entry, or the exclusion.`) : []);
891
+ });
892
+ }
797
893
  /** The `options` entry for one path, or `undefined` where the pin says nothing about it. */
798
894
  function directiveAt(body, accessor, path) {
799
895
  return body.options?.[accessor]?.[path];
@@ -41,6 +41,7 @@ type CssPrefix = z.output<typeof prefix>;
41
41
  declare const CSS_DEFAULTS: {
42
42
  readonly prefix: "uds";
43
43
  readonly preflight: true;
44
+ readonly fontFaces: true;
44
45
  readonly rem: "16px";
45
46
  };
46
47
  /** The `css` section — how a web system names what it emits and where its stylesheet applies. A
@@ -52,6 +53,7 @@ declare const CssSectionDefinition: SubEntityClass<z.ZodObject<{
52
53
  }, z.core.$strict>]>>;
53
54
  scope: z.ZodOptional<z.ZodString>;
54
55
  preflight: z.ZodOptional<z.ZodBoolean>;
56
+ fontFaces: z.ZodOptional<z.ZodBoolean>;
55
57
  rem: z.ZodOptional<z.ZodString>;
56
58
  content: z.ZodOptional<z.ZodArray<z.ZodString>>;
57
59
  safelist: z.ZodOptional<z.ZodType<SafelistEntry[], readonly SafelistEntry[], z.core.$ZodTypeInternals<SafelistEntry[], readonly SafelistEntry[]>>>;
@@ -115,6 +117,7 @@ declare const SettingsDefinition: EntityClass<z.ZodObject<{
115
117
  }, z.core.$strict>]>>;
116
118
  scope: z.ZodOptional<z.ZodString>;
117
119
  preflight: z.ZodOptional<z.ZodBoolean>;
120
+ fontFaces: z.ZodOptional<z.ZodBoolean>;
118
121
  rem: z.ZodOptional<z.ZodString>;
119
122
  content: z.ZodOptional<z.ZodArray<z.ZodString>>;
120
123
  safelist: z.ZodOptional<z.ZodType<SafelistEntry[], readonly SafelistEntry[], z.core.$ZodTypeInternals<SafelistEntry[], readonly SafelistEntry[]>>>;
@@ -64,6 +64,7 @@ const prefix = z.union([z.string(), z.strictObject({
64
64
  const CSS_DEFAULTS = {
65
65
  prefix: "uds",
66
66
  preflight: true,
67
+ fontFaces: true,
67
68
  rem: "16px"
68
69
  };
69
70
  const cssFields = z.strictObject({
@@ -84,6 +85,12 @@ const cssFields = z.strictObject({
84
85
  description: "Start the stylesheet with the element resets, so buttons, inputs and headings begin from the same baseline in every browser. Off, the page keeps its own base styles. Unset, on.",
85
86
  default: CSS_DEFAULTS.preflight
86
87
  }).optional(),
88
+ /** Whether the app's stylesheet carries an `@font-face` rule for each file its fonts declare. */
89
+ fontFaces: z.boolean().meta({
90
+ title: "Font faces",
91
+ description: "Write an `@font-face` rule into the stylesheet for each file a font declares, so a `font-family` that names it loads that file. Off, the app loads its fonts itself, as with `next/font` or its own `@font-face` rules; Create's canvas still paints them. Unset, on.",
92
+ default: CSS_DEFAULTS.fontFaces
93
+ }).optional(),
87
94
  /** The root font size of the page this sheet is for, as CSS — what one `rem` is. Stated, the app's
88
95
  * sheet sets it and a token's `px()` reads a `rem` value against it; a fixed size (`16px`, `62.5%`,
89
96
  * `1em`) folds to pixels against the browser's 16px default, a fluid one (`clamp(…)`) leaves a
@@ -3,19 +3,60 @@ import type { LinkDeclaration } from './LinkedSystem';
3
3
  export declare const PIN_KIND = "linked-system";
4
4
  /** The pin's `link` as stored. */
5
5
  export declare function declarationOf(config: Config, slug: string): LinkDeclaration | undefined;
6
- /** Whether a declaration covers the accessor's path: the whole source, the kind, the path, or a folder above it. */
6
+ /**
7
+ * What a declaration says about one accessor: its own entry, or the record's `'*'` entry, which stands
8
+ * for every accessor the record doesn't name (`{ '*': '*', modifiers: ['*', '!text'] }`).
9
+ */
10
+ export declare function entriesFor(declared: LinkDeclaration | undefined, accessor: string): '*' | readonly string[] | undefined;
11
+ /** The paths a list leaves out, without their `!`. */
12
+ export declare function excludedIn(entries: '*' | readonly string[] | undefined): string[];
13
+ /**
14
+ * Whether a declaration leaves the accessor's path out on purpose: a `!` entry names it or a folder
15
+ * above it. An excluded path derives no row however the link reaches it, a ref pulling it in included.
16
+ */
17
+ export declare function excludes({ declared, accessor, path, }: {
18
+ declared: LinkDeclaration | undefined;
19
+ accessor: string;
20
+ path: string;
21
+ }): boolean;
22
+ /**
23
+ * Whether a declaration covers the accessor's path: the whole source, the kind, the path, or a folder
24
+ * above it, and no `!` entry leaves it out. An exclusion wins whatever else in the list reaches the
25
+ * path, so `['*', '!text']` is every path but `text`'s.
26
+ */
7
27
  export declare function covers({ declared, accessor, path, }: {
8
28
  declared: LinkDeclaration | undefined;
9
29
  accessor: string;
10
30
  path: string;
11
31
  }): boolean;
32
+ /**
33
+ * A list with `paths` taken into it: each is added unless a path the list takes already reaches it,
34
+ * and an exclusion that would leave one out is lifted. One naming the path or a folder under it goes;
35
+ * one naming a folder above it is spelled out as the folder's other members, so they stay out.
36
+ */
37
+ export declare function includePaths({ config, slug, kind, entries, paths, }: {
38
+ config: Config;
39
+ slug: string;
40
+ kind: string;
41
+ entries: readonly string[];
42
+ paths: readonly string[];
43
+ }): string[];
44
+ /**
45
+ * A list with `paths` left out: a `!` entry for each the list doesn't exclude already. A path the
46
+ * list names under one of them goes, since the exclusion now says it, and so does a narrower
47
+ * exclusion a new one reaches. A folder named above an excluded path stays, less that path. With
48
+ * nothing listed, the list is the exclusions alone, which still keep the paths out when a linked row
49
+ * reaches them.
50
+ */
51
+ export declare function excludePaths(entries: '*' | readonly string[] | undefined, paths: readonly string[]): string[];
12
52
  /**
13
53
  * The declaration with `removal` taken out of it, or `undefined` when nothing stays declared.
14
54
  *
15
55
  * A declaration of everything is spelled out first, one kind per accessor the source has rows for, so
16
56
  * one kind can be taken out of it; a kind taken whole is spelled out as the source's top-level paths
17
57
  * so some can go; a declared folder above a removed path is spelled out as its other members, so the
18
- * rest of it stays covered.
58
+ * rest of it stays covered. A list that takes every path (`['*', …]`) is spelled out the same way.
59
+ * Its `!` entries stay, a list of them alone included, and narrowing writes none.
19
60
  */
20
61
  export declare function narrowDeclaration({ config, slug, current, removal, verb, }: {
21
62
  config: Config;
@@ -7,14 +7,90 @@ function declarationOf(config, slug) {
7
7
  if (!pin) rejectData(`No linked system "${slug}".`);
8
8
  return pin.link;
9
9
  }
10
- /** Whether a declaration covers the accessor's path: the whole source, the kind, the path, or a folder above it. */
10
+ /** An entry that leaves a path out: `'!text'`. */
11
+ const EXCLUDE = "!";
12
+ /** Whether a declared entry names `path` or a folder above it. */
13
+ const reaches = (entry, path) => entry === "*" || entry === path || path.startsWith(`${entry}/`);
14
+ /**
15
+ * What a declaration says about one accessor: its own entry, or the record's `'*'` entry, which stands
16
+ * for every accessor the record doesn't name (`{ '*': '*', modifiers: ['*', '!text'] }`).
17
+ */
18
+ function entriesFor(declared, accessor) {
19
+ if (declared === void 0 || declared === "*") return declared;
20
+ return declared[accessor] ?? declared["*"];
21
+ }
22
+ /** The paths a list leaves out, without their `!`. */
23
+ function excludedIn(entries) {
24
+ if (entries === void 0 || entries === "*") return [];
25
+ return entries.filter((entry) => entry.startsWith(EXCLUDE)).map((entry) => entry.slice(1));
26
+ }
27
+ /**
28
+ * Whether a declaration leaves the accessor's path out on purpose: a `!` entry names it or a folder
29
+ * above it. An excluded path derives no row however the link reaches it, a ref pulling it in included.
30
+ */
31
+ function excludes({ declared, accessor, path }) {
32
+ return excludedIn(entriesFor(declared, accessor)).some((excluded) => reaches(excluded, path));
33
+ }
34
+ /**
35
+ * Whether a declaration covers the accessor's path: the whole source, the kind, the path, or a folder
36
+ * above it, and no `!` entry leaves it out. An exclusion wins whatever else in the list reaches the
37
+ * path, so `['*', '!text']` is every path but `text`'s.
38
+ */
11
39
  function covers({ declared, accessor, path }) {
12
- if (declared === void 0) return false;
13
- if (declared === "*") return true;
14
- const paths = declared[accessor];
15
- if (paths === void 0) return false;
16
- if (paths === "*") return true;
17
- return paths.some((declaredPath) => declaredPath === path || path.startsWith(`${declaredPath}/`));
40
+ const entries = entriesFor(declared, accessor);
41
+ if (entries === void 0) return false;
42
+ if (entries === "*") return true;
43
+ if (excludes({
44
+ declared,
45
+ accessor,
46
+ path
47
+ })) return false;
48
+ return entries.some((entry) => !entry.startsWith(EXCLUDE) && reaches(entry, path));
49
+ }
50
+ /**
51
+ * A list with `paths` taken into it: each is added unless a path the list takes already reaches it,
52
+ * and an exclusion that would leave one out is lifted. One naming the path or a folder under it goes;
53
+ * one naming a folder above it is spelled out as the folder's other members, so they stay out.
54
+ */
55
+ function includePaths({ config, slug, kind, entries, paths }) {
56
+ const source = config.sourceFor(slug);
57
+ const included = [];
58
+ const exclusions = [];
59
+ for (const entry of entries) {
60
+ if (!entry.startsWith(EXCLUDE)) {
61
+ included.push(entry);
62
+ continue;
63
+ }
64
+ const excluded = entry.slice(1);
65
+ if (paths.some((path) => reaches(path, excluded))) continue;
66
+ if (!paths.some((path) => reaches(excluded, path))) {
67
+ exclusions.push(entry);
68
+ continue;
69
+ }
70
+ for (const member of source?.members(kind, excluded) ?? []) if (!paths.some((path) => reaches(path, member.path))) exclusions.push(`${EXCLUDE}${member.path}`);
71
+ }
72
+ for (const path of paths) {
73
+ if (included.some((entry) => reaches(entry, path))) continue;
74
+ included.push(path);
75
+ }
76
+ return [...included, ...exclusions];
77
+ }
78
+ /**
79
+ * A list with `paths` left out: a `!` entry for each the list doesn't exclude already. A path the
80
+ * list names under one of them goes, since the exclusion now says it, and so does a narrower
81
+ * exclusion a new one reaches. A folder named above an excluded path stays, less that path. With
82
+ * nothing listed, the list is the exclusions alone, which still keep the paths out when a linked row
83
+ * reaches them.
84
+ */
85
+ function excludePaths(entries, paths) {
86
+ const list = entries === void 0 ? [] : entries === "*" ? ["*"] : entries;
87
+ const excluded = excludedIn(list);
88
+ const added = [...new Set(paths)].filter((path) => !excluded.some((already) => reaches(already, path)));
89
+ return [...list.filter((entry) => {
90
+ if (entry === "*") return true;
91
+ const path = entry.startsWith(EXCLUDE) ? entry.slice(1) : entry;
92
+ return !added.some((adding) => reaches(adding, path));
93
+ }), ...added.map((path) => `${EXCLUDE}${path}`)];
18
94
  }
19
95
  /**
20
96
  * The declaration with `removal` taken out of it, or `undefined` when nothing stays declared.
@@ -22,7 +98,8 @@ function covers({ declared, accessor, path }) {
22
98
  * A declaration of everything is spelled out first, one kind per accessor the source has rows for, so
23
99
  * one kind can be taken out of it; a kind taken whole is spelled out as the source's top-level paths
24
100
  * so some can go; a declared folder above a removed path is spelled out as its other members, so the
25
- * rest of it stays covered.
101
+ * rest of it stays covered. A list that takes every path (`['*', …]`) is spelled out the same way.
102
+ * Its `!` entries stay, a list of them alone included, and narrowing writes none.
26
103
  */
27
104
  function narrowDeclaration({ config, slug, current, removal, verb }) {
28
105
  const source = config.sourceFor(slug);
@@ -31,7 +108,18 @@ function narrowDeclaration({ config, slug, current, removal, verb }) {
31
108
  if (source && source.list(kind).length === 0) continue;
32
109
  next[config.accessorOf(kind) ?? kind] = "*";
33
110
  }
34
- else Object.assign(next, current);
111
+ else {
112
+ Object.assign(next, current);
113
+ const fallback = current["*"];
114
+ if (fallback !== void 0) {
115
+ delete next["*"];
116
+ for (const kind of config.borrowableKinds(slug)) {
117
+ if (source && source.list(kind).length === 0) continue;
118
+ const accessor = config.accessorOf(kind) ?? kind;
119
+ if (!(accessor in current)) next[accessor] = fallback;
120
+ }
121
+ }
122
+ }
35
123
  for (const [accessor, paths] of removal) {
36
124
  const before = next[accessor];
37
125
  if (before === void 0) continue;
@@ -40,25 +128,32 @@ function narrowDeclaration({ config, slug, current, removal, verb }) {
40
128
  continue;
41
129
  }
42
130
  const kind = config.accessors()[accessor]?.kind ?? accessor;
131
+ const excluded = excludedIn(before);
43
132
  let declaredPaths;
44
- if (before === "*") {
133
+ if (before === "*" || before.includes("*")) {
45
134
  if (!source) rejectData(`Cannot ${verb} ${paths.join(", ")} through "${slug}" — the declaration takes every "${accessor}", and nothing has resolved that link to spell them out. Unlink the kind (\`{ "${accessor}": "*" }\`) instead.`, ["data", accessor]);
46
135
  declaredPaths = source.tree(kind).map((node) => node.path);
47
- } else declaredPaths = before;
136
+ } else declaredPaths = before.filter((entry) => !entry.startsWith(EXCLUDE));
48
137
  const removed = (candidate) => paths.some((path) => path === candidate || candidate.startsWith(`${path}/`));
49
138
  const kept = [];
50
139
  for (const declaredPath of declaredPaths) {
51
140
  if (removed(declaredPath)) continue;
141
+ if (excluded.some((path) => reaches(path, declaredPath))) continue;
52
142
  if (!paths.some((path) => path.startsWith(`${declaredPath}/`)) || !source) {
53
143
  kept.push(declaredPath);
54
144
  continue;
55
145
  }
56
- for (const member of source.members(kind, declaredPath)) if (!removed(member.path)) kept.push(member.path);
146
+ for (const member of source.members(kind, declaredPath)) {
147
+ if (removed(member.path)) continue;
148
+ if (excluded.some((path) => reaches(path, member.path))) continue;
149
+ kept.push(member.path);
150
+ }
57
151
  }
58
- if (kept.length === 0) delete next[accessor];
59
- else next[accessor] = kept;
152
+ const exclusions = excluded.map((path) => `${EXCLUDE}${path}`);
153
+ if (kept.length === 0 && exclusions.length === 0) delete next[accessor];
154
+ else next[accessor] = [...kept, ...exclusions];
60
155
  }
61
156
  return Object.keys(next).length === 0 ? void 0 : next;
62
157
  }
63
158
  //#endregion
64
- export { PIN_KIND, covers, declarationOf, narrowDeclaration };
159
+ export { PIN_KIND, covers, declarationOf, entriesFor, excludePaths, excludes, includePaths, narrowDeclaration };