@czap/astro 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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/Satellite.d.ts +42 -0
  4. package/dist/Satellite.d.ts.map +1 -0
  5. package/dist/Satellite.js +55 -0
  6. package/dist/Satellite.js.map +1 -0
  7. package/dist/client-directives/gpu.d.ts +3 -0
  8. package/dist/client-directives/gpu.d.ts.map +1 -0
  9. package/dist/client-directives/gpu.js +5 -0
  10. package/dist/client-directives/gpu.js.map +1 -0
  11. package/dist/client-directives/llm.d.ts +3 -0
  12. package/dist/client-directives/llm.d.ts.map +1 -0
  13. package/dist/client-directives/llm.js +5 -0
  14. package/dist/client-directives/llm.js.map +1 -0
  15. package/dist/client-directives/satellite.d.ts +3 -0
  16. package/dist/client-directives/satellite.d.ts.map +1 -0
  17. package/dist/client-directives/satellite.js +5 -0
  18. package/dist/client-directives/satellite.js.map +1 -0
  19. package/dist/client-directives/stream.d.ts +3 -0
  20. package/dist/client-directives/stream.d.ts.map +1 -0
  21. package/dist/client-directives/stream.js +5 -0
  22. package/dist/client-directives/stream.js.map +1 -0
  23. package/dist/client-directives/wasm.d.ts +3 -0
  24. package/dist/client-directives/wasm.d.ts.map +1 -0
  25. package/dist/client-directives/wasm.js +6 -0
  26. package/dist/client-directives/wasm.js.map +1 -0
  27. package/dist/client-directives/worker.d.ts +3 -0
  28. package/dist/client-directives/worker.d.ts.map +1 -0
  29. package/dist/client-directives/worker.js +5 -0
  30. package/dist/client-directives/worker.js.map +1 -0
  31. package/dist/detect-upgrade.d.ts +16 -0
  32. package/dist/detect-upgrade.d.ts.map +1 -0
  33. package/dist/detect-upgrade.js +105 -0
  34. package/dist/detect-upgrade.js.map +1 -0
  35. package/dist/headers.d.ts +45 -0
  36. package/dist/headers.d.ts.map +1 -0
  37. package/dist/headers.js +64 -0
  38. package/dist/headers.js.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +26 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/integration.d.ts +76 -0
  44. package/dist/integration.d.ts.map +1 -0
  45. package/dist/integration.js +240 -0
  46. package/dist/integration.js.map +1 -0
  47. package/dist/middleware.d.ts +69 -0
  48. package/dist/middleware.d.ts.map +1 -0
  49. package/dist/middleware.js +75 -0
  50. package/dist/middleware.js.map +1 -0
  51. package/dist/quantize.d.ts +50 -0
  52. package/dist/quantize.d.ts.map +1 -0
  53. package/dist/quantize.js +122 -0
  54. package/dist/quantize.js.map +1 -0
  55. package/dist/runtime/boundary.d.ts +123 -0
  56. package/dist/runtime/boundary.d.ts.map +1 -0
  57. package/dist/runtime/boundary.js +164 -0
  58. package/dist/runtime/boundary.js.map +1 -0
  59. package/dist/runtime/globals.d.ts +32 -0
  60. package/dist/runtime/globals.d.ts.map +1 -0
  61. package/dist/runtime/globals.js +45 -0
  62. package/dist/runtime/globals.js.map +1 -0
  63. package/dist/runtime/gpu.d.ts +15 -0
  64. package/dist/runtime/gpu.d.ts.map +1 -0
  65. package/dist/runtime/gpu.js +266 -0
  66. package/dist/runtime/gpu.js.map +1 -0
  67. package/dist/runtime/index.d.ts +7 -0
  68. package/dist/runtime/index.d.ts.map +1 -0
  69. package/dist/runtime/index.js +5 -0
  70. package/dist/runtime/index.js.map +1 -0
  71. package/dist/runtime/llm-receipt-tracker.d.ts +21 -0
  72. package/dist/runtime/llm-receipt-tracker.d.ts.map +1 -0
  73. package/dist/runtime/llm-receipt-tracker.js +60 -0
  74. package/dist/runtime/llm-receipt-tracker.js.map +1 -0
  75. package/dist/runtime/llm-render-pipeline.d.ts +89 -0
  76. package/dist/runtime/llm-render-pipeline.d.ts.map +1 -0
  77. package/dist/runtime/llm-render-pipeline.js +241 -0
  78. package/dist/runtime/llm-render-pipeline.js.map +1 -0
  79. package/dist/runtime/llm-session.d.ts +126 -0
  80. package/dist/runtime/llm-session.d.ts.map +1 -0
  81. package/dist/runtime/llm-session.js +385 -0
  82. package/dist/runtime/llm-session.js.map +1 -0
  83. package/dist/runtime/llm.d.ts +16 -0
  84. package/dist/runtime/llm.d.ts.map +1 -0
  85. package/dist/runtime/llm.js +273 -0
  86. package/dist/runtime/llm.js.map +1 -0
  87. package/dist/runtime/policy.d.ts +100 -0
  88. package/dist/runtime/policy.d.ts.map +1 -0
  89. package/dist/runtime/policy.js +147 -0
  90. package/dist/runtime/policy.js.map +1 -0
  91. package/dist/runtime/receipt-chain.d.ts +22 -0
  92. package/dist/runtime/receipt-chain.d.ts.map +1 -0
  93. package/dist/runtime/receipt-chain.js +80 -0
  94. package/dist/runtime/receipt-chain.js.map +1 -0
  95. package/dist/runtime/runtime-session.d.ts +34 -0
  96. package/dist/runtime/runtime-session.d.ts.map +1 -0
  97. package/dist/runtime/runtime-session.js +102 -0
  98. package/dist/runtime/runtime-session.js.map +1 -0
  99. package/dist/runtime/satellite.d.ts +13 -0
  100. package/dist/runtime/satellite.d.ts.map +1 -0
  101. package/dist/runtime/satellite.js +59 -0
  102. package/dist/runtime/satellite.js.map +1 -0
  103. package/dist/runtime/slots.d.ts +34 -0
  104. package/dist/runtime/slots.d.ts.map +1 -0
  105. package/dist/runtime/slots.js +108 -0
  106. package/dist/runtime/slots.js.map +1 -0
  107. package/dist/runtime/stream-session.d.ts +47 -0
  108. package/dist/runtime/stream-session.d.ts.map +1 -0
  109. package/dist/runtime/stream-session.js +82 -0
  110. package/dist/runtime/stream-session.js.map +1 -0
  111. package/dist/runtime/stream.d.ts +9 -0
  112. package/dist/runtime/stream.d.ts.map +1 -0
  113. package/dist/runtime/stream.js +308 -0
  114. package/dist/runtime/stream.js.map +1 -0
  115. package/dist/runtime/url-policy.d.ts +28 -0
  116. package/dist/runtime/url-policy.d.ts.map +1 -0
  117. package/dist/runtime/url-policy.js +87 -0
  118. package/dist/runtime/url-policy.js.map +1 -0
  119. package/dist/runtime/wasm.d.ts +20 -0
  120. package/dist/runtime/wasm.d.ts.map +1 -0
  121. package/dist/runtime/wasm.js +70 -0
  122. package/dist/runtime/wasm.js.map +1 -0
  123. package/dist/runtime/worker.d.ts +11 -0
  124. package/dist/runtime/worker.d.ts.map +1 -0
  125. package/dist/runtime/worker.js +249 -0
  126. package/dist/runtime/worker.js.map +1 -0
  127. package/package.json +106 -0
  128. package/src/Satellite.astro +39 -0
  129. package/src/Satellite.ts +84 -0
  130. package/src/client-directives/gpu.ts +5 -0
  131. package/src/client-directives/llm.ts +5 -0
  132. package/src/client-directives/satellite.ts +5 -0
  133. package/src/client-directives/stream.ts +5 -0
  134. package/src/client-directives/wasm.ts +6 -0
  135. package/src/client-directives/worker.ts +5 -0
  136. package/src/detect-upgrade.ts +105 -0
  137. package/src/headers.ts +84 -0
  138. package/src/index.ts +30 -0
  139. package/src/integration.ts +309 -0
  140. package/src/middleware.ts +133 -0
  141. package/src/quantize.ts +173 -0
  142. package/src/runtime/boundary.ts +263 -0
  143. package/src/runtime/globals.ts +57 -0
  144. package/src/runtime/gpu.ts +291 -0
  145. package/src/runtime/index.ts +12 -0
  146. package/src/runtime/llm-receipt-tracker.ts +88 -0
  147. package/src/runtime/llm-render-pipeline.ts +366 -0
  148. package/src/runtime/llm-session.ts +548 -0
  149. package/src/runtime/llm.ts +344 -0
  150. package/src/runtime/policy.ts +229 -0
  151. package/src/runtime/receipt-chain.ts +106 -0
  152. package/src/runtime/runtime-session.ts +139 -0
  153. package/src/runtime/satellite.ts +80 -0
  154. package/src/runtime/slots.ts +136 -0
  155. package/src/runtime/stream-session.ts +125 -0
  156. package/src/runtime/stream.ts +407 -0
  157. package/src/runtime/url-policy.ts +107 -0
  158. package/src/runtime/wasm.ts +85 -0
  159. package/src/runtime/worker.ts +307 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Quantize component helpers -- server-side initial state resolution.
