@czap/core 0.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/addressed-digest.d.ts +15 -0
- package/dist/addressed-digest.d.ts.map +1 -0
- package/dist/addressed-digest.js +35 -0
- package/dist/addressed-digest.js.map +1 -0
- package/dist/animation.d.ts +46 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +70 -0
- package/dist/animation.js.map +1 -0
- package/dist/assembly.d.ts +25 -0
- package/dist/assembly.d.ts.map +1 -0
- package/dist/assembly.js +58 -0
- package/dist/assembly.js.map +1 -0
- package/dist/av-bridge.d.ts +74 -0
- package/dist/av-bridge.d.ts.map +1 -0
- package/dist/av-bridge.js +107 -0
- package/dist/av-bridge.js.map +1 -0
- package/dist/av-renderer.d.ts +56 -0
- package/dist/av-renderer.d.ts.map +1 -0
- package/dist/av-renderer.js +65 -0
- package/dist/av-renderer.js.map +1 -0
- package/dist/blend.d.ts +61 -0
- package/dist/blend.d.ts.map +1 -0
- package/dist/blend.js +100 -0
- package/dist/blend.js.map +1 -0
- package/dist/boundary.d.ts +154 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +269 -0
- package/dist/boundary.js.map +1 -0
- package/dist/brands.d.ts +63 -0
- package/dist/brands.d.ts.map +1 -0
- package/dist/brands.js +31 -0
- package/dist/brands.js.map +1 -0
- package/dist/caps.d.ts +49 -0
- package/dist/caps.d.ts.map +1 -0
- package/dist/caps.js +73 -0
- package/dist/caps.js.map +1 -0
- package/dist/capsule.d.ts +77 -0
- package/dist/capsule.d.ts.map +1 -0
- package/dist/capsule.js +18 -0
- package/dist/capsule.js.map +1 -0
- package/dist/capsules/boundary-evaluate.d.ts +28 -0
- package/dist/capsules/boundary-evaluate.d.ts.map +1 -0
- package/dist/capsules/boundary-evaluate.js +117 -0
- package/dist/capsules/boundary-evaluate.js.map +1 -0
- package/dist/capsules/canonical-cbor.d.ts +13 -0
- package/dist/capsules/canonical-cbor.d.ts.map +1 -0
- package/dist/capsules/canonical-cbor.js +60 -0
- package/dist/capsules/canonical-cbor.js.map +1 -0
- package/dist/capsules/token-buffer.d.ts +24 -0
- package/dist/capsules/token-buffer.d.ts.map +1 -0
- package/dist/capsules/token-buffer.js +53 -0
- package/dist/capsules/token-buffer.js.map +1 -0
- package/dist/capture.d.ts +40 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +10 -0
- package/dist/capture.js.map +1 -0
- package/dist/cbor.d.ts +33 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +179 -0
- package/dist/cbor.js.map +1 -0
- package/dist/cell.d.ts +53 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +83 -0
- package/dist/cell.js.map +1 -0
- package/dist/codec.d.ts +30 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +25 -0
- package/dist/codec.js.map +1 -0
- package/dist/component.d.ts +52 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +44 -0
- package/dist/component.js.map +1 -0
- package/dist/composable.d.ts +76 -0
- package/dist/composable.d.ts.map +1 -0
- package/dist/composable.js +221 -0
- package/dist/composable.js.map +1 -0
- package/dist/compositor-pool.d.ts +74 -0
- package/dist/compositor-pool.d.ts.map +1 -0
- package/dist/compositor-pool.js +119 -0
- package/dist/compositor-pool.js.map +1 -0
- package/dist/compositor.d.ts +90 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +278 -0
- package/dist/compositor.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/dag.d.ts +251 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +450 -0
- package/dist/dag.js.map +1 -0
- package/dist/defaults.d.ts +45 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +45 -0
- package/dist/defaults.js.map +1 -0
- package/dist/derived.d.ts +34 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +101 -0
- package/dist/derived.js.map +1 -0
- package/dist/diagnostics.d.ts +77 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dirty.d.ts +55 -0
- package/dist/dirty.d.ts.map +1 -0
- package/dist/dirty.js +80 -0
- package/dist/dirty.js.map +1 -0
- package/dist/easing.d.ts +55 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +291 -0
- package/dist/easing.js.map +1 -0
- package/dist/ecs.d.ts +105 -0
- package/dist/ecs.d.ts.map +1 -0
- package/dist/ecs.js +245 -0
- package/dist/ecs.js.map +1 -0
- package/dist/fnv.d.ts +14 -0
- package/dist/fnv.d.ts.map +1 -0
- package/dist/fnv.js +28 -0
- package/dist/fnv.js.map +1 -0
- package/dist/frame-budget.d.ts +73 -0
- package/dist/frame-budget.d.ts.map +1 -0
- package/dist/frame-budget.js +114 -0
- package/dist/frame-budget.js.map +1 -0
- package/dist/gen-frame.d.ts +102 -0
- package/dist/gen-frame.d.ts.map +1 -0
- package/dist/gen-frame.js +121 -0
- package/dist/gen-frame.js.map +1 -0
- package/dist/harness/arbitrary-from-schema.d.ts +28 -0
- package/dist/harness/arbitrary-from-schema.d.ts.map +1 -0
- package/dist/harness/arbitrary-from-schema.js +262 -0
- package/dist/harness/arbitrary-from-schema.js.map +1 -0
- package/dist/harness/cached-projection.d.ts +19 -0
- package/dist/harness/cached-projection.d.ts.map +1 -0
- package/dist/harness/cached-projection.js +39 -0
- package/dist/harness/cached-projection.js.map +1 -0
- package/dist/harness/index.d.ts +16 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +15 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/policy-gate.d.ts +18 -0
- package/dist/harness/policy-gate.d.ts.map +1 -0
- package/dist/harness/policy-gate.js +46 -0
- package/dist/harness/policy-gate.js.map +1 -0
- package/dist/harness/pure-transform.d.ts +42 -0
- package/dist/harness/pure-transform.d.ts.map +1 -0
- package/dist/harness/pure-transform.js +76 -0
- package/dist/harness/pure-transform.js.map +1 -0
- package/dist/harness/receipted-mutation.d.ts +23 -0
- package/dist/harness/receipted-mutation.d.ts.map +1 -0
- package/dist/harness/receipted-mutation.js +52 -0
- package/dist/harness/receipted-mutation.js.map +1 -0
- package/dist/harness/scene-composition.d.ts +19 -0
- package/dist/harness/scene-composition.d.ts.map +1 -0
- package/dist/harness/scene-composition.js +47 -0
- package/dist/harness/scene-composition.js.map +1 -0
- package/dist/harness/site-adapter.d.ts +18 -0
- package/dist/harness/site-adapter.d.ts.map +1 -0
- package/dist/harness/site-adapter.js +38 -0
- package/dist/harness/site-adapter.js.map +1 -0
- package/dist/harness/state-machine.d.ts +19 -0
- package/dist/harness/state-machine.d.ts.map +1 -0
- package/dist/harness/state-machine.js +44 -0
- package/dist/harness/state-machine.js.map +1 -0
- package/dist/hlc.d.ts +99 -0
- package/dist/hlc.d.ts.map +1 -0
- package/dist/hlc.js +219 -0
- package/dist/hlc.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.d.ts +14 -0
- package/dist/interpolate.d.ts.map +1 -0
- package/dist/interpolate.js +31 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/live-cell.d.ts +46 -0
- package/dist/live-cell.d.ts.map +1 -0
- package/dist/live-cell.js +154 -0
- package/dist/live-cell.js.map +1 -0
- package/dist/op.d.ts +58 -0
- package/dist/op.d.ts.map +1 -0
- package/dist/op.js +171 -0
- package/dist/op.js.map +1 -0
- package/dist/plan.d.ts +195 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +211 -0
- package/dist/plan.js.map +1 -0
- package/dist/protocol.d.ts +33 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +10 -0
- package/dist/protocol.js.map +1 -0
- package/dist/quantizer-types.d.ts +28 -0
- package/dist/quantizer-types.d.ts.map +1 -0
- package/dist/quantizer-types.js +9 -0
- package/dist/quantizer-types.js.map +1 -0
- package/dist/receipt.d.ts +294 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +352 -0
- package/dist/receipt.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +75 -0
- package/dist/runtime-coordinator.d.ts.map +1 -0
- package/dist/runtime-coordinator.js +149 -0
- package/dist/runtime-coordinator.js.map +1 -0
- package/dist/scheduler.d.ts +58 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +109 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/ship-capsule.d.ts +54 -0
- package/dist/ship-capsule.d.ts.map +1 -0
- package/dist/ship-capsule.js +142 -0
- package/dist/ship-capsule.js.map +1 -0
- package/dist/ship-manifest.d.ts +45 -0
- package/dist/ship-manifest.d.ts.map +1 -0
- package/dist/ship-manifest.js +175 -0
- package/dist/ship-manifest.js.map +1 -0
- package/dist/signal.d.ts +149 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +277 -0
- package/dist/signal.js.map +1 -0
- package/dist/speculative.d.ts +67 -0
- package/dist/speculative.d.ts.map +1 -0
- package/dist/speculative.js +139 -0
- package/dist/speculative.js.map +1 -0
- package/dist/store.d.ts +39 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +42 -0
- package/dist/store.js.map +1 -0
- package/dist/style.d.ts +119 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +168 -0
- package/dist/style.js.map +1 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +14 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme.d.ts +78 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +109 -0
- package/dist/theme.js.map +1 -0
- package/dist/timeline.d.ts +45 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +101 -0
- package/dist/timeline.js.map +1 -0
- package/dist/token-buffer.d.ts +43 -0
- package/dist/token-buffer.d.ts.map +1 -0
- package/dist/token-buffer.js +112 -0
- package/dist/token-buffer.js.map +1 -0
- package/dist/token.d.ts +107 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +143 -0
- package/dist/token.js.map +1 -0
- package/dist/tuple.d.ts +16 -0
- package/dist/tuple.d.ts.map +1 -0
- package/dist/tuple.js +16 -0
- package/dist/tuple.js.map +1 -0
- package/dist/type-utils.d.ts +41 -0
- package/dist/type-utils.d.ts.map +1 -0
- package/dist/type-utils.js +10 -0
- package/dist/type-utils.js.map +1 -0
- package/dist/typed-ref.d.ts +50 -0
- package/dist/typed-ref.d.ts.map +1 -0
- package/dist/typed-ref.js +59 -0
- package/dist/typed-ref.js.map +1 -0
- package/dist/ui-quality.d.ts +50 -0
- package/dist/ui-quality.d.ts.map +1 -0
- package/dist/ui-quality.js +64 -0
- package/dist/ui-quality.js.map +1 -0
- package/dist/validation-error.d.ts +25 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +32 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/vector-clock.d.ts +46 -0
- package/dist/vector-clock.d.ts.map +1 -0
- package/dist/vector-clock.js +91 -0
- package/dist/vector-clock.js.map +1 -0
- package/dist/video.d.ts +62 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +59 -0
- package/dist/video.js.map +1 -0
- package/dist/wasm-dispatch.d.ts +52 -0
- package/dist/wasm-dispatch.d.ts.map +1 -0
- package/dist/wasm-dispatch.js +204 -0
- package/dist/wasm-dispatch.js.map +1 -0
- package/dist/wasm-fallback.d.ts +19 -0
- package/dist/wasm-fallback.d.ts.map +1 -0
- package/dist/wasm-fallback.js +93 -0
- package/dist/wasm-fallback.js.map +1 -0
- package/dist/wire.d.ts +49 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +201 -0
- package/dist/wire.js.map +1 -0
- package/dist/zap.d.ts +42 -0
- package/dist/zap.d.ts.map +1 -0
- package/dist/zap.js +172 -0
- package/dist/zap.js.map +1 -0
- package/package.json +71 -0
- package/src/addressed-digest.ts +48 -0
- package/src/animation.ts +103 -0
- package/src/assembly.ts +76 -0
- package/src/av-bridge.ts +161 -0
- package/src/av-renderer.ts +118 -0
- package/src/blend.ts +135 -0
- package/src/boundary.ts +363 -0
- package/src/brands.ts +86 -0
- package/src/caps.ts +100 -0
- package/src/capsule.ts +95 -0
- package/src/capsules/boundary-evaluate.ts +128 -0
- package/src/capsules/canonical-cbor.ts +60 -0
- package/src/capsules/token-buffer.ts +57 -0
- package/src/capture.ts +48 -0
- package/src/cbor.ts +199 -0
- package/src/cell.ts +130 -0
- package/src/codec.ts +39 -0
- package/src/component.ts +102 -0
- package/src/composable.ts +328 -0
- package/src/compositor-pool.ts +162 -0
- package/src/compositor.ts +387 -0
- package/src/config.ts +157 -0
- package/src/dag.ts +527 -0
- package/src/defaults.ts +60 -0
- package/src/derived.ts +164 -0
- package/src/diagnostics.ts +186 -0
- package/src/dirty.ts +101 -0
- package/src/easing.ts +334 -0
- package/src/ecs.ts +382 -0
- package/src/fnv.ts +31 -0
- package/src/frame-budget.ts +149 -0
- package/src/gen-frame.ts +229 -0
- package/src/harness/arbitrary-from-schema.ts +270 -0
- package/src/harness/cached-projection.ts +46 -0
- package/src/harness/index.ts +16 -0
- package/src/harness/policy-gate.ts +51 -0
- package/src/harness/pure-transform.ts +121 -0
- package/src/harness/receipted-mutation.ts +59 -0
- package/src/harness/scene-composition.ts +54 -0
- package/src/harness/site-adapter.ts +43 -0
- package/src/harness/state-machine.ts +49 -0
- package/src/hlc.ts +238 -0
- package/src/index.ts +274 -0
- package/src/interpolate.ts +37 -0
- package/src/live-cell.ts +199 -0
- package/src/op.ts +233 -0
- package/src/plan.ts +317 -0
- package/src/protocol.ts +49 -0
- package/src/quantizer-types.ts +29 -0
- package/src/receipt.ts +444 -0
- package/src/runtime-coordinator.ts +230 -0
- package/src/scheduler.ts +161 -0
- package/src/ship-capsule.ts +191 -0
- package/src/signal.ts +345 -0
- package/src/speculative.ts +186 -0
- package/src/store.ts +77 -0
- package/src/style.ts +249 -0
- package/src/testing.ts +14 -0
- package/src/theme.ts +153 -0
- package/src/timeline.ts +146 -0
- package/src/token-buffer.ts +151 -0
- package/src/token.ts +197 -0
- package/src/tuple.ts +19 -0
- package/src/type-utils.ts +48 -0
- package/src/typed-ref.ts +79 -0
- package/src/ui-quality.ts +105 -0
- package/src/validation-error.ts +34 -0
- package/src/vector-clock.ts +111 -0
- package/src/video.ts +106 -0
- package/src/wasm-dispatch.ts +300 -0
- package/src/wasm-fallback.ts +102 -0
- package/src/wire.ts +274 -0
- package/src/zap.ts +241 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpeculativeEvaluator -- threshold proximity prefetching.
|
|
3
|
+
*
|
|
4
|
+
* When a boundary signal is near a threshold, pre-compute the next state.
|
|
5
|
+
* Uses hysteresis dead zone as the prefetch window and linear extrapolation
|
|
6
|
+
* from velocity (last 3-4 signal values) to predict crossings.
|
|
7
|
+
*
|
|
8
|
+
* Wrong prediction cost: ~80ns to recompute (negligible).
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Boundary } from './boundary.js';
|
|
14
|
+
import type { StateUnion } from './type-utils.js';
|
|
15
|
+
|
|
16
|
+
// Speculative pre-computation: when signal velocity indicates an imminent threshold crossing, pre-evaluate the predicted next state.
|
|
17
|
+
|
|
18
|
+
interface SpeculativeResult<B extends Boundary.Shape> {
|
|
19
|
+
readonly current: StateUnion<B>;
|
|
20
|
+
readonly prefetched?: StateUnion<B>;
|
|
21
|
+
readonly confidence: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SpeculativeEvaluatorShape<B extends Boundary.Shape> {
|
|
25
|
+
evaluate(value: number, velocity?: number): SpeculativeResult<B>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a speculative evaluator for a boundary that prefetches the next state
|
|
30
|
+
* when the signal value is near a threshold and moving toward it.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const boundary = Boundary.make({
|
|
35
|
+
* thresholds: [768, 1024],
|
|
36
|
+
* states: ['mobile', 'tablet', 'desktop'] as const,
|
|
37
|
+
* hysteresis: 20,
|
|
38
|
+
* });
|
|
39
|
+
* const spec = SpeculativeEvaluator.make(boundary);
|
|
40
|
+
* const result = spec.evaluate(760, 2.0); // approaching 768 threshold
|
|
41
|
+
* result.current; // 'mobile'
|
|
42
|
+
* result.prefetched; // 'tablet' (pre-computed)
|
|
43
|
+
* result.confidence; // 0.0-1.0 likelihood of crossing
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function _make<B extends Boundary.Shape>(boundary: B): SpeculativeEvaluatorShape<B> {
|
|
47
|
+
const thresholds = boundary.thresholds as readonly number[];
|
|
48
|
+
const hysteresis = boundary.hysteresis ?? 0;
|
|
49
|
+
const prefetchWindow = Math.max(hysteresis, 1); // Use hysteresis as window, min 1
|
|
50
|
+
|
|
51
|
+
// Compute epsilon from boundary scale rather than hardcoded constant
|
|
52
|
+
const minGap =
|
|
53
|
+
thresholds.length >= 2
|
|
54
|
+
? Math.min(
|
|
55
|
+
...Array.from(
|
|
56
|
+
{ length: thresholds.length - 1 },
|
|
57
|
+
(_, i) => (thresholds[i + 1] as number) - (thresholds[i] as number),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
: 1;
|
|
61
|
+
const epsilon = Math.min(minGap * 0.001, hysteresis > 0 ? hysteresis * 0.01 : 0.001);
|
|
62
|
+
|
|
63
|
+
// Velocity estimation ring buffer (last 2 values)
|
|
64
|
+
const history: { value: number; time: number }[] = [];
|
|
65
|
+
// 2-sample velocity estimation buffer — gives instant responsiveness
|
|
66
|
+
const HISTORY_SIZE = 2;
|
|
67
|
+
|
|
68
|
+
// Boundary.make guarantees states is non-empty (readonly [string, ...string[]]).
|
|
69
|
+
let previousState: StateUnion<B> = boundary.states[0];
|
|
70
|
+
|
|
71
|
+
// Simple finite difference (not least-squares) — 2-sample gives instant responsiveness
|
|
72
|
+
// for UI prefetch.
|
|
73
|
+
function estimateVelocity(currentValue: number, explicitVelocity?: number): number {
|
|
74
|
+
if (explicitVelocity !== undefined) return explicitVelocity;
|
|
75
|
+
if (history.length < 2) return 0;
|
|
76
|
+
|
|
77
|
+
// Linear regression over recent samples
|
|
78
|
+
const last = history[history.length - 1]!;
|
|
79
|
+
const prev = history[history.length - 2]!;
|
|
80
|
+
const dt = last.time - prev.time;
|
|
81
|
+
if (dt <= 0) return 0;
|
|
82
|
+
return (last.value - prev.value) / dt;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findNearestThreshold(
|
|
86
|
+
value: number,
|
|
87
|
+
): { threshold: number; distance: number; direction: 'up' | 'down' } | null {
|
|
88
|
+
let nearest: { threshold: number; distance: number; direction: 'up' | 'down' } | null = null;
|
|
89
|
+
|
|
90
|
+
for (const t of thresholds) {
|
|
91
|
+
const dist = Math.abs(value - (t as number));
|
|
92
|
+
if (nearest === null || dist < nearest.distance) {
|
|
93
|
+
nearest = {
|
|
94
|
+
threshold: t as number,
|
|
95
|
+
distance: dist,
|
|
96
|
+
direction: value < (t as number) ? 'up' : 'down',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return nearest;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
evaluate(value: number, velocity?: number): SpeculativeResult<B> {
|
|
106
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
107
|
+
history.push({ value, time: now });
|
|
108
|
+
if (history.length > HISTORY_SIZE) history.shift();
|
|
109
|
+
|
|
110
|
+
// Evaluate current state
|
|
111
|
+
const current = boundary.hysteresis
|
|
112
|
+
? Boundary.evaluateWithHysteresis(boundary, value, previousState)
|
|
113
|
+
: Boundary.evaluate(boundary, value);
|
|
114
|
+
previousState = current;
|
|
115
|
+
|
|
116
|
+
// Find nearest threshold
|
|
117
|
+
const nearest = findNearestThreshold(value);
|
|
118
|
+
if (!nearest) {
|
|
119
|
+
return { current, confidence: 0 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const vel = estimateVelocity(value, velocity);
|
|
123
|
+
|
|
124
|
+
// Check if moving toward the threshold
|
|
125
|
+
const movingToward = (nearest.direction === 'up' && vel > 0) || (nearest.direction === 'down' && vel < 0);
|
|
126
|
+
|
|
127
|
+
if (!movingToward || nearest.distance > prefetchWindow) {
|
|
128
|
+
return { current, confidence: 0 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compute confidence: closer to threshold + faster velocity = higher confidence
|
|
132
|
+
const distanceFactor = 1 - nearest.distance / prefetchWindow;
|
|
133
|
+
// Distance weighted 70%, velocity 30% — distance is more reliable for prefetch confidence
|
|
134
|
+
const velocityFactor = Math.min(Math.abs(vel) * 10, 1); // Normalize velocity contribution
|
|
135
|
+
const confidence = distanceFactor * 0.7 + velocityFactor * 0.3;
|
|
136
|
+
|
|
137
|
+
// Below 30% confidence, skip prefetch — not worth the speculative cost
|
|
138
|
+
if (confidence < 0.3) {
|
|
139
|
+
return { current, confidence };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pre-compute the predicted next state (jump past hysteresis zone if present)
|
|
143
|
+
const hysteresisJump = boundary.hysteresis ?? 0;
|
|
144
|
+
const predictedValue =
|
|
145
|
+
nearest.direction === 'up'
|
|
146
|
+
? nearest.threshold + hysteresisJump + epsilon
|
|
147
|
+
: nearest.threshold - hysteresisJump - epsilon;
|
|
148
|
+
|
|
149
|
+
const prefetched = boundary.hysteresis
|
|
150
|
+
? Boundary.evaluateWithHysteresis(boundary, predictedValue, current)
|
|
151
|
+
: Boundary.evaluate(boundary, predictedValue);
|
|
152
|
+
|
|
153
|
+
// Only return prefetch if it's actually different
|
|
154
|
+
if (prefetched === current) {
|
|
155
|
+
return { current, confidence: 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { current, prefetched, confidence };
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* SpeculativeEvaluator -- threshold proximity prefetching for boundaries.
|
|
165
|
+
* Pre-computes the next discrete state when a signal is near a threshold,
|
|
166
|
+
* using velocity estimation and hysteresis-based prefetch windows.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const boundary = Boundary.make({
|
|
171
|
+
* thresholds: [600],
|
|
172
|
+
* states: ['small', 'large'] as const,
|
|
173
|
+
* });
|
|
174
|
+
* const spec = SpeculativeEvaluator.make(boundary);
|
|
175
|
+
* const { current, prefetched, confidence } = spec.evaluate(595, 1.5);
|
|
176
|
+
* // current='small', prefetched='large', confidence ~0.85
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export const SpeculativeEvaluator = { make: _make };
|
|
180
|
+
|
|
181
|
+
export declare namespace SpeculativeEvaluator {
|
|
182
|
+
/** Structural shape of an evaluator bound to a specific {@link Boundary}. */
|
|
183
|
+
export type Shape<B extends Boundary.Shape> = SpeculativeEvaluatorShape<B>;
|
|
184
|
+
/** Prediction result from `evaluate()` — current state, optional prefetched next state, and confidence. */
|
|
185
|
+
export type Result<B extends Boundary.Shape> = SpeculativeResult<B>;
|
|
186
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Store<S, Msg>` — TEA-style reducer store.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Stream } from 'effect';
|
|
8
|
+
import { Effect, SubscriptionRef, Semaphore } from 'effect';
|
|
9
|
+
|
|
10
|
+
interface StoreShape<S, Msg> {
|
|
11
|
+
readonly _tag: 'Store';
|
|
12
|
+
readonly get: Effect.Effect<S>;
|
|
13
|
+
readonly changes: Stream.Stream<S>;
|
|
14
|
+
dispatch(msg: Msg): Effect.Effect<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EffectfulStoreShape<S, Msg, E = never, R = never> {
|
|
18
|
+
readonly _tag: 'Store';
|
|
19
|
+
readonly get: Effect.Effect<S>;
|
|
20
|
+
readonly changes: Stream.Stream<S>;
|
|
21
|
+
dispatch(msg: Msg): Effect.Effect<void, E, R>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const _make = <S, Msg>(initial: S, reducer: (state: S, msg: Msg) => S): Effect.Effect<StoreShape<S, Msg>> =>
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const ref = yield* SubscriptionRef.make(initial);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
_tag: 'Store' as const,
|
|
30
|
+
get: SubscriptionRef.get(ref),
|
|
31
|
+
changes: SubscriptionRef.changes(ref),
|
|
32
|
+
dispatch: (msg: Msg) => SubscriptionRef.update(ref, (state) => reducer(state, msg)),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const _makeWithEffect = <S, Msg, E, R>(
|
|
37
|
+
initial: S,
|
|
38
|
+
reducer: (state: S, msg: Msg) => Effect.Effect<S, E, R>,
|
|
39
|
+
): Effect.Effect<EffectfulStoreShape<S, Msg, E, R>> =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const ref = yield* SubscriptionRef.make(initial);
|
|
42
|
+
const mutex = yield* Semaphore.make(1);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
_tag: 'Store' as const,
|
|
46
|
+
get: SubscriptionRef.get(ref),
|
|
47
|
+
changes: SubscriptionRef.changes(ref),
|
|
48
|
+
dispatch: (msg: Msg) =>
|
|
49
|
+
mutex.withPermits(1)(
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const current = yield* SubscriptionRef.get(ref);
|
|
52
|
+
const next = yield* reducer(current, msg);
|
|
53
|
+
yield* SubscriptionRef.set(ref, next);
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Store — TEA-style state container.
|
|
61
|
+
* Build with an initial state and a pure `reducer(state, msg) => state`, then
|
|
62
|
+
* dispatch messages; the store publishes the resulting state via `changes`.
|
|
63
|
+
* Use `makeWithEffect` when the reducer is itself an `Effect`.
|
|
64
|
+
*/
|
|
65
|
+
export const Store = {
|
|
66
|
+
/** Synchronous reducer store. */
|
|
67
|
+
make: _make,
|
|
68
|
+
/** Reducer store where state transitions are themselves `Effect`s. */
|
|
69
|
+
makeWithEffect: _makeWithEffect,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export declare namespace Store {
|
|
73
|
+
/** Structural shape of a synchronous store. */
|
|
74
|
+
export type Shape<S, Msg> = StoreShape<S, Msg>;
|
|
75
|
+
/** Structural shape of an effectful store; adds error channel `E` and requirements `R`. */
|
|
76
|
+
export type Effectful<S, Msg, E = never, R = never> = EffectfulStoreShape<S, Msg, E, R>;
|
|
77
|
+
}
|
package/src/style.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StyleDef -- adaptive style primitive for constraint-based rendering.
|
|
3
|
+
*
|
|
4
|
+
* A style binds a base style layer to optional boundary states with
|
|
5
|
+
* per-state overrides and transitions. Content-addressed via FNV-1a.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentAddress, Millis } from './brands.js';
|
|
11
|
+
import type { Boundary } from './boundary.js';
|
|
12
|
+
import type { StateUnion } from './type-utils.js';
|
|
13
|
+
import { CanonicalCbor } from './cbor.js';
|
|
14
|
+
import { fnv1aBytes } from './fnv.js';
|
|
15
|
+
import { CzapValidationError } from './validation-error.js';
|
|
16
|
+
|
|
17
|
+
/** Single `box-shadow` layer — compiled into a space-separated CSS value by {@link Style.tap}. */
|
|
18
|
+
export interface ShadowLayer {
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
readonly blur: number;
|
|
22
|
+
readonly spread?: number;
|
|
23
|
+
readonly color: string;
|
|
24
|
+
readonly inset?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One layer of a {@link Style}: a flat property bag plus optional pseudo
|
|
29
|
+
* selectors (`:hover`, `::before`, …) and structured `box-shadow` layers.
|
|
30
|
+
*/
|
|
31
|
+
export interface StyleLayer {
|
|
32
|
+
readonly properties: Record<string, string>;
|
|
33
|
+
readonly pseudo?: Record<string, Record<string, string>>;
|
|
34
|
+
readonly boxShadow?: readonly ShadowLayer[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface StyleDef<B extends Boundary.Shape = Boundary.Shape> {
|
|
38
|
+
readonly _tag: 'StyleDef';
|
|
39
|
+
readonly _version: 1;
|
|
40
|
+
readonly id: ContentAddress;
|
|
41
|
+
readonly boundary?: B;
|
|
42
|
+
readonly base: StyleLayer;
|
|
43
|
+
readonly states?: { readonly [S in StateUnion<B> & string]?: StyleLayer };
|
|
44
|
+
readonly transition?: {
|
|
45
|
+
readonly duration: Millis;
|
|
46
|
+
readonly easing?: string;
|
|
47
|
+
readonly properties?: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface StyleFactory {
|
|
52
|
+
make<B extends Boundary.Shape>(config: {
|
|
53
|
+
readonly boundary?: B;
|
|
54
|
+
readonly base: StyleLayer;
|
|
55
|
+
readonly states?: { readonly [S in StateUnion<B> & string]?: StyleLayer };
|
|
56
|
+
readonly transition?: StyleDef['transition'];
|
|
57
|
+
}): StyleDef<B>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deterministicId<B extends Boundary.Shape>(
|
|
61
|
+
boundary: B | undefined,
|
|
62
|
+
base: StyleLayer,
|
|
63
|
+
states: StyleDef<B>['states'],
|
|
64
|
+
transition: StyleDef['transition'] | undefined,
|
|
65
|
+
): ContentAddress {
|
|
66
|
+
return fnv1aBytes(
|
|
67
|
+
CanonicalCbor.encode({
|
|
68
|
+
_tag: 'StyleDef',
|
|
69
|
+
_version: 1,
|
|
70
|
+
boundaryId: boundary?.id ?? null,
|
|
71
|
+
base,
|
|
72
|
+
states: states ?? {},
|
|
73
|
+
transition: transition ?? null,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Deep merge two style layers: properties spread, pseudo merge per selector, boxShadow concat.
|
|
80
|
+
*
|
|
81
|
+
* Override properties win over base. Pseudo-element selectors are merged per
|
|
82
|
+
* key. Box shadows are concatenated (base first, then override).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const base = { properties: { color: 'red', padding: '4px' } };
|
|
87
|
+
* const override = { properties: { color: 'blue', margin: '8px' } };
|
|
88
|
+
* const merged = Style.mergeLayers(base, override);
|
|
89
|
+
* // merged.properties === { color: 'blue', padding: '4px', margin: '8px' }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function _mergeLayers(base: StyleLayer, override: StyleLayer): StyleLayer {
|
|
93
|
+
const properties = { ...base.properties, ...override.properties };
|
|
94
|
+
|
|
95
|
+
let pseudo: Record<string, Record<string, string>> | undefined;
|
|
96
|
+
if (base.pseudo || override.pseudo) {
|
|
97
|
+
const allSelectors = new Set([...Object.keys(base.pseudo ?? {}), ...Object.keys(override.pseudo ?? {})]);
|
|
98
|
+
pseudo = {};
|
|
99
|
+
for (const sel of allSelectors) {
|
|
100
|
+
pseudo[sel] = { ...base.pseudo?.[sel], ...override.pseudo?.[sel] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let boxShadow: readonly ShadowLayer[] | undefined;
|
|
105
|
+
if (base.boxShadow || override.boxShadow) {
|
|
106
|
+
boxShadow = [...(base.boxShadow ?? []), ...(override.boxShadow ?? [])];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
properties,
|
|
111
|
+
...(pseudo !== undefined ? { pseudo } : {}),
|
|
112
|
+
...(boxShadow !== undefined ? { boxShadow } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a style to a flat `Record<string, string>` for the given state.
|
|
118
|
+
*
|
|
119
|
+
* Merges base layer with the state-specific override (if any), flattens
|
|
120
|
+
* pseudo selectors and box-shadow into the result map.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* const style = Style.make({
|
|
125
|
+
* base: { properties: { color: 'black' } },
|
|
126
|
+
* states: { dark: { properties: { color: 'white' } } },
|
|
127
|
+
* });
|
|
128
|
+
* const props = Style.tap(style, 'dark');
|
|
129
|
+
* // props === { color: 'white' }
|
|
130
|
+
* const baseProps = Style.tap(style);
|
|
131
|
+
* // baseProps === { color: 'black' }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
function _tap(style: StyleDef, state?: string): Record<string, string> {
|
|
135
|
+
let layer = style.base;
|
|
136
|
+
|
|
137
|
+
if (state && style.states) {
|
|
138
|
+
const stateLayer = style.states[state];
|
|
139
|
+
if (stateLayer) {
|
|
140
|
+
layer = _mergeLayers(layer, stateLayer);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result: Record<string, string> = { ...layer.properties };
|
|
145
|
+
|
|
146
|
+
if (layer.pseudo) {
|
|
147
|
+
for (const [sel, props] of Object.entries(layer.pseudo)) {
|
|
148
|
+
for (const [prop, val] of Object.entries(props)) {
|
|
149
|
+
result[`${sel}::${prop}`] = val;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (layer.boxShadow && layer.boxShadow.length > 0) {
|
|
155
|
+
result['box-shadow'] = layer.boxShadow
|
|
156
|
+
.map((s) => {
|
|
157
|
+
const parts: string[] = [];
|
|
158
|
+
if (s.inset) parts.push('inset');
|
|
159
|
+
parts.push(`${s.x}px`, `${s.y}px`, `${s.blur}px`);
|
|
160
|
+
if (s.spread !== undefined) parts.push(`${s.spread}px`);
|
|
161
|
+
parts.push(s.color);
|
|
162
|
+
return parts.join(' ');
|
|
163
|
+
})
|
|
164
|
+
.join(', ');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Style namespace -- adaptive style primitive for constraint-based rendering.
|
|
172
|
+
*
|
|
173
|
+
* Bind base styles to optional boundary states with per-state overrides and
|
|
174
|
+
* CSS transitions. Resolve to flat property maps for any given state.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { Boundary, Style } from '@czap/core';
|
|
179
|
+
*
|
|
180
|
+
* const bp = Boundary.make({ input: 'viewport.width', at: [[0, 'sm'], [768, 'lg']] });
|
|
181
|
+
* const style = Style.make({
|
|
182
|
+
* boundary: bp,
|
|
183
|
+
* base: { properties: { 'font-size': '14px' } },
|
|
184
|
+
* states: { lg: { properties: { 'font-size': '18px' } } },
|
|
185
|
+
* transition: { duration: Millis(200) },
|
|
186
|
+
* });
|
|
187
|
+
* const resolved = Style.tap(style, 'lg');
|
|
188
|
+
* // resolved === { 'font-size': '18px' }
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export const Style: StyleFactory & {
|
|
192
|
+
tap: typeof _tap;
|
|
193
|
+
mergeLayers: typeof _mergeLayers;
|
|
194
|
+
} = {
|
|
195
|
+
/**
|
|
196
|
+
* Create a new StyleDef from a configuration object.
|
|
197
|
+
*
|
|
198
|
+
* Validates that state keys match the boundary's states (if a boundary is
|
|
199
|
+
* provided). The resulting object is frozen and content-addressed.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* const style = Style.make({
|
|
204
|
+
* base: { properties: { display: 'flex', gap: '8px' } },
|
|
205
|
+
* });
|
|
206
|
+
* // style._tag === 'StyleDef'
|
|
207
|
+
* // style.id === 'fnv1a:...'
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
make<B extends Boundary.Shape>(config: {
|
|
211
|
+
readonly boundary?: B;
|
|
212
|
+
readonly base: StyleLayer;
|
|
213
|
+
readonly states?: { readonly [S in StateUnion<B> & string]?: StyleLayer };
|
|
214
|
+
readonly transition?: StyleDef['transition'];
|
|
215
|
+
}): StyleDef<B> {
|
|
216
|
+
if (config.boundary && config.states) {
|
|
217
|
+
const boundaryStates = config.boundary.states as readonly string[];
|
|
218
|
+
const stateKeys = Object.keys(config.states);
|
|
219
|
+
for (const key of stateKeys) {
|
|
220
|
+
if (!boundaryStates.includes(key)) {
|
|
221
|
+
throw new CzapValidationError(
|
|
222
|
+
'Style.make',
|
|
223
|
+
`state "${key}" does not match boundary states [${boundaryStates.join(', ')}]`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const id = deterministicId<B>(config.boundary, config.base, config.states, config.transition);
|
|
230
|
+
|
|
231
|
+
const def: StyleDef<B> = {
|
|
232
|
+
_tag: 'StyleDef',
|
|
233
|
+
_version: 1,
|
|
234
|
+
id,
|
|
235
|
+
...(config.boundary !== undefined ? { boundary: config.boundary } : {}),
|
|
236
|
+
base: config.base,
|
|
237
|
+
...(config.states !== undefined ? { states: config.states } : {}),
|
|
238
|
+
...(config.transition !== undefined ? { transition: config.transition } : {}),
|
|
239
|
+
};
|
|
240
|
+
return Object.freeze(def);
|
|
241
|
+
},
|
|
242
|
+
tap: _tap,
|
|
243
|
+
mergeLayers: _mergeLayers,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export declare namespace Style {
|
|
247
|
+
/** Structural shape of a style definition parameterized by its governing {@link Boundary}. */
|
|
248
|
+
export type Shape<B extends Boundary.Shape = Boundary.Shape> = StyleDef<B>;
|
|
249
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only entrypoint for `@czap/core`. Imported as `@czap/core/testing`.
|
|
3
|
+
*
|
|
4
|
+
* These helpers mutate global registry state and would be footguns in
|
|
5
|
+
* production code paths (an edge worker warm-start that calls
|
|
6
|
+
* `resetCapsuleCatalog` would silently wipe every registered capsule,
|
|
7
|
+
* causing dispatch to fail intermittently). They are intentionally
|
|
8
|
+
* partitioned off the main package entry so a consumer cannot reach
|
|
9
|
+
* them by importing `@czap/core` directly.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { resetCapsuleCatalog } from './assembly.js';
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeDef -- theme primitive for constraint-based adaptive rendering.
|
|
3
|
+
*
|
|
4
|
+
* A theme maps a set of token names to variant-keyed values, enabling
|
|
5
|
+
* coherent multi-variant token resolution. Content-addressed via FNV-1a.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentAddress } from './brands.js';
|
|
11
|
+
import { CanonicalCbor } from './cbor.js';
|
|
12
|
+
import { fnv1aBytes } from './fnv.js';
|
|
13
|
+
import { CzapValidationError } from './validation-error.js';
|
|
14
|
+
|
|
15
|
+
interface ThemeDef<V extends readonly string[] = readonly string[]> {
|
|
16
|
+
readonly _tag: 'ThemeDef';
|
|
17
|
+
readonly _version: 1;
|
|
18
|
+
readonly id: ContentAddress;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly variants: V;
|
|
21
|
+
readonly tokens: Record<string, Record<V[number] & string, unknown>>;
|
|
22
|
+
readonly meta?: Record<V[number] & string, { readonly label: string; readonly mode: 'light' | 'dark' }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ThemeFactory {
|
|
26
|
+
make<const V extends readonly [string, ...string[]]>(config: {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly variants: V;
|
|
29
|
+
readonly tokens: Record<string, Record<V[number] & string, unknown>>;
|
|
30
|
+
readonly meta?: ThemeDef<V>['meta'];
|
|
31
|
+
}): ThemeDef<V>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deterministicId<V extends readonly string[]>(
|
|
35
|
+
name: string,
|
|
36
|
+
variants: V,
|
|
37
|
+
tokens: ThemeDef<V>['tokens'],
|
|
38
|
+
meta: ThemeDef<V>['meta'] | undefined,
|
|
39
|
+
): ContentAddress {
|
|
40
|
+
return fnv1aBytes(
|
|
41
|
+
CanonicalCbor.encode({
|
|
42
|
+
_tag: 'ThemeDef',
|
|
43
|
+
_version: 1,
|
|
44
|
+
name,
|
|
45
|
+
variants,
|
|
46
|
+
tokens,
|
|
47
|
+
meta: meta ?? null,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve all tokens for a given variant, returning a map of token name to value.
|
|
54
|
+
*
|
|
55
|
+
* Iterates the theme's token map and extracts each token's value for the
|
|
56
|
+
* specified variant.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const theme = Theme.make({
|
|
61
|
+
* name: 'brand',
|
|
62
|
+
* variants: ['light', 'dark'] as const,
|
|
63
|
+
* tokens: { bg: { light: '#fff', dark: '#111' }, fg: { light: '#000', dark: '#eee' } },
|
|
64
|
+
* });
|
|
65
|
+
* const darkTokens = Theme.tap(theme, 'dark');
|
|
66
|
+
* // darkTokens === { bg: '#111', fg: '#eee' }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
function _tap<V extends readonly string[]>(theme: ThemeDef<V>, variant: V[number] & string): Record<string, unknown> {
|
|
70
|
+
const result: Record<string, unknown> = {};
|
|
71
|
+
for (const [tokenName, variantMap] of Object.entries(theme.tokens)) {
|
|
72
|
+
result[tokenName] = variantMap[variant];
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Theme namespace -- theme primitive for constraint-based adaptive rendering.
|
|
79
|
+
*
|
|
80
|
+
* Map token names to variant-keyed values, enabling coherent multi-variant
|
|
81
|
+
* token resolution (e.g. light/dark themes). Content-addressed via FNV-1a.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { Theme } from '@czap/core';
|
|
86
|
+
*
|
|
87
|
+
* const theme = Theme.make({
|
|
88
|
+
* name: 'brand',
|
|
89
|
+
* variants: ['light', 'dark'] as const,
|
|
90
|
+
* tokens: {
|
|
91
|
+
* bg: { light: '#fff', dark: '#111' },
|
|
92
|
+
* fg: { light: '#000', dark: '#eee' },
|
|
93
|
+
* },
|
|
94
|
+
* });
|
|
95
|
+
* const lightTokens = Theme.tap(theme, 'light');
|
|
96
|
+
* // lightTokens === { bg: '#fff', fg: '#000' }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export const Theme: ThemeFactory & {
|
|
100
|
+
tap: typeof _tap;
|
|
101
|
+
} = {
|
|
102
|
+
/**
|
|
103
|
+
* Create a new ThemeDef from a configuration object.
|
|
104
|
+
*
|
|
105
|
+
* Validates that every token has a value for each declared variant.
|
|
106
|
+
* The resulting object is frozen and content-addressed.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const theme = Theme.make({
|
|
111
|
+
* name: 'ocean',
|
|
112
|
+
* variants: ['light', 'dark'] as const,
|
|
113
|
+
* tokens: { primary: { light: '#0066cc', dark: '#3399ff' } },
|
|
114
|
+
* meta: { light: { label: 'Light', mode: 'light' }, dark: { label: 'Dark', mode: 'dark' } },
|
|
115
|
+
* });
|
|
116
|
+
* // theme._tag === 'ThemeDef'
|
|
117
|
+
* // theme.id === 'fnv1a:...'
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
make<const V extends readonly [string, ...string[]]>(config: {
|
|
121
|
+
readonly name: string;
|
|
122
|
+
readonly variants: V;
|
|
123
|
+
readonly tokens: Record<string, Record<V[number] & string, unknown>>;
|
|
124
|
+
readonly meta?: ThemeDef<V>['meta'];
|
|
125
|
+
}): ThemeDef<V> {
|
|
126
|
+
const variantSet = new Set(config.variants as readonly string[]);
|
|
127
|
+
for (const [tokenName, variantMap] of Object.entries(config.tokens)) {
|
|
128
|
+
for (const variant of variantSet) {
|
|
129
|
+
if (!(variant in variantMap)) {
|
|
130
|
+
throw new CzapValidationError('Theme.make', `Token "${tokenName}" is missing value for variant "${variant}"`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const id = deterministicId<V>(config.name, config.variants, config.tokens, config.meta);
|
|
136
|
+
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
_tag: 'ThemeDef' as const,
|
|
139
|
+
_version: 1 as const,
|
|
140
|
+
id,
|
|
141
|
+
name: config.name,
|
|
142
|
+
variants: config.variants,
|
|
143
|
+
tokens: config.tokens,
|
|
144
|
+
...(config.meta !== undefined ? { meta: config.meta } : {}),
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
tap: _tap,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export declare namespace Theme {
|
|
151
|
+
/** Structural shape of a {@link Theme} definition, parameterized by its variant tuple `V`. */
|
|
152
|
+
export type Shape<V extends readonly string[] = readonly string[]> = ThemeDef<V>;
|
|
153
|
+
}
|