@texuli/spec 0.5.0 → 0.6.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 +37 -10
- package/dist/generated/typescript/blueprint.d.ts +44 -3
- package/dist/generated/typescript/blueprint.js +1 -1
- package/dist/generated/typescript/catalog-schema.js +1 -1
- package/dist/generated/typescript/diagnostics.js +1 -1
- package/dist/generated/typescript/manifest.js +1 -1
- package/dist/generated/typescript/vocabulary.js +1 -1
- package/dist/generated/typescript/workflow.js +1 -1
- package/dist/migrations/0.5.0-0.6.0.d.ts +9 -0
- package/dist/migrations/0.5.0-0.6.0.js +12 -0
- package/dist/migrations/index.js +2 -0
- package/dist/motion.d.ts +40 -0
- package/dist/motion.js +133 -0
- package/dist/stream/assembler.d.ts +101 -0
- package/dist/stream/assembler.js +284 -0
- package/dist/stream/index.d.ts +11 -0
- package/dist/stream/index.js +11 -0
- package/dist/stream/source.d.ts +22 -0
- package/dist/stream/source.js +72 -0
- package/dist/stream/tokenizer.d.ts +67 -0
- package/dist/stream/tokenizer.js +383 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -14,9 +14,14 @@ 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.
|
|
17
|
+
> published to npm as `0.6.0`, a pre-1.0 release (1.0.0 will be the first
|
|
18
|
+
> stable release). Under 0.x, a new minor (`0.7.0`) may contain breaking
|
|
19
|
+
> changes, so pin the exact version or use `^0.6.0`, which accepts only 0.6.x.
|
|
20
|
+
|
|
21
|
+
> **New in `0.6.0`.** `@texuli/spec@0.6.0` carries **spec 0.6.0**: the
|
|
22
|
+
> closed motion vocabulary (`@texuli/spec/motion`, below), and the new
|
|
23
|
+
> `@texuli/spec/stream` subpath reads streamed blueprints. Every `0.5.0`
|
|
24
|
+
> envelope migrates by stamping `spec`.
|
|
20
25
|
|
|
21
26
|
> **New in `0.4.0`.** `@texuli/spec@0.4.0` carries **spec 0.4.0**: spec
|
|
22
27
|
> 0.3.0 (state, bindings, closed actions, `reorder`, workflow graphs), spec
|
|
@@ -56,7 +61,7 @@ import {
|
|
|
56
61
|
validateInstance,
|
|
57
62
|
} from '@texuli/spec';
|
|
58
63
|
|
|
59
|
-
SPEC_VERSION; // the spec version: '0.
|
|
64
|
+
SPEC_VERSION; // the spec version: '0.6.0' in @texuli/spec@0.6.0
|
|
60
65
|
SPEC_COMPONENT_IDS.includes('alert'); // true: the closed component vocabulary
|
|
61
66
|
|
|
62
67
|
// The dialect validator: a closed subset of JSON Schema 2020-12.
|
|
@@ -88,7 +93,7 @@ import { type Schema, type SpecCatalog, validateBlueprint } from '@texuli/spec';
|
|
|
88
93
|
import { migrateBlueprint } from '@texuli/spec/migrate';
|
|
89
94
|
import { validateWorkflowGraph } from '@texuli/spec/workflow';
|
|
90
95
|
|
|
91
|
-
const migrated = migrateBlueprint(blueprint); // 0.2.0 →
|
|
96
|
+
const migrated = migrateBlueprint(blueprint); // 0.2.0 → … → 0.5.0 → 0.6.0; never throws
|
|
92
97
|
const result = validateBlueprint(migrated.blueprint, {
|
|
93
98
|
blueprintSchema: blueprintSchemaJson as Schema,
|
|
94
99
|
catalog: componentsJson as SpecCatalog,
|
|
@@ -120,6 +125,8 @@ warning without failing.
|
|
|
120
125
|
| `@texuli/spec/workflow` | Runtime, opt-in: workflow graph validation | `validateWorkflowGraph` (pass it in `SpecDocuments.validateWorkflowGraph`) |
|
|
121
126
|
| `@texuli/spec/migrate` | Runtime, opt-in: envelope migrations | `migrateBlueprint`, `SPEC_MIGRATIONS`, `UNDECLARED_SPEC_VERSION` |
|
|
122
127
|
| `@texuli/spec/surface` | Runtime: the state surface rules every validator shares (no imports) | `checkStateSurface`, `surfaceInputs`, `SPEC_CONTROL_STATE_TYPES`, `SURFACE_PLAYER_CODES`, `LIST_TEXT_PATTERN` |
|
|
128
|
+
| `@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` |
|
|
129
|
+
| `@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` |
|
|
123
130
|
| `@texuli/spec/codegen` | Build time: generation and conformance | `buildSpec`, `projectCatalog`, `checkConformance`, `checkCatalogContract`, `formatJson`, `nextSpecVersion`, `checkVersionHistory` |
|
|
124
131
|
|
|
125
132
|
## Key guarantees
|
|
@@ -148,6 +155,19 @@ warning without failing.
|
|
|
148
155
|
component slides. The change is additive: every 0.3.0 envelope is a valid
|
|
149
156
|
0.4.0 envelope, and the `0.3.0 → 0.4.0` migration only stamps `spec`. The
|
|
150
157
|
deck model itself is validated by `@texuli/deck`, not by this package.
|
|
158
|
+
- **Motion as data (spec 0.6.0, new in `0.6.0`).** A node may declare `motion`
|
|
159
|
+
(`enter` and `exit` presets `fade`, `rise`, `scale`, `slide-start` and
|
|
160
|
+
`slide-end`; `layout`; `stagger` and `duration` in token steps) and the
|
|
161
|
+
envelope a `motion.level` (`full`, `subtle`, `none`). Names map to the
|
|
162
|
+
motion tokens: a blueprint never carries CSS, keyframes, milliseconds or
|
|
163
|
+
code, and anything else is refused. The `0.5.0 → 0.6.0` migration only
|
|
164
|
+
stamps `spec`.
|
|
165
|
+
- **Streams parse, never guess.** `@texuli/spec/stream` (new in `0.6.0`)
|
|
166
|
+
reads a blueprint split anywhere in linear time under a byte budget and a
|
|
167
|
+
depth cap, reports a node only once its shell (everything but
|
|
168
|
+
`children`) has closed, and refuses duplicate and prototype keys. It
|
|
169
|
+
judges no rule: the catalog checks each node, and the complete draft
|
|
170
|
+
goes through `validateDraft`.
|
|
151
171
|
- **Structure only.** This validator checks structure. URL policy, tree
|
|
152
172
|
limits and rendering live in the runtime validator of
|
|
153
173
|
[`@texuli/catalog`](https://www.npmjs.com/package/@texuli/catalog). The
|
|
@@ -190,11 +210,14 @@ catalog hash, and from 0.3.0 may declare the exact spec version it was
|
|
|
190
210
|
written for (`spec`; omitted means 0.2.0). `migrateBlueprint`
|
|
191
211
|
(`@texuli/spec/migrate`) brings an
|
|
192
212
|
envelope up to the current spec with pure, per-version steps
|
|
193
|
-
(`src/migrations/`: `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0
|
|
194
|
-
each of which only stamps `spec`); a compatible version range is planned,
|
|
195
|
-
not built. `@texuli/spec@0.
|
|
196
|
-
`
|
|
197
|
-
|
|
213
|
+
(`src/migrations/`: `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0`, `0.4.0 → 0.5.0` and
|
|
214
|
+
`0.5.0 → 0.6.0`, each of which only stamps `spec`); a compatible version range is planned,
|
|
215
|
+
not built. `@texuli/spec@0.6.0` carries spec `0.6.0` (digest
|
|
216
|
+
`37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc`), which
|
|
217
|
+
adds the closed motion vocabulary (ADR-012); `@texuli/spec@0.5.0` carried
|
|
218
|
+
spec `0.5.0`, which added the `data-cohort-table`,
|
|
219
|
+
`data-notification-inbox`, `data-filter-builder` and `time-field` catalog
|
|
220
|
+
ids, and `@texuli/spec@0.4.0` spec `0.4.0` (digest
|
|
198
221
|
`fac29f542b16ddb78ab401c508e5b8346f655991c56e25aa811f4931661a8cb3`). Every
|
|
199
222
|
pin is in `versions.json`.
|
|
200
223
|
|
|
@@ -211,6 +234,10 @@ conformance harness) are in
|
|
|
211
234
|
[packages/spec](https://github.com/DailybotHQ/texuli/tree/main/packages/spec)
|
|
212
235
|
- The contract in depth (the JSON Schema dialect, projections, conformance
|
|
213
236
|
and versioning): [contract.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/contract.md)
|
|
237
|
+
- The motion vocabulary (spec 0.6.0):
|
|
238
|
+
[motion.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/motion.md)
|
|
239
|
+
- Streamed blueprints (the incremental tokenizer, the canonical order and
|
|
240
|
+
node shells; new in `0.6.0`): [stream.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/stream.md)
|
|
214
241
|
- The Kotlin Multiplatform twin that consumes the generated models:
|
|
215
242
|
[native/README.md](https://github.com/DailybotHQ/texuli/blob/main/native/README.md)
|
|
216
243
|
- Architecture: [ARCHITECTURE.md](https://github.com/DailybotHQ/texuli/blob/main/docs/ARCHITECTURE.md)
|
|
@@ -47,7 +47,10 @@ export interface BlueprintEditOp {
|
|
|
47
47
|
tree?: BlueprintNode;
|
|
48
48
|
}
|
|
49
49
|
export type BlueprintNodePath = readonly number[];
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* One catalog node: id, props, children (each with an optional named slot), when, bind, on and
|
|
52
|
+
* motion.
|
|
53
|
+
*/
|
|
51
54
|
export interface BlueprintNode {
|
|
52
55
|
/** A catalog component id. */
|
|
53
56
|
id: string;
|
|
@@ -63,6 +66,7 @@ export interface BlueprintNode {
|
|
|
63
66
|
/** Two-way bindings: a prop the component lists as bindable → a $state binding it writes back. */
|
|
64
67
|
bind?: Readonly<Record<string, BlueprintBinding>>;
|
|
65
68
|
on?: BlueprintEventMap;
|
|
69
|
+
motion?: BlueprintNodeMotion;
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* Shows the node when true (audit G07): the closed Condition AST of the blocks formula grammar.
|
|
@@ -185,11 +189,47 @@ export interface BlueprintReorder {
|
|
|
185
189
|
pull?: 'move' | 'clone' | 'none';
|
|
186
190
|
disabled?: boolean;
|
|
187
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* A closed motion preset (ADR-013): fade, rise (fade in from the block end), scale (fade and grow
|
|
194
|
+
* from 96%), slide-start or slide-end (fade in from the inline start or end, mirrored right to
|
|
195
|
+
* left). The runtime maps each to token keyframes; a blueprint never carries keyframes.
|
|
196
|
+
*/
|
|
197
|
+
export type BlueprintMotionPreset = 'fade' | 'rise' | 'scale' | 'slide-start' | 'slide-end';
|
|
198
|
+
/** A duration token step (@texuli/tokens motion durations), never milliseconds. */
|
|
199
|
+
export type BlueprintMotionDuration = 'fast' | 'base' | 'slow';
|
|
200
|
+
/**
|
|
201
|
+
* The offset between the entering children of this node, in token steps: none, fast or base. The
|
|
202
|
+
* runtime caps the whole spread.
|
|
203
|
+
*/
|
|
204
|
+
export type BlueprintMotionStagger = 'none' | 'fast' | 'base';
|
|
205
|
+
/**
|
|
206
|
+
* A motion ceiling (ADR-013): full, subtle (shorter, no travel or scale) or none (instant). The
|
|
207
|
+
* host and the reader's reduced-motion preference can only lower it.
|
|
208
|
+
*/
|
|
209
|
+
export type BlueprintMotionLevel = 'full' | 'subtle' | 'none';
|
|
210
|
+
/**
|
|
211
|
+
* How a node moves (spec 0.6.0, ADR-012): named presets and token steps only. Nothing here is CSS,
|
|
212
|
+
* a duration in milliseconds or code. The server paints the end state, and nothing plays on
|
|
213
|
+
* hydration or under reduced motion.
|
|
214
|
+
*/
|
|
215
|
+
export interface BlueprintNodeMotion {
|
|
216
|
+
enter?: BlueprintMotionPreset;
|
|
217
|
+
exit?: BlueprintMotionPreset;
|
|
218
|
+
/** Animate a change of this node's position or size among its siblings (FLIP). */
|
|
219
|
+
layout?: boolean;
|
|
220
|
+
stagger?: BlueprintMotionStagger;
|
|
221
|
+
duration?: BlueprintMotionDuration;
|
|
222
|
+
}
|
|
223
|
+
/** The blueprint's motion settings (spec 0.6.0): the level every node motion plays at. */
|
|
224
|
+
export interface BlueprintEnvelopeMotion {
|
|
225
|
+
level: BlueprintMotionLevel;
|
|
226
|
+
}
|
|
188
227
|
/**
|
|
189
228
|
* Envelope version 1 around one catalog tree (ADR-004, audit G13). Mirrors what
|
|
190
229
|
* @texuli/catalog/envelope reads; a declared status, activation or tier is display data, never
|
|
191
230
|
* authority. Spec 0.4.0 (ADR-008 + PLAN_008 deck encoding) adds an optional spec version, local
|
|
192
|
-
* state and a flow.
|
|
231
|
+
* state and a flow; spec 0.6.0 (ADR-012, ADR-013) adds the closed motion vocabulary: a node motion
|
|
232
|
+
* and the envelope motion level. Planned minor additions: the native profile and several roots.
|
|
193
233
|
*/
|
|
194
234
|
export interface BlueprintEnvelope {
|
|
195
235
|
format: 'texuli-blueprint';
|
|
@@ -213,11 +253,12 @@ export interface BlueprintEnvelope {
|
|
|
213
253
|
/**
|
|
214
254
|
* The spec version the envelope was written for. Omitted means 0.2.0; migrateBlueprint stamps it.
|
|
215
255
|
*/
|
|
216
|
-
spec?: '0.3.0' | '0.4.0' | '0.5.0';
|
|
256
|
+
spec?: '0.3.0' | '0.4.0' | '0.5.0' | '0.6.0';
|
|
217
257
|
/**
|
|
218
258
|
* Local state declarations (ADR-008), at most 64. Names, counts and per-type rules are checked by
|
|
219
259
|
* the validator.
|
|
220
260
|
*/
|
|
221
261
|
state?: Readonly<Record<string, BlueprintStateDeclaration>>;
|
|
222
262
|
flow?: BlueprintFlow;
|
|
263
|
+
motion?: BlueprintEnvelopeMotion;
|
|
223
264
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/blueprint.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
|
|
3
3
|
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/catalog.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
|
|
3
3
|
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/diagnostics.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
|
|
3
3
|
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/manifest.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
|
|
3
3
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by @texuli/spec from catalog/components.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
|
|
3
3
|
/** The built catalog this vocabulary was projected from. */
|
|
4
4
|
export const SPEC_CATALOG_HASH = '4ac7ecedd1c59d1999a9f7eea2f523c7653fececb97258a478f09253f6b6057f';
|
|
5
5
|
/** Every closed component id, sorted. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/workflow.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
|
|
3
3
|
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SpecMigration } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 0.5.0 → 0.6.0 is additive (PLAN_010 T-motion-spec, ADR-012 and ADR-013):
|
|
4
|
+
* nodes gain an optional closed `motion` (enter and exit presets, `layout`,
|
|
5
|
+
* `stagger` and `duration` in token steps) and the envelope an optional
|
|
6
|
+
* `motion.level`. Every 0.5.0 envelope is a valid 0.6.0 envelope, so the
|
|
7
|
+
* step only stamps `spec`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const migration050to060: SpecMigration;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.5.0 → 0.6.0 is additive (PLAN_010 T-motion-spec, ADR-012 and ADR-013):
|
|
3
|
+
* nodes gain an optional closed `motion` (enter and exit presets, `layout`,
|
|
4
|
+
* `stagger` and `duration` in token steps) and the envelope an optional
|
|
5
|
+
* `motion.level`. Every 0.5.0 envelope is a valid 0.6.0 envelope, so the
|
|
6
|
+
* step only stamps `spec`.
|
|
7
|
+
*/
|
|
8
|
+
export const migration050to060 = Object.freeze({
|
|
9
|
+
from: '0.5.0',
|
|
10
|
+
to: '0.6.0',
|
|
11
|
+
migrate: (envelope) => ({ ...envelope, spec: '0.6.0' }),
|
|
12
|
+
});
|
package/dist/migrations/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SPEC_VERSION } from "../version.js";
|
|
|
3
3
|
import { migration020to030 } from "./0.2.0-0.3.0.js";
|
|
4
4
|
import { migration030to040 } from "./0.3.0-0.4.0.js";
|
|
5
5
|
import { migration040to050 } from "./0.4.0-0.5.0.js";
|
|
6
|
+
import { migration050to060 } from "./0.5.0-0.6.0.js";
|
|
6
7
|
/** The version an envelope without `spec` was written for. */
|
|
7
8
|
export const UNDECLARED_SPEC_VERSION = '0.2.0';
|
|
8
9
|
/** Every step, in order; each `to` is the next step's `from`. */
|
|
@@ -10,6 +11,7 @@ export const SPEC_MIGRATIONS = Object.freeze([
|
|
|
10
11
|
migration020to030,
|
|
11
12
|
migration030to040,
|
|
12
13
|
migration040to050,
|
|
14
|
+
migration050to060,
|
|
13
15
|
]);
|
|
14
16
|
/**
|
|
15
17
|
* Bring a blueprint envelope up to `target` (the current spec by default)
|
package/dist/motion.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/spec/motion` — the closed motion vocabulary of spec 0.6.0
|
|
3
|
+
* (ADR-012, ADR-013) in code, mirrored from `schemas/blueprint.schema.json`
|
|
4
|
+
* (`tests/spec.test.ts` asserts they stay equal), and the one structural
|
|
5
|
+
* check every runtime shares: `@texuli/catalog` refuses with it, and
|
|
6
|
+
* `@texuli/player` paints only what it accepted.
|
|
7
|
+
*
|
|
8
|
+
* Agents name presets and token steps. Nothing here is CSS, keyframes, a
|
|
9
|
+
* duration in milliseconds or code. No imports at runtime.
|
|
10
|
+
*/
|
|
11
|
+
import type { BlueprintEnvelopeMotion, BlueprintMotionDuration, BlueprintMotionLevel, BlueprintMotionPreset, BlueprintMotionStagger, BlueprintNodeMotion } from './generated/typescript/blueprint.js';
|
|
12
|
+
export type { BlueprintEnvelopeMotion, BlueprintMotionDuration, BlueprintMotionLevel, BlueprintMotionPreset, BlueprintMotionStagger, BlueprintNodeMotion, };
|
|
13
|
+
/** Enter and exit presets (`$defs.motionPreset`). */
|
|
14
|
+
export declare const SPEC_MOTION_PRESETS: readonly BlueprintMotionPreset[];
|
|
15
|
+
/** Duration token steps (`$defs.motionDuration`); `base` when omitted. */
|
|
16
|
+
export declare const SPEC_MOTION_DURATIONS: readonly BlueprintMotionDuration[];
|
|
17
|
+
/** Stagger token steps between entering children (`$defs.motionStagger`). */
|
|
18
|
+
export declare const SPEC_MOTION_STAGGERS: readonly BlueprintMotionStagger[];
|
|
19
|
+
/** Motion ceilings (`$defs.motionLevel`), highest first. */
|
|
20
|
+
export declare const SPEC_MOTION_LEVELS: readonly BlueprintMotionLevel[];
|
|
21
|
+
/** The fields a node `motion` takes (`$defs.nodeMotion`). */
|
|
22
|
+
export declare const SPEC_NODE_MOTION_FIELDS: readonly string[];
|
|
23
|
+
export type SpecMotionCheck<T> = {
|
|
24
|
+
ok: true;
|
|
25
|
+
motion: Readonly<T>;
|
|
26
|
+
}
|
|
27
|
+
/** `path` is relative to the motion value (`""`, `.enter` …). */
|
|
28
|
+
| {
|
|
29
|
+
ok: false;
|
|
30
|
+
path: string;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Check a node `motion` against the closed vocabulary. Returns a frozen
|
|
35
|
+
* copy with only the known fields, or the first refusal. Linear in the
|
|
36
|
+
* number of keys, and it stops at the first unknown one.
|
|
37
|
+
*/
|
|
38
|
+
export declare function checkNodeMotion(value: unknown): SpecMotionCheck<BlueprintNodeMotion>;
|
|
39
|
+
/** Check an envelope `motion` (`{ level }`). Returns a frozen copy, or the first refusal. */
|
|
40
|
+
export declare function checkEnvelopeMotion(value: unknown): SpecMotionCheck<BlueprintEnvelopeMotion>;
|
package/dist/motion.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/spec/motion` — the closed motion vocabulary of spec 0.6.0
|
|
3
|
+
* (ADR-012, ADR-013) in code, mirrored from `schemas/blueprint.schema.json`
|
|
4
|
+
* (`tests/spec.test.ts` asserts they stay equal), and the one structural
|
|
5
|
+
* check every runtime shares: `@texuli/catalog` refuses with it, and
|
|
6
|
+
* `@texuli/player` paints only what it accepted.
|
|
7
|
+
*
|
|
8
|
+
* Agents name presets and token steps. Nothing here is CSS, keyframes, a
|
|
9
|
+
* duration in milliseconds or code. No imports at runtime.
|
|
10
|
+
*/
|
|
11
|
+
/** Enter and exit presets (`$defs.motionPreset`). */
|
|
12
|
+
export const SPEC_MOTION_PRESETS = Object.freeze([
|
|
13
|
+
'fade',
|
|
14
|
+
'rise',
|
|
15
|
+
'scale',
|
|
16
|
+
'slide-start',
|
|
17
|
+
'slide-end',
|
|
18
|
+
]);
|
|
19
|
+
/** Duration token steps (`$defs.motionDuration`); `base` when omitted. */
|
|
20
|
+
export const SPEC_MOTION_DURATIONS = Object.freeze([
|
|
21
|
+
'fast',
|
|
22
|
+
'base',
|
|
23
|
+
'slow',
|
|
24
|
+
]);
|
|
25
|
+
/** Stagger token steps between entering children (`$defs.motionStagger`). */
|
|
26
|
+
export const SPEC_MOTION_STAGGERS = Object.freeze([
|
|
27
|
+
'none',
|
|
28
|
+
'fast',
|
|
29
|
+
'base',
|
|
30
|
+
]);
|
|
31
|
+
/** Motion ceilings (`$defs.motionLevel`), highest first. */
|
|
32
|
+
export const SPEC_MOTION_LEVELS = Object.freeze([
|
|
33
|
+
'full',
|
|
34
|
+
'subtle',
|
|
35
|
+
'none',
|
|
36
|
+
]);
|
|
37
|
+
/** The fields a node `motion` takes (`$defs.nodeMotion`). */
|
|
38
|
+
export const SPEC_NODE_MOTION_FIELDS = Object.freeze([
|
|
39
|
+
'enter',
|
|
40
|
+
'exit',
|
|
41
|
+
'layout',
|
|
42
|
+
'stagger',
|
|
43
|
+
'duration',
|
|
44
|
+
]);
|
|
45
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
46
|
+
/** A short, single-line label for a refused value: never the value itself in full. */
|
|
47
|
+
function describe(value) {
|
|
48
|
+
if (typeof value === 'string')
|
|
49
|
+
return JSON.stringify(value.slice(0, 24));
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return 'an array';
|
|
52
|
+
return value === null ? 'null' : typeof value;
|
|
53
|
+
}
|
|
54
|
+
function oneOf(value, allowed, field) {
|
|
55
|
+
if (typeof value === 'string' && allowed.includes(value)) {
|
|
56
|
+
return { ok: true, value: value };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
path: `.${field}`,
|
|
61
|
+
message: `"${field}" must be one of ${allowed.join(', ')} (got ${describe(value)}).`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check a node `motion` against the closed vocabulary. Returns a frozen
|
|
66
|
+
* copy with only the known fields, or the first refusal. Linear in the
|
|
67
|
+
* number of keys, and it stops at the first unknown one.
|
|
68
|
+
*/
|
|
69
|
+
export function checkNodeMotion(value) {
|
|
70
|
+
if (!isRecord(value)) {
|
|
71
|
+
return { ok: false, path: '', message: `"motion" must be an object (got ${describe(value)}).` };
|
|
72
|
+
}
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const key of Object.keys(value)) {
|
|
75
|
+
if (!SPEC_NODE_MOTION_FIELDS.includes(key)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
path: `.${key.slice(0, 32)}`,
|
|
79
|
+
message: `"motion" takes only ${SPEC_NODE_MOTION_FIELDS.join(', ')}; no CSS, keyframes or code.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const entry = value[key];
|
|
83
|
+
if (key === 'layout') {
|
|
84
|
+
if (typeof entry !== 'boolean') {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
path: '.layout',
|
|
88
|
+
message: `"layout" must be a boolean (got ${describe(entry)}).`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
out.layout = entry;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (key === 'enter' || key === 'exit') {
|
|
95
|
+
const checked = oneOf(entry, SPEC_MOTION_PRESETS, key);
|
|
96
|
+
if (!checked.ok)
|
|
97
|
+
return checked;
|
|
98
|
+
out[key] = checked.value;
|
|
99
|
+
}
|
|
100
|
+
else if (key === 'stagger') {
|
|
101
|
+
const checked = oneOf(entry, SPEC_MOTION_STAGGERS, key);
|
|
102
|
+
if (!checked.ok)
|
|
103
|
+
return checked;
|
|
104
|
+
out.stagger = checked.value;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const checked = oneOf(entry, SPEC_MOTION_DURATIONS, key);
|
|
108
|
+
if (!checked.ok)
|
|
109
|
+
return checked;
|
|
110
|
+
out.duration = checked.value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, motion: Object.freeze(out) };
|
|
114
|
+
}
|
|
115
|
+
/** Check an envelope `motion` (`{ level }`). Returns a frozen copy, or the first refusal. */
|
|
116
|
+
export function checkEnvelopeMotion(value) {
|
|
117
|
+
if (!isRecord(value)) {
|
|
118
|
+
return { ok: false, path: '', message: `"motion" must be an object (got ${describe(value)}).` };
|
|
119
|
+
}
|
|
120
|
+
for (const key of Object.keys(value)) {
|
|
121
|
+
if (key !== 'level') {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
path: `.${key.slice(0, 32)}`,
|
|
125
|
+
message: 'The envelope "motion" takes only "level".',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const level = oneOf(value.level, SPEC_MOTION_LEVELS, 'level');
|
|
130
|
+
if (!level.ok)
|
|
131
|
+
return level;
|
|
132
|
+
return { ok: true, motion: Object.freeze({ level: level.value }) };
|
|
133
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type JsonTokenizerOptions, type StreamError } from './tokenizer.js';
|
|
2
|
+
/**
|
|
3
|
+
* The partial-blueprint assembler (ADR-014): builds the value the tokenizer
|
|
4
|
+
* reads and reports, for every catalog node, when its **shell** is final.
|
|
5
|
+
* Streamed nodes put `children` last (`key`, `id`, `slot`, `props`, `when`,
|
|
6
|
+
* `bind`, `on`, `children`), so a node's shell (every member but `children`)
|
|
7
|
+
* is final once `children` starts, or once the node closes without it.
|
|
8
|
+
* Envelopes put `state` and `flow` before the tree, so the envelope header
|
|
9
|
+
* is final once `tree` starts. A member after `children` (or after `tree`)
|
|
10
|
+
* is a `stream-order` issue: the node it lands in is held until the final
|
|
11
|
+
* verdict. The assembler judges no rule itself; the catalog checks each
|
|
12
|
+
* shell and `validateDraft` judges the whole value.
|
|
13
|
+
*/
|
|
14
|
+
/** Members a streamed node may have, in the canonical order (`children` last). */
|
|
15
|
+
export declare const STREAM_NODE_ORDER: readonly string[];
|
|
16
|
+
/** A node's members except `children`. Values are the assembled ones (not copies). */
|
|
17
|
+
export type StreamNodeShell = Readonly<Record<string, unknown>>;
|
|
18
|
+
export type BlueprintStreamEvent =
|
|
19
|
+
/** An envelope's members before its tree (`final` once the tree starts; again for a late member). */
|
|
20
|
+
{
|
|
21
|
+
type: 'header';
|
|
22
|
+
header: Readonly<Record<string, unknown>>;
|
|
23
|
+
offset: number;
|
|
24
|
+
}
|
|
25
|
+
/** A node object opened. Its shell is not final yet. */
|
|
26
|
+
| {
|
|
27
|
+
type: 'node-start';
|
|
28
|
+
path: string;
|
|
29
|
+
parent: string | null;
|
|
30
|
+
index: number;
|
|
31
|
+
/** `true` for an element of an array (children, or an array of roots). */
|
|
32
|
+
inArray: boolean;
|
|
33
|
+
offset: number;
|
|
34
|
+
}
|
|
35
|
+
/** A node's `id` member closed (for a skeleton of the right family). */
|
|
36
|
+
| {
|
|
37
|
+
type: 'node-id';
|
|
38
|
+
path: string;
|
|
39
|
+
id: unknown;
|
|
40
|
+
offset: number;
|
|
41
|
+
}
|
|
42
|
+
/** A node's shell is final: every member but `children` arrived. */
|
|
43
|
+
| {
|
|
44
|
+
type: 'node';
|
|
45
|
+
path: string;
|
|
46
|
+
/** The node's stable stream id: its `key` when it has a string one, else its path. */
|
|
47
|
+
streamId: string;
|
|
48
|
+
parent: string | null;
|
|
49
|
+
index: number;
|
|
50
|
+
shell: StreamNodeShell;
|
|
51
|
+
/** `children` started (more nodes follow under this one). */
|
|
52
|
+
hasChildren: boolean;
|
|
53
|
+
/** `true` for an element of an array (children, or an array of roots). */
|
|
54
|
+
inArray: boolean;
|
|
55
|
+
offset: number;
|
|
56
|
+
}
|
|
57
|
+
/** A node object closed: all its children arrived. */
|
|
58
|
+
| {
|
|
59
|
+
type: 'node-end';
|
|
60
|
+
path: string;
|
|
61
|
+
offset: number;
|
|
62
|
+
}
|
|
63
|
+
/** A member after `children` (node) or after the tree (envelope, path `$`). */
|
|
64
|
+
| {
|
|
65
|
+
type: 'stream-order';
|
|
66
|
+
path: string;
|
|
67
|
+
key: string;
|
|
68
|
+
offset: number;
|
|
69
|
+
}
|
|
70
|
+
/** The whole value arrived and the stream ended cleanly. */
|
|
71
|
+
| {
|
|
72
|
+
type: 'complete';
|
|
73
|
+
value: unknown;
|
|
74
|
+
offset: number;
|
|
75
|
+
}
|
|
76
|
+
/** The stream stopped: nothing more is reported. */
|
|
77
|
+
| {
|
|
78
|
+
type: 'error';
|
|
79
|
+
error: StreamError;
|
|
80
|
+
};
|
|
81
|
+
export interface BlueprintAssemblerOptions extends JsonTokenizerOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Root members that hold the tree. Default `['tree']` (the blueprint
|
|
84
|
+
* envelope). A host that also streams another wrapper (the player's
|
|
85
|
+
* local-state wrapper uses `blueprint`) adds its member here.
|
|
86
|
+
*/
|
|
87
|
+
treeMembers?: readonly string[];
|
|
88
|
+
}
|
|
89
|
+
export interface BlueprintAssembler {
|
|
90
|
+
/** Feed a chunk; returns what it completed. */
|
|
91
|
+
write(chunk: string): BlueprintStreamEvent[];
|
|
92
|
+
/** No more input. Returns `complete` or the `truncated` error. */
|
|
93
|
+
end(): BlueprintStreamEvent[];
|
|
94
|
+
/** Stop with an `aborted` or `connection` error (a dropped stream). */
|
|
95
|
+
abort(code?: 'aborted' | 'connection', message?: string): BlueprintStreamEvent[];
|
|
96
|
+
readonly bytes: number;
|
|
97
|
+
readonly error: StreamError | null;
|
|
98
|
+
/** The value assembled so far (containers still open are partial). */
|
|
99
|
+
readonly value: unknown;
|
|
100
|
+
}
|
|
101
|
+
export declare function createBlueprintAssembler(options?: BlueprintAssemblerOptions): BlueprintAssembler;
|