3
+ *
4
+ * Maps {@link ServerIslandContext} (user agent, client hints, detected
5
+ * tier) to the best initial boundary state for SSR and server islands.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import type { Boundary, CapLevel, Quantizer } from '@czap/core';
11
+ import { VIEWPORT } from '@czap/core';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Server-only context that {@link resolveInitialState} consumes. Astro
19
+ * builds this from the incoming request (user agent + Client Hints)
20
+ * and the tier detected by the edge middleware.
21
+ */
22
+ export interface ServerIslandContext {
23
+ /** Raw `User-Agent` header. */
24
+ readonly userAgent: string;
25
+ /** Flat Client Hints header map. */
26
+ readonly clientHints: Record<string, string>;
27
+ /** Tier detected by `@czap/edge`. */
28
+ readonly detectedTier: CapLevel;
29
+ }
30
+
31
+ /**
32
+ * Props accepted by the `Quantize` Astro component and by
33
+ * {@link resolveInitialState}.
34
+ */
35
+ export interface QuantizeProps<B extends Boundary.Shape = Boundary.Shape> {
36
+ /** Boundary to quantize. */
37
+ readonly boundary: B;
38
+ /** Optional explicit quantizer definition. */
39
+ readonly quantizer?: Quantizer<B>;
40
+ /** Explicit initial state (skips resolution). */
41
+ readonly initialState?: string;
42
+ /** Final fallback if resolution fails. */
43
+ readonly fallback?: string;
44
+ /** Extra CSS class names. */
45
+ readonly class?: string;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Client Hint Parsing
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Parse a viewport width from client hints.
54
+ * Supports Sec-CH-Viewport-Width and Sec-CH-Width headers.
55
+ */
56
+ function parseViewportWidth(clientHints: Record<string, string>): number | undefined {
57
+ const raw =
58
+ clientHints['sec-ch-viewport-width'] ??
59
+ clientHints['Sec-CH-Viewport-Width'] ??
60
+ clientHints['sec-ch-width'] ??
61
+ clientHints['Sec-CH-Width'];
62
+
63
+ if (raw === undefined) return undefined;
64
+ const parsed = parseInt(raw, 10);
65
+ return Number.isFinite(parsed) ? parsed : undefined;
66
+ }
67
+
68
+ /**
69
+ * Parse prefers-reduced-motion from client hints.
70
+ */
71
+ function parsePrefersReducedMotion(clientHints: Record<string, string>): boolean | undefined {
72
+ const raw = clientHints['sec-ch-prefers-reduced-motion'] ?? clientHints['Sec-CH-Prefers-Reduced-Motion'];
73
+
74
+ if (raw === undefined) return undefined;
75
+ return raw === 'reduce';
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // User Agent Heuristics
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /**
83
+ * Estimate a viewport width from user agent string for common device classes.
84
+ */
85
+ function estimateViewportFromUA(ua: string): number {
86
+ const lower = ua.toLowerCase();
87
+
88
+ if (lower.includes('mobile') || lower.includes('android') || lower.includes('iphone')) {
89
+ return VIEWPORT.mobile;
90
+ }
91
+ if (lower.includes('tablet') || lower.includes('ipad')) {
92
+ return VIEWPORT.tablet;
93
+ }
94
+ return VIEWPORT.desktop;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Tier-Based Heuristic
99
+ // ---------------------------------------------------------------------------
100
+
101
+ const TIER_ORDINALS: Record<CapLevel, number> = {
102
+ static: 0,
103
+ styled: 1,
104
+ reactive: 2,
105
+ animated: 3,
106
+ gpu: 4,
107
+ };
108
+
109
+ /**
110
+ * Map a CapLevel tier to a synthetic viewport-like value for boundary evaluation.
111
+ * This bridges between the capability tier system and viewport-based boundaries.
112
+ */
113
+ function syntheticValueFromTier(tier: CapLevel): number {
114
+ const ord = TIER_ORDINALS[tier];
115
+ // Map tier ordinal to viewport-like breakpoints: 320, 640, 960, 1280, 1920
116
+ return 320 + ord * 320;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Public API
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Resolve the initial boundary state for server-side rendering.
125
+ *
126
+ * Priority:
127
+ * 1. Use viewport width from client hints if available
128
+ * 2. Estimate viewport from user agent
129
+ * 3. Fall back to tier-based synthetic value
130
+ *
131
+ * Evaluates the boundary thresholds to find the matching state.
132
+ */
133
+ export function resolveInitialState<B extends Boundary.Shape>(boundary: B, context: ServerIslandContext): string {
134
+ const stateNames = boundary.states as readonly string[];
135
+ const thresholds = boundary.thresholds as readonly number[];
136
+
137
+ if (stateNames.length === 0) return '';
138
+ if (stateNames.length === 1) return stateNames[0]!;
139
+
140
+ // Determine the signal value to evaluate against the boundary
141
+ let value: number;
142
+
143
+ // Check client hints first (most accurate)
144
+ const hintWidth = parseViewportWidth(context.clientHints);
145
+ const reducedMotion = parsePrefersReducedMotion(context.clientHints);
146
+
147
+ if (hintWidth !== undefined) {
148
+ value = hintWidth;
149
+ } else if (context.userAgent) {
150
+ value = estimateViewportFromUA(context.userAgent);
151
+ } else {
152
+ value = syntheticValueFromTier(context.detectedTier);
153
+ }
154
+
155
+ // If reduced motion is detected and the tier suggests limited capability,
156
+ // bias toward the lowest state
157
+ if (reducedMotion === true && TIER_ORDINALS[context.detectedTier] <= 1) {
158
+ return stateNames[0]!;
159
+ }
160
+
161
+ // Evaluate against boundary thresholds to find the matching state.
162
+ // thresholds[i] is the lower bound for state[i].
163
+ // Walk backwards from the highest threshold to find the first match.
164
+ for (let i = stateNames.length - 1; i >= 0; i--) {
165
+ const threshold = thresholds[i];
166
+ if (threshold !== undefined && value >= threshold) {
167
+ return stateNames[i]!;
168
+ }
169
+ }
170
+
171
+ // Fallback to first state
172
+ return stateNames[0]!;
173
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Client-runtime helpers for parsing serialized boundaries out of
3
+ * `data-czap-boundary` attributes, attaching viewport observers,
4
+ * evaluating boundaries live, and applying the resulting state to a
5
+ * satellite element.
6
+ *
7
+ * Consumed by the Astro `client:satellite` / `client:worker` directives
8
+ * when they hydrate a server-rendered `<div data-czap-boundary="...">`.
9
+ *
10
+ * @module
11
+ */
12
+ import { Boundary } from '@czap/core';
13
+
14
+ /**
15
+ * JSON shape produced on the server by `satelliteAttrs()` and read back
16
+ * on the client via {@link parseBoundary}. Every field corresponds
17
+ * directly to a {@link Boundary.Shape} input.
18
+ */
19
+ export interface SerializedBoundary {
20
+ /** Optional stable boundary id (becomes the runtime `name`). */
21
+ readonly id?: string;
22
+ /** Signal key this boundary consumes (e.g. `"viewport.width"`). */
23
+ readonly input: string;
24
+ /** Ordered ascending thresholds (`thresholds[i]` lower bound of `states[i]`). */
25
+ readonly thresholds: readonly number[];
26
+ /** Non-empty ordered state labels. */
27
+ readonly states: readonly [string, ...string[]];
28
+ /** Optional hysteresis band applied during evaluation. */
29
+ readonly hysteresis?: number;
30
+ }
31
+
32
+ /**
33
+ * Client-side representation of a parsed boundary plus its resolved
34
+ * runtime name, ready to be evaluated against a live signal.
35
+ */
36
+ export interface RuntimeBoundary {
37
+ /** Resolved boundary name (defaults to `"default"`). */
38
+ readonly name: string;
39
+ /** Signal key this boundary consumes. */
40
+ readonly input: string;
41
+ /** Fully-constructed `Boundary.Shape` ready for evaluation. */
42
+ readonly boundary: Boundary.Shape<string, readonly [string, ...string[]]>;
43
+ }
44
+
45
+ /**
46
+ * Normalised boundary-state payload used for `CustomEvent` dispatch and
47
+ * DOM application. CSS keys are filtered to `--czap-*`; ARIA keys to
48
+ * `role` / `aria-*`.
49
+ */
50
+ export interface BoundaryStateDetail {
51
+ /** Discrete state per quantizer name. */
52
+ readonly discrete: Record<string, string>;
53
+ /** Whitelisted `--czap-*` CSS variable map. */
54
+ readonly css: Record<string, string | number>;
55
+ /** GLSL uniform map (`u_*`). */
56
+ readonly glsl: Record<string, number>;
57
+ /** Whitelisted ARIA attribute map. */
58
+ readonly aria: Record<string, string>;
59
+ }
60
+
61
+ function isAllowedBoundaryCssProperty(property: string): boolean {
62
+ return property.startsWith('--czap-');
63
+ }
64
+
65
+ // NOTE: This logic is intentionally duplicated from `isValidAriaKey` in
66
+ // packages/compiler/src/aria.ts. @czap/astro does not depend on @czap/compiler,
67
+ // so the check cannot be shared without introducing a new dependency. Keep the
68
+ // two implementations in sync if either changes.
69
+ function isAllowedBoundaryAttribute(attribute: string): boolean {
70
+ return attribute === 'role' || attribute.startsWith('aria-');
71
+ }
72
+
73
+ function parseBoundaryPayload(boundaryJson: string): Partial<SerializedBoundary> | null {
74
+ let parsed: Partial<SerializedBoundary> | null = null;
75
+ let malformed = false;
76
+
77
+ try {
78
+ parsed = JSON.parse(boundaryJson) as Partial<SerializedBoundary>;
79
+ } catch (error) {
80
+ if (error instanceof SyntaxError) {
81
+ malformed = true;
82
+ } else {
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ return malformed ? null : parsed;
88
+ }
89
+
90
+ /**
91
+ * Parse a JSON-serialised boundary (as produced by
92
+ * `satelliteAttrs()`) into a {@link RuntimeBoundary}. Returns `null`
93
+ * for malformed or structurally invalid payloads so callers can fall
94
+ * back cleanly rather than throwing mid-hydration.
95
+ */
96
+ export function parseBoundary(boundaryJson: string | null): RuntimeBoundary | null {
97
+ if (!boundaryJson) {
98
+ return null;
99
+ }
100
+
101
+ const parsed = parseBoundaryPayload(boundaryJson);
102
+ if (!parsed) {
103
+ return null;
104
+ }
105
+
106
+ if (
107
+ typeof parsed.input !== 'string' ||
108
+ !Array.isArray(parsed.thresholds) ||
109
+ parsed.thresholds.length === 0 ||
110
+ !Array.isArray(parsed.states) ||
111
+ parsed.states.length === 0 ||
112
+ !parsed.thresholds.every((value) => typeof value === 'number') ||
113
+ !parsed.states.every((value) => typeof value === 'string')
114
+ ) {
115
+ return null;
116
+ }
117
+
118
+ const states = parsed.states as readonly [string, ...string[]];
119
+ const first = [parsed.thresholds[0]!, states[0]] as const;
120
+ const rest = parsed.thresholds.slice(1).map((threshold, index) => [threshold, states[index + 1]!] as const);
121
+ const at = [first, ...rest] as const;
122
+
123
+ return {
124
+ name: parsed.id ?? 'default',
125
+ input: parsed.input,
126
+ boundary: Boundary.make({
127
+ input: parsed.input,
128
+ at,
129
+ ...(typeof parsed.hysteresis === 'number' ? { hysteresis: parsed.hysteresis } : {}),
130
+ }),
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Attach a ResizeObserver on `document.documentElement` that calls `callback`
136
+ * whenever the viewport resizes, but only when `input` is a viewport signal
137
+ * (i.e. starts with `"viewport."`) and `ResizeObserver` is available.
138
+ *
139
+ * Returns a cleanup function that disconnects the observer, or `null` when no
140
+ * observer was attached (non-viewport input or no ResizeObserver support).
141
+ *
142
+ * Centralises the identical `observeIfNeeded` blocks that previously lived in
143
+ * satellite.ts and worker.ts.
144
+ */
145
+ export function attachViewportObserver(input: string, callback: () => void): (() => void) | null {
146
+ if (!input.startsWith('viewport.') || typeof ResizeObserver === 'undefined') {
147
+ return null;
148
+ }
149
+
150
+ const observer = new ResizeObserver(callback);
151
+ observer.observe(document.documentElement);
152
+ return () => observer.disconnect();
153
+ }
154
+
155
+ /**
156
+ * Read the current numeric value for a signal `input` (e.g.
157
+ * `"viewport.width"`). Returns `undefined` for unknown inputs; returns
158
+ * `0` in non-DOM environments so callers can treat SSR and malformed
159
+ * signals uniformly.
160
+ */
161
+ export function readSignalValue(input: string): number | undefined {
162
+ if (typeof window === 'undefined') return 0;
163
+
164
+ if (!input.startsWith('viewport.')) {
165
+ return undefined;
166
+ }
167
+
168
+ const axis = input.slice('viewport.'.length);
169
+ return axis === 'height' ? window.innerHeight : window.innerWidth;
170
+ }
171
+
172
+ /**
173
+ * Evaluate a {@link RuntimeBoundary} against a signal value, applying
174
+ * hysteresis when `previousState` is provided and the boundary has a
175
+ * hysteresis band.
176
+ */
177
+ export function evaluateBoundary(boundary: RuntimeBoundary, value: number, previousState?: string): string {
178
+ if (previousState && boundary.boundary.hysteresis) {
179
+ return Boundary.evaluateWithHysteresis(boundary.boundary, value, previousState);
180
+ }
181
+
182
+ return Boundary.evaluate(boundary.boundary, value);
183
+ }
184
+
185
+ /**
186
+ * Merge `state.*` and `state.outputs.*` fields into a single
187
+ * {@link BoundaryStateDetail}, filtering CSS keys to `--czap-*` and
188
+ * ARIA keys to `role` / `aria-*`. Used as the `detail` of the
189
+ * `czap:state` custom event.
190
+ */
191
+ export function normalizeBoundaryState(state: {
192
+ readonly discrete?: Record<string, string>;
193
+ readonly css?: Record<string, string | number>;
194
+ readonly glsl?: Record<string, number>;
195
+ readonly aria?: Record<string, string>;
196
+ readonly outputs?: {
197
+ readonly css?: Record<string, string | number>;
198
+ readonly glsl?: Record<string, number>;
199
+ readonly aria?: Record<string, string>;
200
+ };
201
+ }): BoundaryStateDetail {
202
+ const css = { ...(state.outputs?.css ?? {}), ...(state.css ?? {}) };
203
+ const aria = { ...(state.outputs?.aria ?? {}), ...(state.aria ?? {}) };
204
+
205
+ return {
206
+ discrete: { ...(state.discrete ?? {}) },
207
+ css: Object.fromEntries(Object.entries(css).filter(([property]) => isAllowedBoundaryCssProperty(property))),
208
+ glsl: { ...(state.outputs?.glsl ?? {}), ...(state.glsl ?? {}) },
209
+ aria: Object.fromEntries(Object.entries(aria).filter(([attribute]) => isAllowedBoundaryAttribute(attribute))),
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Apply a normalised state to a satellite element: sets
215
+ * `data-czap-state`, writes whitelisted CSS variables and ARIA
216
+ * attributes, and dispatches `eventName` + `czap:uniform-update`
217
+ * custom events for downstream listeners (GPU/WASM runtimes).
218
+ */
219
+ export function applyBoundaryState(
220
+ element: HTMLElement,
221
+ boundary: RuntimeBoundary,
222
+ state: {
223
+ readonly discrete?: Record<string, string>;
224
+ readonly css?: Record<string, string | number>;
225
+ readonly glsl?: Record<string, number>;
226
+ readonly aria?: Record<string, string>;
227
+ readonly outputs?: {
228
+ readonly css?: Record<string, string | number>;
229
+ readonly glsl?: Record<string, number>;
230
+ readonly aria?: Record<string, string>;
231
+ };
232
+ },
233
+ eventName: string,
234
+ ): void {
235
+ const detail = normalizeBoundaryState(state);
236
+ const stateName = detail.discrete[boundary.name];
237
+
238
+ if (stateName && element.getAttribute('data-czap-state') !== stateName) {
239
+ element.setAttribute('data-czap-state', stateName);
240
+ }
241
+
242
+ for (const [property, value] of Object.entries(detail.css)) {
243
+ element.style.setProperty(property, String(value));
244
+ }
245
+
246
+ for (const [attribute, value] of Object.entries(detail.aria)) {
247
+ element.setAttribute(attribute, value);
248
+ }
249
+
250
+ element.dispatchEvent(
251
+ new CustomEvent(eventName, {
252
+ detail,
253
+ bubbles: true,
254
+ }),
255
+ );
256
+
257
+ element.dispatchEvent(
258
+ new CustomEvent('czap:uniform-update', {
259
+ detail,
260
+ bubbles: true,
261
+ }),
262
+ );
263
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Safe read/write helpers for named `window` globals used as runtime
3
+ * handshake points between inline detect scripts and the hydrated
4
+ * runtime (e.g. `__CZAP_DETECT__`, `__CZAP_SLOTS__`). Works on both
5
+ * client and server entry paths -- returns `undefined` under SSR.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ declare global {
11
+ interface Window {
12
+ [key: string]: unknown;
13
+ }
14
+ }
15
+
16
+ function runtimeWindow(): Window | null {
17
+ return typeof window === 'undefined' ? null : window;
18
+ }
19
+
20
+ /**
21
+ * Read a named `window` global, narrowed through `guard`. Returns
22
+ * `undefined` under SSR or when the guard rejects the runtime value.
23
+ */
24
+ export function readRuntimeGlobal<T>(name: string, guard: (v: unknown) => v is T): T | undefined {
25
+ const win = runtimeWindow();
26
+ if (!win) return undefined;
27
+ const raw: unknown = win[name];
28
+ return guard(raw) ? raw : undefined;
29
+ }
30
+
31
+ /**
32
+ * Write a named `window` global as a non-enumerable property.
33
+ *
34
+ * `options.writable` defaults to `false` so the value is lock-down by default.
35
+ * `options.configurable` defaults to `true` so HMR and bootstrap re-runs can
36
+ * replace the global; security-critical globals (e.g. `__CZAP_RUNTIME_POLICY__`)
37
+ * should pass `configurable: false` to prevent post-install redefinition by
38
+ * any later script on the page.
39
+ */
40
+ export function writeRuntimeGlobal<T>(
41
+ name: string,
42
+ value: T,
43
+ options?: { readonly writable?: boolean; readonly configurable?: boolean },
44
+ ): T {
45
+ const win = runtimeWindow();
46
+ if (!win) {
47
+ return value;
48
+ }
49
+
50
+ Object.defineProperty(win, name, {
51
+ value,
52
+ configurable: options?.configurable ?? true,
53
+ enumerable: false,
54
+ writable: options?.writable ?? false,
55
+ });
56
+ return value;
57
+ }