@texuli/spec 0.9.0 → 0.10.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/README.md +33 -11
- package/dist/build.d.ts +5 -0
- package/dist/build.js +1 -0
- package/dist/codegen/kotlin-tokens.d.ts +38 -0
- package/dist/codegen/kotlin-tokens.js +138 -0
- package/dist/codegen/kotlin.d.ts +3 -1
- package/dist/codegen/kotlin.js +5 -2
- package/dist/design-system/index.d.ts +11 -0
- package/dist/design-system/index.js +10 -0
- package/dist/design-system/schema.d.ts +3 -0
- package/dist/design-system/schema.js +164 -0
- package/dist/design-system/validate.d.ts +69 -0
- package/dist/design-system/validate.js +273 -0
- package/dist/design-system/vocabulary.d.ts +70 -0
- package/dist/design-system/vocabulary.js +351 -0
- package/dist/format.js +10 -1
- package/dist/generated/typescript/blueprint.d.ts +67 -11
- package/dist/generated/typescript/blueprint.js +1 -1
- package/dist/generated/typescript/catalog-schema.js +1 -1
- package/dist/generated/typescript/design-system.d.ts +1110 -0
- package/dist/generated/typescript/design-system.js +3 -0
- package/dist/generated/typescript/diagnostics.js +1 -1
- package/dist/generated/typescript/manifest.js +1 -1
- package/dist/generated/typescript/vocabulary.d.ts +3 -1
- package/dist/generated/typescript/vocabulary.js +2 -2
- package/dist/generated/typescript/workflow.js +1 -1
- package/dist/migrations/0.9.0-0.10.0.d.ts +10 -0
- package/dist/migrations/0.9.0-0.10.0.js +13 -0
- package/dist/migrations/index.js +2 -0
- package/dist/validate.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -14,11 +14,19 @@ by Texuli's Kotlin Multiplatform twin. The package is framework-free: no
|
|
|
14
14
|
DOM, no Svelte, no Node APIs and no runtime dependencies.
|
|
15
15
|
|
|
16
16
|
> **Status: 0.x release.** Texuli is in development. The packages are
|
|
17
|
-
> published to npm as `0.
|
|
18
|
-
> stable release). Under 0.x, a new minor (`0.
|
|
19
|
-
> changes, so pin the exact version or use `^0.
|
|
20
|
-
|
|
21
|
-
> **New in `0.
|
|
17
|
+
> published to npm as `0.10.0`, a pre-1.0 release (1.0.0 will be the first
|
|
18
|
+
> stable release). Under 0.x, a new minor (`0.11.0`) may contain breaking
|
|
19
|
+
> changes, so pin the exact version or use `^0.10.0`, which accepts only 0.10.x.
|
|
20
|
+
|
|
21
|
+
> **New in `0.10.0`.** `@texuli/spec@0.10.0` carries **spec 0.10.0**: the
|
|
22
|
+
> design-system document (`schemas/design-system.schema.json`, format
|
|
23
|
+
> `texuli/design-system`) with its new `@texuli/spec/design-system`
|
|
24
|
+
> subpath, and the optional `designSystem` on the envelope and on a
|
|
25
|
+
> `document` (`$defs.designSystemReference`: `{ ref: "id@x.y.z" }` or
|
|
26
|
+
> `{ inline: <document> }`). Every `0.9.0` envelope migrates by stamping
|
|
27
|
+
> `spec`.
|
|
28
|
+
|
|
29
|
+
> **New in `0.9.0`.** `@texuli/spec@0.9.0` carried **spec 0.9.0**, a
|
|
22
30
|
> stamp-only catalog projection refresh (the Button pending note and the
|
|
23
31
|
> cartesian charts' `xKind` and `locale`, from the Protoyard gaps G94 and
|
|
24
32
|
> G97): the envelope, the `$defs` and the diagnostic codes are unchanged,
|
|
@@ -81,7 +89,7 @@ import {
|
|
|
81
89
|
validateInstance,
|
|
82
90
|
} from '@texuli/spec';
|
|
83
91
|
|
|
84
|
-
SPEC_VERSION; // the spec version: '0.
|
|
92
|
+
SPEC_VERSION; // the spec version: '0.10.0' in @texuli/spec@0.10.0
|
|
85
93
|
SPEC_COMPONENT_IDS.includes('alert'); // true: the closed component vocabulary
|
|
86
94
|
|
|
87
95
|
// The dialect validator: a closed subset of JSON Schema 2020-12.
|
|
@@ -113,7 +121,7 @@ import { type Schema, type SpecCatalog, validateBlueprint } from '@texuli/spec';
|
|
|
113
121
|
import { migrateBlueprint } from '@texuli/spec/migrate';
|
|
114
122
|
import { validateWorkflowGraph } from '@texuli/spec/workflow';
|
|
115
123
|
|
|
116
|
-
const migrated = migrateBlueprint(blueprint); // 0.2.0 → … → 0.
|
|
124
|
+
const migrated = migrateBlueprint(blueprint); // 0.2.0 → … → 0.9.0 → 0.10.0; never throws
|
|
117
125
|
const result = validateBlueprint(migrated.blueprint, {
|
|
118
126
|
blueprintSchema: blueprintSchemaJson as Schema,
|
|
119
127
|
catalog: componentsJson as SpecCatalog,
|
|
@@ -152,6 +160,7 @@ warning without failing.
|
|
|
152
160
|
| `@texuli/spec/motion` | Runtime (new in `0.6.0`, spec `0.6.0`): the closed motion vocabulary and its check, shared by the catalog and the player ([motion.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/motion.md)) | `SPEC_MOTION_PRESETS`, `SPEC_MOTION_DURATIONS`, `SPEC_MOTION_STAGGERS`, `SPEC_MOTION_LEVELS`, `checkNodeMotion`, `checkEnvelopeMotion` |
|
|
153
161
|
| `@texuli/spec/stream` | Runtime, opt-in (new in `0.6.0`): streamed blueprints ([stream.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/stream.md)) | `createJsonTokenizer`, `createBlueprintAssembler`, `createBlueprintStream`, `STREAM_NODE_ORDER` |
|
|
154
162
|
| `@texuli/spec/source` | Runtime (new in `0.8.0`, spec `0.8.0`): data-source bindings, shared by the spec validator, `@texuli/catalog` and the player ([source.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/source.md)) | `checkSourceBinding`, `isSourceBinding`, `SOURCE_BINDING_KEY`, `SPEC_SOURCE_LIMITS`, `SPEC_SOURCE_FILTER_OPERATORS`, `SpecSourceDescriptor` |
|
|
163
|
+
| `@texuli/spec/design-system` | Runtime, opt-in (new in `0.10.0`, spec `0.10.0`): the design-system document contract (ADR-021) ([design-system.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/design-system.md)) | `validateDesignSystem`, `validateDesignSystemPatch`, `applyDesignSystemPatch`, `migrateDesignSystem`, `checkDesignSystemReference`, `DesignSystem`, `DesignSystemPatch`, `DESIGN_SYSTEM_CODES`, `DESIGN_SYSTEM_LIMITS` |
|
|
155
164
|
| `@texuli/spec/codegen` | Build time: generation and conformance | `buildSpec`, `projectCatalog`, `checkConformance`, `checkCatalogContract`, `formatJson`, `nextSpecVersion`, `checkVersionHistory` |
|
|
156
165
|
|
|
157
166
|
## Key guarantees
|
|
@@ -222,7 +231,9 @@ warning without failing.
|
|
|
222
231
|
([source.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/source.md)).
|
|
223
232
|
- **Forms as data, never patterns or code (spec 0.8.0, new in `0.8.0`).** The
|
|
224
233
|
agent-emittable `schema-form` id carries a closed form schema
|
|
225
|
-
(`$defs.formSchema`): fields of 15 closed types in sections or steps
|
|
234
|
+
(`$defs.formSchema`): fields of 15 closed types in sections or steps (20
|
|
235
|
+
from `0.10.0`, which adds password, otp, color, datetime and the
|
|
236
|
+
host-painted custom type),
|
|
226
237
|
named formats and host validator names (never a regular expression),
|
|
227
238
|
and conditions and cross-field rules in the closed expression AST of
|
|
228
239
|
`@texuli/core/expression` (`$defs.expression`, the `numberFormula` and
|
|
@@ -280,9 +291,18 @@ written for (`spec`; omitted means 0.2.0). `migrateBlueprint`
|
|
|
280
291
|
(`@texuli/spec/migrate`) brings an
|
|
281
292
|
envelope up to the current spec with pure, per-version steps
|
|
282
293
|
(`src/migrations/`: `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0`, `0.4.0 → 0.5.0`,
|
|
283
|
-
`0.5.0 → 0.6.0`, `0.6.0 → 0.7.0`, `0.7.0 → 0.8.0
|
|
284
|
-
each of which only stamps `spec`); a compatible version
|
|
285
|
-
not built. `@texuli/spec@0.
|
|
294
|
+
`0.5.0 → 0.6.0`, `0.6.0 → 0.7.0`, `0.7.0 → 0.8.0`, `0.8.0 → 0.9.0` and
|
|
295
|
+
`0.9.0 → 0.10.0`, each of which only stamps `spec`); a compatible version
|
|
296
|
+
range is planned, not built. `@texuli/spec@0.10.0` carries spec `0.10.0` (digest
|
|
297
|
+
`afcb9e3ed6b74b833a0f7477a657168621785ee98602ff582aa047e2d86aef17`), which adds the
|
|
298
|
+
design-system document (PLAN_011 T-ds-spec) and the envelope's
|
|
299
|
+
`designSystem` (T-ds-blueprint), plus a catalog projection refresh
|
|
300
|
+
(`data-filter-builder` gains the optional `issues` prop and
|
|
301
|
+
`fields[].optionLabels`, T-gaps-100-104) and an additive forms field
|
|
302
|
+
vocabulary (`$defs.formField` gains the `password`, `otp`, `color`,
|
|
303
|
+
`datetime` and `custom` types and the `kind`, `config`, `length`,
|
|
304
|
+
`protocols` and `clearable` members, T-gaps-113-121; `command-palette` lists
|
|
305
|
+
`searchLabel`, T-gaps-107-110). `@texuli/spec@0.9.0` carried spec `0.9.0` (digest
|
|
286
306
|
`497df4d3b0c67d53483163ebd9bd73733b52caf5f8e2966478f1cf860e6f9d9d`), an
|
|
287
307
|
additive catalog projection refresh (PLAN_011 T-gaps-87-95 and
|
|
288
308
|
T-gaps-96-98); `@texuli/spec@0.8.0` carried spec `0.8.0` (digest
|
|
@@ -318,6 +338,8 @@ conformance harness) are in
|
|
|
318
338
|
node shells; new in `0.6.0`): [stream.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/stream.md)
|
|
319
339
|
- Data-source bindings (spec 0.8.0; new in `0.8.0`):
|
|
320
340
|
[source.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/source.md)
|
|
341
|
+
- The design-system document (spec 0.10.0; new in `0.10.0`):
|
|
342
|
+
[design-system.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/design-system.md)
|
|
321
343
|
- The Kotlin Multiplatform twin that consumes the generated models:
|
|
322
344
|
[native/README.md](https://github.com/DailybotHQ/texuli/blob/main/native/README.md)
|
|
323
345
|
- Architecture: [ARCHITECTURE.md](https://github.com/DailybotHQ/texuli/blob/main/docs/ARCHITECTURE.md)
|
package/dist/build.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export declare const SCHEMA_UNITS: readonly [{
|
|
|
38
38
|
readonly prefix: "";
|
|
39
39
|
readonly ts: "workflow.ts";
|
|
40
40
|
readonly kt: "Workflow.kt";
|
|
41
|
+
}, {
|
|
42
|
+
readonly name: "design-system";
|
|
43
|
+
readonly prefix: "DesignSystem";
|
|
44
|
+
readonly ts: "design-system.ts";
|
|
45
|
+
readonly kt: "DesignSystem.kt";
|
|
41
46
|
}];
|
|
42
47
|
export declare const TS_OUT = "generated/typescript/";
|
|
43
48
|
export declare const KOTLIN_OUT = "generated/kotlin/com/texuli/spec/";
|
package/dist/build.js
CHANGED
|
@@ -26,6 +26,7 @@ export const SCHEMA_UNITS = [
|
|
|
26
26
|
{ name: 'diagnostics', prefix: 'Spec', ts: 'diagnostics.ts', kt: 'Diagnostics.kt' },
|
|
27
27
|
{ name: 'manifest', prefix: 'Spec', ts: 'manifest.ts', kt: 'Manifest.kt' },
|
|
28
28
|
{ name: 'workflow', prefix: '', ts: 'workflow.ts', kt: 'Workflow.kt' },
|
|
29
|
+
{ name: 'design-system', prefix: 'DesignSystem', ts: 'design-system.ts', kt: 'DesignSystem.kt' },
|
|
29
30
|
];
|
|
30
31
|
export const TS_OUT = 'generated/typescript/';
|
|
31
32
|
export const KOTLIN_OUT = 'generated/kotlin/com/texuli/spec/';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The color roles a token set carries, in output order (camelCase of `--tx-<role>`). */
|
|
2
|
+
export declare const KOTLIN_TOKEN_ROLES: readonly ["background", "foreground", "surface", "surfaceForeground", "muted", "mutedForeground", "border", "borderStrong", "ring", "primary", "primaryForeground", "primarySoft", "primarySoftForeground", "secondary", "secondaryForeground", "accent", "accentForeground", "link", "destructive", "destructiveForeground", "destructiveSoft", "destructiveSoftForeground", "success", "successForeground", "warning", "warningForeground", "info", "infoForeground", "scrim"];
|
|
3
|
+
export type KotlinTokenRole = (typeof KOTLIN_TOKEN_ROLES)[number];
|
|
4
|
+
/** The web variable a role reads (`primarySoft` → `--tx-primary-soft`). */
|
|
5
|
+
export declare function tokenVarName(role: string): string;
|
|
6
|
+
/** One design system, resolved. Colors are `#rrggbb` or `#rrggbbaa`. */
|
|
7
|
+
export interface KotlinTokenSystem {
|
|
8
|
+
/** The Kotlin property name (`Default`, `Acme`). */
|
|
9
|
+
name: string;
|
|
10
|
+
id: string;
|
|
11
|
+
version: string;
|
|
12
|
+
/** The compiled design system's hash. */
|
|
13
|
+
hash: string;
|
|
14
|
+
fonts: {
|
|
15
|
+
sans: string;
|
|
16
|
+
heading: string;
|
|
17
|
+
mono: string;
|
|
18
|
+
};
|
|
19
|
+
/** `--tx-radius` and `--tx-space-1`, in pixels (dp on native). */
|
|
20
|
+
radius: number;
|
|
21
|
+
space: number;
|
|
22
|
+
light: Readonly<Record<KotlinTokenRole, string>>;
|
|
23
|
+
dark?: Readonly<Record<KotlinTokenRole, string>> | null;
|
|
24
|
+
}
|
|
25
|
+
export interface KotlinTokenOptions {
|
|
26
|
+
/** Default `com.texuli.spec.tokens`. */
|
|
27
|
+
packageName?: string;
|
|
28
|
+
/** What produced the input, for the header comment. */
|
|
29
|
+
source?: string;
|
|
30
|
+
}
|
|
31
|
+
/** `#rrggbb` or `#rrggbbaa` as a Kotlin `0xAARRGGBBL` literal. */
|
|
32
|
+
export declare function argbLiteral(color: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The Kotlin source of `DesignTokenColors`, `DesignTokenSet` and an object
|
|
35
|
+
* holding each system. Throws on input it cannot express (a bad name, a
|
|
36
|
+
* color that is not sRGB hex, a negative length).
|
|
37
|
+
*/
|
|
38
|
+
export declare function designTokensToKotlin(systems: readonly KotlinTokenSystem[], options?: KotlinTokenOptions): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kotlin design tokens (ADR-024): a compiled design system's values as a
|
|
3
|
+
* Kotlin source file, so the native twin paints with the same colors,
|
|
4
|
+
* fonts, radius and spacing as the web. The input is plain resolved data
|
|
5
|
+
* (colors already sRGB hex, lengths in pixels), which a build step makes
|
|
6
|
+
* from `compileDesignSystem` in `@texuli/tokens`; this module never
|
|
7
|
+
* imports the compiler, so `@texuli/spec` stays at L0. Deterministic: the
|
|
8
|
+
* same input gives byte-identical output. Internal for now: only
|
|
9
|
+
* `scripts/design-tokens.ts` uses it, and it is not exported from
|
|
10
|
+
* `@texuli/spec/codegen` (that entry is at its size budget; a public
|
|
11
|
+
* subpath is planned).
|
|
12
|
+
*/
|
|
13
|
+
import { kotlinString } from "./kotlin.js";
|
|
14
|
+
/** The color roles a token set carries, in output order (camelCase of `--tx-<role>`). */
|
|
15
|
+
export const KOTLIN_TOKEN_ROLES = Object.freeze([
|
|
16
|
+
'background',
|
|
17
|
+
'foreground',
|
|
18
|
+
'surface',
|
|
19
|
+
'surfaceForeground',
|
|
20
|
+
'muted',
|
|
21
|
+
'mutedForeground',
|
|
22
|
+
'border',
|
|
23
|
+
'borderStrong',
|
|
24
|
+
'ring',
|
|
25
|
+
'primary',
|
|
26
|
+
'primaryForeground',
|
|
27
|
+
'primarySoft',
|
|
28
|
+
'primarySoftForeground',
|
|
29
|
+
'secondary',
|
|
30
|
+
'secondaryForeground',
|
|
31
|
+
'accent',
|
|
32
|
+
'accentForeground',
|
|
33
|
+
'link',
|
|
34
|
+
'destructive',
|
|
35
|
+
'destructiveForeground',
|
|
36
|
+
'destructiveSoft',
|
|
37
|
+
'destructiveSoftForeground',
|
|
38
|
+
'success',
|
|
39
|
+
'successForeground',
|
|
40
|
+
'warning',
|
|
41
|
+
'warningForeground',
|
|
42
|
+
'info',
|
|
43
|
+
'infoForeground',
|
|
44
|
+
'scrim',
|
|
45
|
+
]);
|
|
46
|
+
/** The web variable a role reads (`primarySoft` → `--tx-primary-soft`). */
|
|
47
|
+
export function tokenVarName(role) {
|
|
48
|
+
return `--tx-${role.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
49
|
+
}
|
|
50
|
+
const I = ' ';
|
|
51
|
+
const NAME = /^[A-Z][A-Za-z0-9]{0,63}$/;
|
|
52
|
+
const PACKAGE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/;
|
|
53
|
+
/** `#rrggbb` or `#rrggbbaa` as a Kotlin `0xAARRGGBBL` literal. */
|
|
54
|
+
export function argbLiteral(color) {
|
|
55
|
+
const match = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(color);
|
|
56
|
+
if (!match)
|
|
57
|
+
throw new TypeError(`Not an sRGB hex color: ${JSON.stringify(color)}`);
|
|
58
|
+
return `0x${(match[2] ?? 'ff').toUpperCase()}${match[1].toUpperCase()}L`;
|
|
59
|
+
}
|
|
60
|
+
function number(value, what) {
|
|
61
|
+
if (!Number.isFinite(value) || value < 0 || value > 10_000) {
|
|
62
|
+
throw new TypeError(`${what} is not a length in pixels: ${value}`);
|
|
63
|
+
}
|
|
64
|
+
const text = String(Math.round(value * 1000) / 1000);
|
|
65
|
+
return text.includes('.') ? text : `${text}.0`;
|
|
66
|
+
}
|
|
67
|
+
function colors(values, depth) {
|
|
68
|
+
return [
|
|
69
|
+
'DesignTokenColors(',
|
|
70
|
+
...KOTLIN_TOKEN_ROLES.map((role) => `${depth}${I}${role} = ${argbLiteral(values[role])},`),
|
|
71
|
+
`${depth})`,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The Kotlin source of `DesignTokenColors`, `DesignTokenSet` and an object
|
|
76
|
+
* holding each system. Throws on input it cannot express (a bad name, a
|
|
77
|
+
* color that is not sRGB hex, a negative length).
|
|
78
|
+
*/
|
|
79
|
+
export function designTokensToKotlin(systems, options = {}) {
|
|
80
|
+
const packageName = options.packageName ?? 'com.texuli.spec.tokens';
|
|
81
|
+
if (!PACKAGE.test(packageName))
|
|
82
|
+
throw new TypeError(`Bad package name: ${packageName}`);
|
|
83
|
+
const names = new Set();
|
|
84
|
+
for (const system of systems) {
|
|
85
|
+
if (!NAME.test(system.name) || names.has(system.name)) {
|
|
86
|
+
throw new TypeError(`Bad or repeated token set name: ${JSON.stringify(system.name)}`);
|
|
87
|
+
}
|
|
88
|
+
names.add(system.name);
|
|
89
|
+
}
|
|
90
|
+
const lines = [
|
|
91
|
+
`// Generated from compiled design systems${options.source ? ` by ${options.source}` : ''}. Do not edit.`,
|
|
92
|
+
'',
|
|
93
|
+
`package ${packageName}`,
|
|
94
|
+
'',
|
|
95
|
+
'/** One mode of a design system: every color as `0xAARRGGBB` sRGB. */',
|
|
96
|
+
'class DesignTokenColors(',
|
|
97
|
+
...KOTLIN_TOKEN_ROLES.map((role) => `${I}val ${role}: Long,`),
|
|
98
|
+
') {',
|
|
99
|
+
`${I}/** Each color by the web variable it mirrors (\`--tx-background\` …). */`,
|
|
100
|
+
`${I}val byVar: Map<String, Long>`,
|
|
101
|
+
`${I}${I}get() = mapOf(`,
|
|
102
|
+
...KOTLIN_TOKEN_ROLES.map((role) => `${I}${I}${I}${kotlinString(tokenVarName(role))} to ${role},`),
|
|
103
|
+
`${I}${I})`,
|
|
104
|
+
'}',
|
|
105
|
+
'',
|
|
106
|
+
'/** A compiled design system: fonts as CSS stacks, lengths in px (dp), colors per mode. */',
|
|
107
|
+
'class DesignTokenSet(',
|
|
108
|
+
`${I}val id: String,`,
|
|
109
|
+
`${I}val version: String,`,
|
|
110
|
+
`${I}val hash: String,`,
|
|
111
|
+
`${I}val fontSans: String,`,
|
|
112
|
+
`${I}val fontHeading: String,`,
|
|
113
|
+
`${I}val fontMono: String,`,
|
|
114
|
+
`${I}val radius: Double,`,
|
|
115
|
+
`${I}val space: Double,`,
|
|
116
|
+
`${I}val light: DesignTokenColors,`,
|
|
117
|
+
`${I}val dark: DesignTokenColors?,`,
|
|
118
|
+
')',
|
|
119
|
+
'',
|
|
120
|
+
'/** The generated token sets. */',
|
|
121
|
+
'object DesignTokens {',
|
|
122
|
+
];
|
|
123
|
+
for (const system of systems) {
|
|
124
|
+
const d = `${I}${I}`;
|
|
125
|
+
lines.push(`${I}val ${system.name}: DesignTokenSet = DesignTokenSet(`, `${d}id = ${kotlinString(system.id)},`, `${d}version = ${kotlinString(system.version)},`, `${d}hash = ${kotlinString(system.hash)},`, `${d}fontSans = ${kotlinString(system.fonts.sans)},`, `${d}fontHeading = ${kotlinString(system.fonts.heading)},`, `${d}fontMono = ${kotlinString(system.fonts.mono)},`, `${d}radius = ${number(system.radius, 'radius')},`, `${d}space = ${number(system.space, 'space')},`);
|
|
126
|
+
const light = colors(system.light, d);
|
|
127
|
+
lines.push(`${d}light = ${light[0]}`, ...light.slice(1, -1), `${light.at(-1)},`);
|
|
128
|
+
if (system.dark) {
|
|
129
|
+
const dark = colors(system.dark, d);
|
|
130
|
+
lines.push(`${d}dark = ${dark[0]}`, ...dark.slice(1, -1), `${dark.at(-1)},`);
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
lines.push(`${d}dark = null,`);
|
|
134
|
+
lines.push(`${I})`, '');
|
|
135
|
+
}
|
|
136
|
+
lines.push(`${I}/** Every set, in generation order. */`, `${I}val all: List<DesignTokenSet>`, `${I}${I}get() = listOf(${systems.map((system) => system.name).join(', ')})`, '}', '');
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
package/dist/codegen/kotlin.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ export declare const KOTLIN_PACKAGE = "com.texuli.spec";
|
|
|
12
12
|
export declare function kotlinString(value: string): string;
|
|
13
13
|
/**
|
|
14
14
|
* The Kotlin property for a wire name: a leading `$` is stripped (`$state` →
|
|
15
|
-
* `state`), because `$` starts a string template in Kotlin
|
|
15
|
+
* `state`), because `$` starts a string template in Kotlin, and a hyphen
|
|
16
|
+
* joins camelCase (`height-md` → `heightMd`), because kebab-case keys (the
|
|
17
|
+
* design-system component registry) are not Kotlin names. The wire name
|
|
16
18
|
* itself is always emitted through {@link kotlinString}.
|
|
17
19
|
*/
|
|
18
20
|
export declare function kotlinProperty(name: string): string;
|
package/dist/codegen/kotlin.js
CHANGED
|
@@ -66,11 +66,14 @@ function field(name) {
|
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* The Kotlin property for a wire name: a leading `$` is stripped (`$state` →
|
|
69
|
-
* `state`), because `$` starts a string template in Kotlin
|
|
69
|
+
* `state`), because `$` starts a string template in Kotlin, and a hyphen
|
|
70
|
+
* joins camelCase (`height-md` → `heightMd`), because kebab-case keys (the
|
|
71
|
+
* design-system component registry) are not Kotlin names. The wire name
|
|
70
72
|
* itself is always emitted through {@link kotlinString}.
|
|
71
73
|
*/
|
|
72
74
|
export function kotlinProperty(name) {
|
|
73
|
-
|
|
75
|
+
const bare = name.startsWith('$') ? name.slice(1) : name;
|
|
76
|
+
return bare.replace(/-([A-Za-z0-9])/g, (_, char) => char.toUpperCase());
|
|
74
77
|
}
|
|
75
78
|
/** `$` escaped for a Kotlin string template (`\$state`). */
|
|
76
79
|
function templateText(name) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/spec/design-system` — the design-system document contract
|
|
3
|
+
* (ADR-021): the generated types, the closed vocabulary's codes and caps,
|
|
4
|
+
* `validateDesignSystem` with JSON paths, `DesignSystemPatch` with its check
|
|
5
|
+
* and pure merge, `migrateDesignSystem` and the blueprint reference check.
|
|
6
|
+
* Framework-free and dependency-free; the compiler lives in
|
|
7
|
+
* `@texuli/tokens/design-system/compile`.
|
|
8
|
+
*/
|
|
9
|
+
export type * from '../generated/typescript/design-system.js';
|
|
10
|
+
export { applyDesignSystemPatch, checkDesignSystemReference, DESIGN_SYSTEM_MIGRATIONS, type DesignSystemMigration, type DesignSystemMigrationResult, type DesignSystemPatch, type DesignSystemValidateOptions, type DesignSystemValidation, migrateDesignSystem, validateDesignSystem, validateDesignSystemPatch, } from './validate.js';
|
|
11
|
+
export { DESIGN_SYSTEM_CODES, DESIGN_SYSTEM_COMPILER_CODES, DESIGN_SYSTEM_FORMAT, DESIGN_SYSTEM_FORMAT_VERSION, DESIGN_SYSTEM_LIMITS, type DesignSystemCode, } from './vocabulary.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/spec/design-system` — the design-system document contract
|
|
3
|
+
* (ADR-021): the generated types, the closed vocabulary's codes and caps,
|
|
4
|
+
* `validateDesignSystem` with JSON paths, `DesignSystemPatch` with its check
|
|
5
|
+
* and pure merge, `migrateDesignSystem` and the blueprint reference check.
|
|
6
|
+
* Framework-free and dependency-free; the compiler lives in
|
|
7
|
+
* `@texuli/tokens/design-system/compile`.
|
|
8
|
+
*/
|
|
9
|
+
export { applyDesignSystemPatch, checkDesignSystemReference, DESIGN_SYSTEM_MIGRATIONS, migrateDesignSystem, validateDesignSystem, validateDesignSystemPatch, } from "./validate.js";
|
|
10
|
+
export { DESIGN_SYSTEM_CODES, DESIGN_SYSTEM_COMPILER_CODES, DESIGN_SYSTEM_FORMAT, DESIGN_SYSTEM_FORMAT_VERSION, DESIGN_SYSTEM_LIMITS, } from "./vocabulary.js";
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { pascal } from "../codegen/shared.js";
|
|
2
|
+
import { DIALECT_URI } from "../dialect.js";
|
|
3
|
+
import { DESIGN_SYSTEM_CODES, DESIGN_SYSTEM_COMPILER_CODES, DESIGN_SYSTEM_LIMITS, DS_DEFS, DS_PATTERNS, DS_ROOT, } from "./vocabulary.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build-time projection of the design-system vocabulary table into a
|
|
6
|
+
* dialect schema (`schemas/design-system.schema.json`), from which the spec
|
|
7
|
+
* generator writes the TypeScript and Kotlin types. Every object becomes a
|
|
8
|
+
* named `$defs` entry (named definitions keep their name; anonymous ones are
|
|
9
|
+
* named after their path), so both generators can project it. Rules the
|
|
10
|
+
* dialect cannot express (map key patterns, unique items, trust-dependent
|
|
11
|
+
* data URLs, inline byte caps, the `extends` chain) are described here and
|
|
12
|
+
* enforced by `validateDesignSystem`.
|
|
13
|
+
*/
|
|
14
|
+
const DESCRIPTIONS = {
|
|
15
|
+
colorValue: 'A color: #rgb, #rrggbb, #rrggbbaa or a numeric oklch(L C H[ / A]).',
|
|
16
|
+
roleName: 'A semantic color role of the token contract.',
|
|
17
|
+
roleMap: 'Explicit role pins for one mode: checked by the compiler, never moved (ADR-022).',
|
|
18
|
+
dataVizPalette: 'One mode’s chart palette: 8 categorical, 9 sequential and 9 diverging colors.',
|
|
19
|
+
fontFile: `A self-hosted font file: a same-origin relative path (.woff2, .woff, .ttf, .otf), or at developer trust a base64 data:font URL of at most ${DESIGN_SYSTEM_LIMITS.font} bytes.`,
|
|
20
|
+
fontFamily: 'A family stack (1 to 8 names, quoted by the compiler) and its font files.',
|
|
21
|
+
typeRole: 'A type role: family, size (px), weight, line height and tracking (em).',
|
|
22
|
+
typeRoleName: 'A type role name.',
|
|
23
|
+
radiusStep: 'A shape radius step.',
|
|
24
|
+
spaceStep: 'A space step.',
|
|
25
|
+
dimension: 'A space, control, icon or target size step.',
|
|
26
|
+
borderStep: 'A border width step.',
|
|
27
|
+
elevationLevel: 'An elevation level.',
|
|
28
|
+
opacityStep: 'An opacity step.',
|
|
29
|
+
strokeWidth: 'A stroke width in px.',
|
|
30
|
+
asset: `An image: a same-origin relative path (png, jpg, gif, webp, avif, svg, ico; never .., \\, %, ?, # or //), or at developer trust a base64 data:image (png, jpeg, gif, webp, avif) of at most ${DESIGN_SYSTEM_LIMITS.logo} bytes. SVG renders only as <img>.`,
|
|
31
|
+
color: 'Color: brand seeds, exact pins, explicit role pins per mode, modes, data viz per mode and status colors.',
|
|
32
|
+
typography: 'Typography: family stacks with font files, the scale and the type roles.',
|
|
33
|
+
shape: 'Shape: radius (px), radius scale and border width (px).',
|
|
34
|
+
space: 'Space: the base unit (px) and the density multiplier.',
|
|
35
|
+
elevation: 'Elevation style.',
|
|
36
|
+
surface: 'Surface style and glass settings.',
|
|
37
|
+
motion: 'Motion personality, durations (ms) and a named easing.',
|
|
38
|
+
layout: 'Layout widths, breakpoints and the grid (px).',
|
|
39
|
+
icons: 'Icon style, stroke and sizes (px).',
|
|
40
|
+
components: `Component tokens (the token registry, z-index left out) and defaults (variant and size), per component.`,
|
|
41
|
+
assets: 'Brand assets: logos and the favicon.',
|
|
42
|
+
voice: 'Voice data: tone words, a lexicon (at most 200 entries) and guidelines.',
|
|
43
|
+
accessibility: 'Accessibility targets: contrast, minimum target size and the focus style.',
|
|
44
|
+
variant: 'A named variant (high-contrast, compact, seasonal): a closed partial document of sections, orthogonal to mode.',
|
|
45
|
+
issueCode: 'Every stable design-system code; aa-text, aa-focus and target-size are raised by the compiler.',
|
|
46
|
+
issue: 'One design-system issue: a stable code, a JSON path from $, a message and an optional hint.',
|
|
47
|
+
};
|
|
48
|
+
function source(node) {
|
|
49
|
+
const path = (node[1] === 'i' ? DS_PATTERNS.image : DS_PATTERNS.font).source.slice(1, -1);
|
|
50
|
+
const data = node[1] === 'i' ? 'data:image\\/(?:png|jpeg|gif|webp|avif)' : 'data:font\\/(?:woff2?|ttf|otf)';
|
|
51
|
+
return {
|
|
52
|
+
description: `At most ${node[2]} bytes inline; data URLs need developer trust.`,
|
|
53
|
+
type: 'string',
|
|
54
|
+
pattern: `^(?:${path}|${data};base64,[A-Za-z0-9+/]+={0,2})$`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The design-system document schema, projected from the vocabulary table. */
|
|
58
|
+
export function designSystemSchema() {
|
|
59
|
+
const defs = {};
|
|
60
|
+
const toSchema = (node, name) => {
|
|
61
|
+
if (typeof node === 'string')
|
|
62
|
+
return { $ref: `#/$defs/${node}` };
|
|
63
|
+
if (!Array.isArray(node)) {
|
|
64
|
+
if (!Object.hasOwn(defs, name))
|
|
65
|
+
defs[name] = objectSchema(node, name);
|
|
66
|
+
return { $ref: `#/$defs/${name}` };
|
|
67
|
+
}
|
|
68
|
+
const n = node;
|
|
69
|
+
switch (n[0]) {
|
|
70
|
+
case 'e':
|
|
71
|
+
return { type: 'string', enum: n[1].split(' ') };
|
|
72
|
+
case 'n':
|
|
73
|
+
case 'i':
|
|
74
|
+
return { type: n[0] === 'i' ? 'integer' : 'number', minimum: n[1], maximum: n[2] };
|
|
75
|
+
case 's':
|
|
76
|
+
return { type: 'string', maxLength: n[2], pattern: n[1].source };
|
|
77
|
+
case 't':
|
|
78
|
+
return { type: 'string', maxLength: n[1], pattern: DS_PATTERNS.text.source };
|
|
79
|
+
case 'l':
|
|
80
|
+
return {
|
|
81
|
+
...(n[4] ? { description: 'Unique items.' } : {}),
|
|
82
|
+
type: 'array',
|
|
83
|
+
items: toSchema(n[1], `${name}Item`),
|
|
84
|
+
...(n[2] > 0 ? { minItems: n[2] } : {}),
|
|
85
|
+
maxItems: n[3],
|
|
86
|
+
};
|
|
87
|
+
case 'm':
|
|
88
|
+
return {
|
|
89
|
+
description: `At most ${n[2]} entries; keys match ${n[3].source} and are never __proto__, constructor or prototype.`,
|
|
90
|
+
type: 'object',
|
|
91
|
+
additionalProperties: toSchema(n[1], `${name}Value`),
|
|
92
|
+
};
|
|
93
|
+
case 'b':
|
|
94
|
+
return { type: 'boolean' };
|
|
95
|
+
case 'k':
|
|
96
|
+
return { const: n[1] };
|
|
97
|
+
case 'u':
|
|
98
|
+
return source(n);
|
|
99
|
+
case 'x':
|
|
100
|
+
return {
|
|
101
|
+
description: 'texuli:<theme> or an exact id@x.y.z; never a range, a URL or a path. Cycles and chains deeper than 4 are refused.',
|
|
102
|
+
type: 'string',
|
|
103
|
+
pattern: DS_PATTERNS.ref.source,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function objectSchema(shape, name) {
|
|
108
|
+
const properties = {};
|
|
109
|
+
const required = [];
|
|
110
|
+
for (const [raw, child] of Object.entries(shape)) {
|
|
111
|
+
const key = raw.replace(/!$/, '');
|
|
112
|
+
if (raw.endsWith('!'))
|
|
113
|
+
required.push(key);
|
|
114
|
+
properties[key] = toSchema(child, `${name}${pascal(key)}`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
...(DESCRIPTIONS[name] ? { description: DESCRIPTIONS[name] } : {}),
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties,
|
|
120
|
+
...(required.length > 0 ? { required } : {}),
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
for (const [name, node] of Object.entries(DS_DEFS)) {
|
|
125
|
+
if (typeof node !== 'string' && !Array.isArray(node))
|
|
126
|
+
toSchema(node, name);
|
|
127
|
+
else {
|
|
128
|
+
const schema = toSchema(node, name);
|
|
129
|
+
defs[name] = DESCRIPTIONS[name] ? { description: DESCRIPTIONS[name], ...schema } : schema;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
defs.issueCode = {
|
|
133
|
+
description: DESCRIPTIONS.issueCode,
|
|
134
|
+
type: 'string',
|
|
135
|
+
enum: [...DESIGN_SYSTEM_CODES, ...DESIGN_SYSTEM_COMPILER_CODES],
|
|
136
|
+
};
|
|
137
|
+
defs.issue = {
|
|
138
|
+
description: DESCRIPTIONS.issue,
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
code: { $ref: '#/$defs/issueCode' },
|
|
142
|
+
path: { type: 'string' },
|
|
143
|
+
message: { type: 'string' },
|
|
144
|
+
hint: { type: 'string' },
|
|
145
|
+
},
|
|
146
|
+
required: ['code', 'path', 'message'],
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
};
|
|
149
|
+
const root = objectSchema(DS_ROOT, 'designSystem');
|
|
150
|
+
const ordered = {};
|
|
151
|
+
for (const key of Object.keys(defs).sort())
|
|
152
|
+
ordered[key] = defs[key];
|
|
153
|
+
return {
|
|
154
|
+
$schema: DIALECT_URI,
|
|
155
|
+
$id: 'https://texuli.example/spec/schemas/design-system.schema.json',
|
|
156
|
+
title: 'DesignSystem',
|
|
157
|
+
description: 'A Texuli design system (ADR-021, formatVersion 1): one closed JSON document a company or an agent writes once and every Texuli surface follows. Data only: colors are hex or numeric oklch, lengths are numbers in px, fonts and assets are same-origin relative paths or capped data URLs, and nothing reaches CSS that the token contract’s css-safe checks would refuse. Generated from @texuli/spec/design-system’s vocabulary by scripts/design-system-schema.ts; validateDesignSystem enforces the rules the dialect cannot express.',
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: root.properties,
|
|
160
|
+
required: root.required,
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
$defs: ordered,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DesignSystem, DesignSystemIssue, DesignSystemVariant } from '../generated/typescript/design-system.js';
|
|
2
|
+
/** The result of every design-system check: issues with JSON paths from `$`. */
|
|
3
|
+
export interface DesignSystemValidation {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
errors: readonly DesignSystemIssue[];
|
|
6
|
+
}
|
|
7
|
+
export interface DesignSystemValidateOptions {
|
|
8
|
+
/**
|
|
9
|
+
* `untrusted` (the default: agent or user documents) refuses every `data:`
|
|
10
|
+
* URL; `developer` (documents the host supplies) accepts base64 raster
|
|
11
|
+
* images and fonts within their caps. SVG is a relative path only.
|
|
12
|
+
*/
|
|
13
|
+
trust?: 'developer' | 'untrusted';
|
|
14
|
+
/** Resolve `id@version` for the `extends` chain (cycles and depth); synchronous. */
|
|
15
|
+
resolve?: (ref: string) => unknown;
|
|
16
|
+
/** Stop after this many issues (default and ceiling 64). */
|
|
17
|
+
maxErrors?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check a design-system document against the closed vocabulary. Never
|
|
21
|
+
* throws on a JSON value; returns every issue (up to `maxErrors`) with a
|
|
22
|
+
* stable code and a JSON path. A future `formatVersion` stops at
|
|
23
|
+
* `format-version`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateDesignSystem(doc: unknown, options?: DesignSystemValidateOptions): DesignSystemValidation;
|
|
26
|
+
/** A typed partial document: objects merge, arrays replace, `null` removes. */
|
|
27
|
+
export type DesignSystemPatch = PatchOf<DesignSystemVariant>;
|
|
28
|
+
type PatchOf<T> = T extends readonly unknown[] ? T : T extends object ? {
|
|
29
|
+
[K in keyof T]?: PatchOf<T[K]> | null;
|
|
30
|
+
} : T;
|
|
31
|
+
/** Check a patch: the sections only, every field optional, `null` to remove. */
|
|
32
|
+
export declare function validateDesignSystemPatch(patch: unknown, options?: DesignSystemValidateOptions): DesignSystemValidation;
|
|
33
|
+
/**
|
|
34
|
+
* Merge a patch into a document, purely: objects merge, arrays and scalars
|
|
35
|
+
* replace, `null` removes, and `__proto__`, `constructor` and `prototype`
|
|
36
|
+
* are skipped. Validate the result again.
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyDesignSystemPatch(base: DesignSystem, patch: DesignSystemPatch): DesignSystem;
|
|
39
|
+
/** One `formatVersion` step. */
|
|
40
|
+
export interface DesignSystemMigration {
|
|
41
|
+
from: number;
|
|
42
|
+
to: number;
|
|
43
|
+
migrate(doc: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** Every step, in order (none yet: formatVersion 1 is the first). */
|
|
46
|
+
export declare const DESIGN_SYSTEM_MIGRATIONS: readonly DesignSystemMigration[];
|
|
47
|
+
export type DesignSystemMigrationResult = {
|
|
48
|
+
ok: true;
|
|
49
|
+
document: DesignSystem;
|
|
50
|
+
from: number;
|
|
51
|
+
to: number;
|
|
52
|
+
applied: string[];
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
errors: [DesignSystemIssue];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Bring a document up to the current `formatVersion`. Pure and total: never
|
|
59
|
+
* throws on a JSON value and never mutates it; an unknown or future version
|
|
60
|
+
* is a `format-version` result. Validate the migrated document.
|
|
61
|
+
*/
|
|
62
|
+
export declare function migrateDesignSystem(doc: unknown): DesignSystemMigrationResult;
|
|
63
|
+
/**
|
|
64
|
+
* Check a blueprint's design-system reference: exactly one of `ref`
|
|
65
|
+
* (`id@x.y.z` or `texuli:<theme>`) and `inline` (a whole document, checked
|
|
66
|
+
* by {@link validateDesignSystem} at `options.trust`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function checkDesignSystemReference(value: unknown, options?: DesignSystemValidateOptions): DesignSystemValidation;
|
|
69
|
+
export {};
|