@glyphjs/runtime 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/dist/index.js ADDED
@@ -0,0 +1,1714 @@
1
+ import { createContext, useMemo, useContext, Component, useRef, useCallback, useState, useEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { componentSchemas } from '@glyphjs/schemas';
4
+ import DOMPurify from 'dompurify';
5
+
6
+ // src/create-runtime.tsx
7
+
8
+ // src/plugins/validate.ts
9
+ var UI_TYPE_PATTERN = /^ui:.+$/;
10
+ function validateComponentDefinition(definition) {
11
+ const errors = [];
12
+ if (!definition.type) {
13
+ errors.push('Component definition must have a "type" field.');
14
+ } else if (!UI_TYPE_PATTERN.test(definition.type)) {
15
+ errors.push(
16
+ `Component type "${definition.type}" must match the "ui:*" format (e.g. "ui:chart", "ui:graph").`
17
+ );
18
+ }
19
+ if (!definition.schema) {
20
+ errors.push('Component definition must have a "schema" field.');
21
+ } else {
22
+ if (typeof definition.schema.parse !== "function") {
23
+ errors.push(
24
+ 'Component schema must have a "parse" method (expected a Zod-compatible schema).'
25
+ );
26
+ }
27
+ if (typeof definition.schema.safeParse !== "function") {
28
+ errors.push(
29
+ 'Component schema must have a "safeParse" method (expected a Zod-compatible schema).'
30
+ );
31
+ }
32
+ }
33
+ if (!definition.render) {
34
+ errors.push(
35
+ 'Component definition must have a "render" field (function or class component).'
36
+ );
37
+ } else if (typeof definition.render !== "function") {
38
+ errors.push(
39
+ 'Component "render" must be a function (functional component) or a class (class component).'
40
+ );
41
+ }
42
+ return {
43
+ valid: errors.length === 0,
44
+ errors
45
+ };
46
+ }
47
+
48
+ // src/plugins/registry.ts
49
+ var PluginRegistry = class {
50
+ components = /* @__PURE__ */ new Map();
51
+ overrides = /* @__PURE__ */ new Map();
52
+ listeners = /* @__PURE__ */ new Set();
53
+ themeDefaults = {};
54
+ // ─── Registration ────────────────────────────────────────────
55
+ /**
56
+ * Register a `ui:*` component plugin definition.
57
+ * Validates the definition first; throws if invalid.
58
+ * Merges any `themeDefaults` from the definition into
59
+ * the accumulated theme defaults map.
60
+ *
61
+ * @param definition - The component definition to register.
62
+ * @throws Error if the definition fails validation.
63
+ */
64
+ registerComponent(definition) {
65
+ const result = validateComponentDefinition(definition);
66
+ if (!result.valid) {
67
+ throw new Error(
68
+ `Invalid component definition for "${definition.type}":
69
+ - ${result.errors.join("\n - ")}`
70
+ );
71
+ }
72
+ this.components.set(definition.type, definition);
73
+ if (definition.themeDefaults) {
74
+ Object.assign(this.themeDefaults, definition.themeDefaults);
75
+ }
76
+ this.notify();
77
+ }
78
+ /** Bulk-register an array of component definitions. */
79
+ registerAll(definitions) {
80
+ for (const def of definitions) {
81
+ this.registerComponent(def);
82
+ }
83
+ }
84
+ // ─── Overrides ───────────────────────────────────────────────
85
+ /** Set override renderers (keyed by block type). */
86
+ setOverrides(overrides) {
87
+ for (const [type, renderer] of Object.entries(overrides)) {
88
+ if (renderer) {
89
+ this.overrides.set(type, renderer);
90
+ }
91
+ }
92
+ this.notify();
93
+ }
94
+ // ─── Lookups ─────────────────────────────────────────────────
95
+ /** Get a registered `ui:*` component definition. */
96
+ getRenderer(blockType) {
97
+ return this.components.get(blockType);
98
+ }
99
+ /** Get an override renderer for any block type. */
100
+ getOverride(blockType) {
101
+ return this.overrides.get(blockType);
102
+ }
103
+ /** Check if a component type is registered. */
104
+ has(blockType) {
105
+ return this.components.has(blockType);
106
+ }
107
+ /** Get all registered component type names. */
108
+ getRegisteredTypes() {
109
+ return Array.from(this.components.keys());
110
+ }
111
+ // ─── Theme Defaults ──────────────────────────────────────────
112
+ /**
113
+ * Returns the accumulated theme defaults from all registered
114
+ * component definitions. These can be merged into a GlyphTheme
115
+ * to provide sensible defaults for plugin-specific variables.
116
+ */
117
+ getThemeDefaults() {
118
+ return { ...this.themeDefaults };
119
+ }
120
+ /**
121
+ * Merge accumulated plugin theme defaults into a GlyphTheme.
122
+ * Plugin defaults have lower priority than existing theme variables.
123
+ *
124
+ * @param theme - The base theme to merge defaults into.
125
+ * @returns A new GlyphTheme with plugin defaults applied under existing variables.
126
+ */
127
+ mergeThemeDefaults(theme) {
128
+ return {
129
+ ...theme,
130
+ variables: {
131
+ ...this.themeDefaults,
132
+ ...theme.variables
133
+ }
134
+ };
135
+ }
136
+ // ─── Change Notification ─────────────────────────────────────
137
+ /**
138
+ * Subscribe to registry changes.
139
+ *
140
+ * @param listener - Callback invoked whenever a component or override is registered.
141
+ * @returns An unsubscribe function that removes the listener.
142
+ */
143
+ subscribe(listener) {
144
+ this.listeners.add(listener);
145
+ return () => {
146
+ this.listeners.delete(listener);
147
+ };
148
+ }
149
+ notify() {
150
+ for (const listener of this.listeners) {
151
+ listener();
152
+ }
153
+ }
154
+ };
155
+
156
+ // src/theme/light.ts
157
+ var lightTheme = {
158
+ name: "light",
159
+ variables: {
160
+ // Colors
161
+ "--glyph-bg": "#f4f6fa",
162
+ "--glyph-text": "#1a2035",
163
+ "--glyph-text-muted": "#6b7a94",
164
+ "--glyph-heading": "#0a0e1a",
165
+ "--glyph-link": "#0a9d7c",
166
+ "--glyph-link-hover": "#088a6c",
167
+ "--glyph-border": "#d0d8e4",
168
+ "--glyph-border-strong": "#a8b5c8",
169
+ "--glyph-surface": "#e8ecf3",
170
+ "--glyph-surface-raised": "#f4f6fa",
171
+ // Accent
172
+ "--glyph-accent": "#0a9d7c",
173
+ "--glyph-accent-hover": "#088a6c",
174
+ "--glyph-accent-subtle": "#e6f6f2",
175
+ "--glyph-accent-muted": "#b0ddd0",
176
+ // Code
177
+ "--glyph-code-bg": "#e8ecf3",
178
+ "--glyph-code-text": "#1a2035",
179
+ // Blockquote
180
+ "--glyph-blockquote-border": "#0a9d7c",
181
+ "--glyph-blockquote-bg": "#e6f6f2",
182
+ // Grid / Tooltip
183
+ "--glyph-grid": "#d0d8e4",
184
+ "--glyph-tooltip-bg": "rgba(10, 14, 26, 0.9)",
185
+ "--glyph-tooltip-text": "#f4f6fa",
186
+ // Callouts
187
+ "--glyph-callout-info-bg": "#e6f2fa",
188
+ "--glyph-callout-info-border": "#38bdf8",
189
+ "--glyph-callout-warning-bg": "#fef3e2",
190
+ "--glyph-callout-warning-border": "#fb923c",
191
+ "--glyph-callout-error-bg": "#fde8e8",
192
+ "--glyph-callout-error-border": "#f87171",
193
+ "--glyph-callout-tip-bg": "#e6f6f0",
194
+ "--glyph-callout-tip-border": "#22c55e",
195
+ // Spacing
196
+ "--glyph-spacing-xs": "0.25rem",
197
+ "--glyph-spacing-sm": "0.5rem",
198
+ "--glyph-spacing-md": "1rem",
199
+ "--glyph-spacing-lg": "1.5rem",
200
+ "--glyph-spacing-xl": "2rem",
201
+ // Typography
202
+ "--glyph-font-body": '"Inter", "Helvetica Neue", system-ui, sans-serif',
203
+ "--glyph-font-heading": '"Inter", "Helvetica Neue", system-ui, sans-serif',
204
+ "--glyph-font-mono": 'ui-monospace, "Cascadia Code", "Fira Code", monospace',
205
+ // Border radius
206
+ "--glyph-radius-sm": "0.375rem",
207
+ "--glyph-radius-md": "0.5rem",
208
+ "--glyph-radius-lg": "0.75rem",
209
+ // Effects
210
+ "--glyph-shadow-sm": "0 1px 3px rgba(0,0,0,0.1)",
211
+ "--glyph-shadow-md": "0 4px 12px rgba(0,0,0,0.15)",
212
+ "--glyph-shadow-lg": "0 8px 30px rgba(0,0,0,0.2)",
213
+ "--glyph-shadow-glow": "none",
214
+ "--glyph-text-shadow": "none",
215
+ "--glyph-backdrop": "none",
216
+ "--glyph-gradient-accent": "linear-gradient(135deg, #0a9d7c, #22c55e)",
217
+ "--glyph-transition": "0.2s ease",
218
+ "--glyph-opacity-muted": "0.7",
219
+ "--glyph-opacity-disabled": "0.4",
220
+ "--glyph-focus-ring": "0 0 0 2px #0a9d7c",
221
+ // SVG / Data Visualization
222
+ "--glyph-node-fill-opacity": "0.85",
223
+ "--glyph-node-radius": "3",
224
+ "--glyph-node-stroke-width": "1.5",
225
+ "--glyph-node-label-color": "#fff",
226
+ "--glyph-edge-color": "#a8b5c8",
227
+ "--glyph-icon-stroke": "#fff",
228
+ "--glyph-icon-stroke-width": "1.5"
229
+ }
230
+ };
231
+
232
+ // src/theme/dark.ts
233
+ var darkTheme = {
234
+ name: "dark",
235
+ variables: {
236
+ // Colors
237
+ "--glyph-bg": "#0a0e1a",
238
+ "--glyph-text": "#d4dae3",
239
+ "--glyph-text-muted": "#6b7a94",
240
+ "--glyph-heading": "#edf0f5",
241
+ "--glyph-link": "#00d4aa",
242
+ "--glyph-link-hover": "#33e0be",
243
+ "--glyph-border": "#1a2035",
244
+ "--glyph-border-strong": "#2a3550",
245
+ "--glyph-surface": "#0f1526",
246
+ "--glyph-surface-raised": "#162038",
247
+ // Accent
248
+ "--glyph-accent": "#00d4aa",
249
+ "--glyph-accent-hover": "#33e0be",
250
+ "--glyph-accent-subtle": "#0a1a1a",
251
+ "--glyph-accent-muted": "#1a4a3a",
252
+ // Code
253
+ "--glyph-code-bg": "#0f1526",
254
+ "--glyph-code-text": "#d4dae3",
255
+ // Blockquote
256
+ "--glyph-blockquote-border": "#00d4aa",
257
+ "--glyph-blockquote-bg": "#0a1a1a",
258
+ // Grid / Tooltip
259
+ "--glyph-grid": "#1a2035",
260
+ "--glyph-tooltip-bg": "rgba(0, 0, 0, 0.9)",
261
+ "--glyph-tooltip-text": "#d4dae3",
262
+ // Callouts
263
+ "--glyph-callout-info-bg": "#0a1526",
264
+ "--glyph-callout-info-border": "#38bdf8",
265
+ "--glyph-callout-warning-bg": "#1a1608",
266
+ "--glyph-callout-warning-border": "#fb923c",
267
+ "--glyph-callout-error-bg": "#1f0e0e",
268
+ "--glyph-callout-error-border": "#f87171",
269
+ "--glyph-callout-tip-bg": "#0a1a14",
270
+ "--glyph-callout-tip-border": "#22c55e",
271
+ // Spacing (same as light — spacing is mode-independent)
272
+ "--glyph-spacing-xs": "0.25rem",
273
+ "--glyph-spacing-sm": "0.5rem",
274
+ "--glyph-spacing-md": "1rem",
275
+ "--glyph-spacing-lg": "1.5rem",
276
+ "--glyph-spacing-xl": "2rem",
277
+ // Typography (same as light — font stacks are mode-independent)
278
+ "--glyph-font-body": '"Inter", "Helvetica Neue", system-ui, sans-serif',
279
+ "--glyph-font-heading": '"Inter", "Helvetica Neue", system-ui, sans-serif',
280
+ "--glyph-font-mono": 'ui-monospace, "Cascadia Code", "Fira Code", monospace',
281
+ // Border radius
282
+ "--glyph-radius-sm": "0.375rem",
283
+ "--glyph-radius-md": "0.5rem",
284
+ "--glyph-radius-lg": "0.75rem",
285
+ // Effects
286
+ "--glyph-shadow-sm": "0 1px 3px rgba(0,0,0,0.4)",
287
+ "--glyph-shadow-md": "0 4px 12px rgba(0,0,0,0.5)",
288
+ "--glyph-shadow-lg": "0 8px 30px rgba(0,0,0,0.6)",
289
+ "--glyph-shadow-glow": "0 0 15px rgba(0,212,170,0.3)",
290
+ "--glyph-text-shadow": "none",
291
+ "--glyph-backdrop": "none",
292
+ "--glyph-gradient-accent": "linear-gradient(135deg, #00d4aa, #00e5ff)",
293
+ "--glyph-transition": "0.2s ease",
294
+ "--glyph-opacity-muted": "0.7",
295
+ "--glyph-opacity-disabled": "0.4",
296
+ "--glyph-focus-ring": "0 0 0 2px #00d4aa",
297
+ // SVG / Data Visualization
298
+ "--glyph-node-fill-opacity": "0.85",
299
+ "--glyph-node-radius": "3",
300
+ "--glyph-node-stroke-width": "1.5",
301
+ "--glyph-node-label-color": "#fff",
302
+ "--glyph-edge-color": "#6b7a94",
303
+ "--glyph-icon-stroke": "#fff",
304
+ "--glyph-icon-stroke-width": "1.5"
305
+ }
306
+ };
307
+
308
+ // src/theme/resolve.ts
309
+ function resolveTheme(theme) {
310
+ if (!theme || theme === "light") {
311
+ return lightTheme;
312
+ }
313
+ if (theme === "dark") {
314
+ return darkTheme;
315
+ }
316
+ return theme;
317
+ }
318
+ function mergeThemeDefaults(theme, defaults) {
319
+ return {
320
+ ...theme,
321
+ variables: { ...defaults, ...theme.variables }
322
+ };
323
+ }
324
+ function createResolveVar(theme) {
325
+ return (varName) => theme.variables[varName] ?? "";
326
+ }
327
+ function isDarkTheme(theme) {
328
+ const bg = theme.variables["--glyph-bg"];
329
+ if (bg) {
330
+ const luminance = perceivedLuminance(bg);
331
+ if (luminance !== null) {
332
+ return luminance < 0.5;
333
+ }
334
+ }
335
+ return theme.name.toLowerCase().includes("dark");
336
+ }
337
+ function perceivedLuminance(hex) {
338
+ const trimmed = hex.trim();
339
+ if (!trimmed.startsWith("#")) return null;
340
+ let r;
341
+ let g;
342
+ let b;
343
+ if (trimmed.length === 4) {
344
+ const rChar = trimmed.charAt(1);
345
+ const gChar = trimmed.charAt(2);
346
+ const bChar = trimmed.charAt(3);
347
+ r = parseInt(rChar + rChar, 16);
348
+ g = parseInt(gChar + gChar, 16);
349
+ b = parseInt(bChar + bChar, 16);
350
+ } else if (trimmed.length === 7) {
351
+ r = parseInt(trimmed.slice(1, 3), 16);
352
+ g = parseInt(trimmed.slice(3, 5), 16);
353
+ b = parseInt(trimmed.slice(5, 7), 16);
354
+ } else {
355
+ return null;
356
+ }
357
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
358
+ return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
359
+ }
360
+ var ThemeContext = createContext(null);
361
+ function ThemeProvider({
362
+ theme,
363
+ className,
364
+ style: consumerStyle,
365
+ children
366
+ }) {
367
+ const resolved = useMemo(() => resolveTheme(theme), [theme]);
368
+ const themeContext = useMemo(
369
+ () => ({
370
+ name: resolved.name,
371
+ resolveVar: createResolveVar(resolved),
372
+ isDark: isDarkTheme(resolved)
373
+ }),
374
+ [resolved]
375
+ );
376
+ const style = useMemo(
377
+ () => ({ ...resolved.variables, ...consumerStyle }),
378
+ [resolved, consumerStyle]
379
+ );
380
+ return /* @__PURE__ */ jsx(ThemeContext, { value: themeContext, children: /* @__PURE__ */ jsx("div", { "data-glyph-theme": resolved.name, className, style, children }) });
381
+ }
382
+ function useGlyphTheme() {
383
+ const ctx = useContext(ThemeContext);
384
+ if (!ctx) {
385
+ throw new Error(
386
+ "useGlyphTheme() must be used within a <ThemeProvider> or <RuntimeProvider>. Did you forget to wrap your component tree?"
387
+ );
388
+ }
389
+ return ctx;
390
+ }
391
+ var noop = () => {
392
+ };
393
+ var RuntimeContext = createContext(null);
394
+ function RuntimeProvider({
395
+ registry,
396
+ references,
397
+ theme,
398
+ className,
399
+ style: consumerStyle,
400
+ onDiagnostic,
401
+ onNavigate,
402
+ children
403
+ }) {
404
+ const resolvedThemeObject = useMemo(() => resolveTheme(theme), [theme]);
405
+ const resolvedTheme = useMemo(
406
+ () => ({
407
+ name: resolvedThemeObject.name,
408
+ resolveVar: createResolveVar(resolvedThemeObject),
409
+ isDark: isDarkTheme(resolvedThemeObject)
410
+ }),
411
+ [resolvedThemeObject]
412
+ );
413
+ const value = useMemo(
414
+ () => ({
415
+ registry,
416
+ references,
417
+ theme: resolvedTheme,
418
+ onDiagnostic: onDiagnostic ?? noop,
419
+ onNavigate: onNavigate ?? noop
420
+ }),
421
+ [registry, references, resolvedTheme, onDiagnostic, onNavigate]
422
+ );
423
+ const style = useMemo(
424
+ () => ({ ...resolvedThemeObject.variables, ...consumerStyle }),
425
+ [resolvedThemeObject, consumerStyle]
426
+ );
427
+ return /* @__PURE__ */ jsx(RuntimeContext, { value, children: /* @__PURE__ */ jsx(ThemeContext, { value: resolvedTheme, children: /* @__PURE__ */ jsx("div", { "data-glyph-theme": resolvedThemeObject.name, className, style, children }) }) });
428
+ }
429
+ function useRuntime() {
430
+ const ctx = useContext(RuntimeContext);
431
+ if (!ctx) {
432
+ throw new Error(
433
+ "useRuntime() must be used within a <RuntimeProvider>. Did you forget to use createGlyphRuntime()?"
434
+ );
435
+ }
436
+ return ctx;
437
+ }
438
+ function useReferences(blockId) {
439
+ const { references } = useRuntime();
440
+ return useMemo(() => {
441
+ const incoming = [];
442
+ const outgoing = [];
443
+ for (const ref of references) {
444
+ if (ref.sourceBlockId === blockId) {
445
+ outgoing.push(ref);
446
+ }
447
+ if (ref.targetBlockId === blockId) {
448
+ incoming.push(ref);
449
+ }
450
+ if (ref.bidirectional) {
451
+ if (ref.targetBlockId === blockId && ref.sourceBlockId !== blockId) {
452
+ outgoing.push(ref);
453
+ }
454
+ if (ref.sourceBlockId === blockId && ref.targetBlockId !== blockId) {
455
+ incoming.push(ref);
456
+ }
457
+ }
458
+ }
459
+ return { incomingRefs: incoming, outgoingRefs: outgoing };
460
+ }, [references, blockId]);
461
+ }
462
+ var ErrorBoundary = class extends Component {
463
+ constructor(props) {
464
+ super(props);
465
+ this.state = { hasError: false, error: null };
466
+ }
467
+ static getDerivedStateFromError(error) {
468
+ return { hasError: true, error };
469
+ }
470
+ componentDidCatch(error, info) {
471
+ const { blockId, blockType, onDiagnostic } = this.props;
472
+ onDiagnostic({
473
+ severity: "error",
474
+ code: "RUNTIME_RENDER_ERROR",
475
+ message: `Error rendering block "${blockId}" (type: ${blockType}): ${error.message}`,
476
+ source: "runtime",
477
+ details: {
478
+ blockId,
479
+ blockType,
480
+ errorMessage: error.message,
481
+ componentStack: info.componentStack
482
+ }
483
+ });
484
+ }
485
+ render() {
486
+ if (this.state.hasError) {
487
+ const { blockId, blockType } = this.props;
488
+ const { error } = this.state;
489
+ return /* @__PURE__ */ jsxs(
490
+ "div",
491
+ {
492
+ style: {
493
+ border: "1px solid #e53e3e",
494
+ borderRadius: "4px",
495
+ padding: "8px 12px",
496
+ margin: "4px 0",
497
+ backgroundColor: "#fff5f5",
498
+ fontSize: "13px",
499
+ fontFamily: "monospace"
500
+ },
501
+ children: [
502
+ /* @__PURE__ */ jsxs("div", { style: { color: "#e53e3e", fontWeight: 600 }, children: [
503
+ 'Render error in block "',
504
+ blockId,
505
+ '" (',
506
+ blockType,
507
+ ")"
508
+ ] }),
509
+ error && /* @__PURE__ */ jsx("div", { style: { color: "#742a2a", marginTop: "4px" }, children: error.message })
510
+ ]
511
+ }
512
+ );
513
+ }
514
+ return this.props.children;
515
+ }
516
+ };
517
+ function FallbackRenderer({ block }) {
518
+ let dataPreview;
519
+ try {
520
+ dataPreview = JSON.stringify(block.data, null, 2);
521
+ } catch {
522
+ dataPreview = "[Unable to serialize block data]";
523
+ }
524
+ return /* @__PURE__ */ jsxs(
525
+ "div",
526
+ {
527
+ style: {
528
+ border: "1px dashed #a0aec0",
529
+ borderRadius: "4px",
530
+ padding: "8px 12px",
531
+ margin: "4px 0",
532
+ backgroundColor: "#f7fafc",
533
+ fontSize: "12px",
534
+ fontFamily: "monospace",
535
+ color: "#718096"
536
+ },
537
+ children: [
538
+ /* @__PURE__ */ jsxs("div", { style: { fontWeight: 600, marginBottom: "4px" }, children: [
539
+ "Unknown block type: ",
540
+ block.type
541
+ ] }),
542
+ /* @__PURE__ */ jsx(
543
+ "pre",
544
+ {
545
+ style: {
546
+ margin: 0,
547
+ whiteSpace: "pre-wrap",
548
+ wordBreak: "break-word",
549
+ maxHeight: "200px",
550
+ overflow: "auto"
551
+ },
552
+ children: dataPreview
553
+ }
554
+ )
555
+ ]
556
+ }
557
+ );
558
+ }
559
+ var HIGHLIGHT_DURATION_MS = 1500;
560
+ var SCROLL_BEHAVIOR = "smooth";
561
+ var BLOCK_SELECTOR_PREFIX = "data-glyph-block";
562
+ function useNavigation() {
563
+ const { onNavigate, references } = useRuntime();
564
+ const announcerRef = useRef(null);
565
+ const navigateTo = useCallback(
566
+ (blockId, ref) => {
567
+ const el = document.querySelector(
568
+ `[${BLOCK_SELECTOR_PREFIX}="${blockId}"]`
569
+ );
570
+ if (!el || !(el instanceof HTMLElement)) {
571
+ return;
572
+ }
573
+ el.scrollIntoView({ behavior: SCROLL_BEHAVIOR, block: "center" });
574
+ el.setAttribute("data-glyph-highlight", "true");
575
+ setTimeout(() => {
576
+ el.removeAttribute("data-glyph-highlight");
577
+ }, HIGHLIGHT_DURATION_MS);
578
+ if (!el.hasAttribute("tabindex")) {
579
+ el.setAttribute("tabindex", "-1");
580
+ }
581
+ el.focus({ preventScroll: true });
582
+ announceNavigation(blockId, announcerRef);
583
+ if (ref) {
584
+ const targetBlock = {
585
+ id: blockId,
586
+ type: "paragraph",
587
+ data: {},
588
+ position: {
589
+ start: { line: 0, column: 0 },
590
+ end: { line: 0, column: 0 }
591
+ }
592
+ };
593
+ onNavigate(ref, targetBlock);
594
+ } else if (references.length > 0) {
595
+ const matchedRef = references.find(
596
+ (r) => r.targetBlockId === blockId || r.sourceBlockId === blockId
597
+ );
598
+ if (matchedRef) {
599
+ const targetBlock = {
600
+ id: blockId,
601
+ type: "paragraph",
602
+ data: {},
603
+ position: {
604
+ start: { line: 0, column: 0 },
605
+ end: { line: 0, column: 0 }
606
+ }
607
+ };
608
+ onNavigate(matchedRef, targetBlock);
609
+ }
610
+ }
611
+ },
612
+ [onNavigate, references]
613
+ );
614
+ return { navigateTo };
615
+ }
616
+ function announceNavigation(blockId, ref) {
617
+ let announcer = ref.current;
618
+ if (!announcer) {
619
+ announcer = document.createElement("div");
620
+ announcer.setAttribute("aria-live", "polite");
621
+ announcer.setAttribute("aria-atomic", "true");
622
+ announcer.setAttribute("role", "status");
623
+ Object.assign(announcer.style, {
624
+ position: "absolute",
625
+ width: "1px",
626
+ height: "1px",
627
+ padding: "0",
628
+ margin: "-1px",
629
+ overflow: "hidden",
630
+ clip: "rect(0, 0, 0, 0)",
631
+ whiteSpace: "nowrap",
632
+ border: "0"
633
+ });
634
+ document.body.appendChild(announcer);
635
+ ref.current = announcer;
636
+ }
637
+ announcer.textContent = `Navigated to block ${blockId}`;
638
+ }
639
+ var GLYPH_LINK_PREFIX = "#glyph:";
640
+ function GlyphLink({
641
+ blockId,
642
+ title,
643
+ children
644
+ }) {
645
+ const { navigateTo } = useNavigation();
646
+ const handleClick = useCallback(
647
+ (e) => {
648
+ e.preventDefault();
649
+ navigateTo(blockId);
650
+ },
651
+ [navigateTo, blockId]
652
+ );
653
+ return /* @__PURE__ */ jsx(
654
+ "a",
655
+ {
656
+ href: `${GLYPH_LINK_PREFIX}${blockId}`,
657
+ title,
658
+ onClick: handleClick,
659
+ "data-glyph-ref": blockId,
660
+ role: "link",
661
+ children
662
+ }
663
+ );
664
+ }
665
+ function isGlyphLink(url) {
666
+ return url.startsWith(GLYPH_LINK_PREFIX);
667
+ }
668
+ function extractBlockId(url) {
669
+ return url.slice(GLYPH_LINK_PREFIX.length);
670
+ }
671
+ function renderInlineNode(node, index) {
672
+ switch (node.type) {
673
+ case "text":
674
+ return node.value;
675
+ case "strong":
676
+ return /* @__PURE__ */ jsx("strong", { children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: node.children }) }, index);
677
+ case "emphasis":
678
+ return /* @__PURE__ */ jsx("em", { children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: node.children }) }, index);
679
+ case "delete":
680
+ return /* @__PURE__ */ jsx("del", { children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: node.children }) }, index);
681
+ case "inlineCode":
682
+ return /* @__PURE__ */ jsx("code", { children: node.value }, index);
683
+ case "link":
684
+ if (isGlyphLink(node.url)) {
685
+ return /* @__PURE__ */ jsx(
686
+ GlyphLink,
687
+ {
688
+ blockId: extractBlockId(node.url),
689
+ title: node.title,
690
+ children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: node.children })
691
+ },
692
+ index
693
+ );
694
+ }
695
+ return /* @__PURE__ */ jsx("a", { href: node.url, title: node.title, children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: node.children }) }, index);
696
+ case "image":
697
+ return /* @__PURE__ */ jsx("img", { src: node.src, alt: node.alt, title: node.title }, index);
698
+ case "break":
699
+ return /* @__PURE__ */ jsx("br", {}, index);
700
+ default:
701
+ return null;
702
+ }
703
+ }
704
+ function InlineRenderer({ nodes }) {
705
+ return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node, i) => renderInlineNode(node, i)) });
706
+ }
707
+ function extractText(nodes) {
708
+ return nodes.map((node) => {
709
+ switch (node.type) {
710
+ case "text":
711
+ return node.value;
712
+ case "inlineCode":
713
+ return node.value;
714
+ case "strong":
715
+ case "emphasis":
716
+ case "delete":
717
+ case "link":
718
+ return extractText(node.children);
719
+ default:
720
+ return "";
721
+ }
722
+ }).join("");
723
+ }
724
+ function slugify(text) {
725
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
726
+ }
727
+ function GlyphHeading({ block }) {
728
+ const data = block.data;
729
+ const Tag = `h${data.depth}`;
730
+ const id = slugify(extractText(data.children));
731
+ return /* @__PURE__ */ jsx(Tag, { id, children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: data.children }) });
732
+ }
733
+ function GlyphParagraph({ block }) {
734
+ const data = block.data;
735
+ return /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: data.children }) });
736
+ }
737
+ function renderSubList(list) {
738
+ const items = list.items.map((item, i) => renderListItem(item, i));
739
+ if (list.ordered) {
740
+ return /* @__PURE__ */ jsx("ol", { start: list.start, children: items });
741
+ }
742
+ return /* @__PURE__ */ jsx("ul", { children: items });
743
+ }
744
+ function renderListItem(item, index) {
745
+ return /* @__PURE__ */ jsxs("li", { children: [
746
+ /* @__PURE__ */ jsx(InlineRenderer, { nodes: item.children }),
747
+ item.subList ? renderSubList(item.subList) : null
748
+ ] }, index);
749
+ }
750
+ function GlyphList({ block }) {
751
+ const data = block.data;
752
+ return renderSubList(data);
753
+ }
754
+ function GlyphCodeBlock({ block }) {
755
+ const data = block.data;
756
+ const language = data.language ?? void 0;
757
+ return /* @__PURE__ */ jsx("pre", { "data-language": language, "aria-label": language ? `Code block (${language})` : "Code block", children: /* @__PURE__ */ jsx("code", { className: language ? `language-${language}` : void 0, children: data.value }) });
758
+ }
759
+ function GlyphBlockquote({ block }) {
760
+ const data = block.data;
761
+ return /* @__PURE__ */ jsx("blockquote", { children: /* @__PURE__ */ jsx(InlineRenderer, { nodes: data.children }) });
762
+ }
763
+ function GlyphImage({ block }) {
764
+ const data = block.data;
765
+ return /* @__PURE__ */ jsxs("figure", { children: [
766
+ /* @__PURE__ */ jsx("img", { src: data.src, alt: data.alt ?? "", loading: "lazy" }),
767
+ data.title ? /* @__PURE__ */ jsx("figcaption", { children: data.title }) : null
768
+ ] });
769
+ }
770
+ function GlyphThematicBreak(_props) {
771
+ return /* @__PURE__ */ jsx("hr", {});
772
+ }
773
+ function GlyphRawHtml({ block }) {
774
+ const data = block.data;
775
+ const clean = DOMPurify.sanitize(data.value);
776
+ return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: clean } });
777
+ }
778
+
779
+ // src/renderers/index.ts
780
+ var builtInRenderers = {
781
+ heading: GlyphHeading,
782
+ paragraph: GlyphParagraph,
783
+ list: GlyphList,
784
+ code: GlyphCodeBlock,
785
+ blockquote: GlyphBlockquote,
786
+ image: GlyphImage,
787
+ "thematic-break": GlyphThematicBreak,
788
+ html: GlyphRawHtml
789
+ };
790
+
791
+ // src/plugins/resolve-props.ts
792
+ function resolveComponentProps(block, definition, references, onNavigate, themeContext, layoutHints, containerContext) {
793
+ const parseResult = definition.schema.safeParse(block.data);
794
+ const data = parseResult.success ? parseResult.data : (() => {
795
+ const err = parseResult.error;
796
+ const issues = err != null && typeof err === "object" && "issues" in err && Array.isArray(err.issues) ? err.issues.map(
797
+ (i) => `${i.path.join(".")}: ${i.message}`
798
+ ).join("; ") : "unknown error";
799
+ console.warn(
800
+ `[GlyphJS] Schema validation failed for block "${block.id}" (${block.type}). Falling back to raw data. Issues: ${issues}`
801
+ );
802
+ return block.data;
803
+ })();
804
+ const outgoingRefs = [];
805
+ const incomingRefs = [];
806
+ for (const ref of references) {
807
+ if (ref.sourceBlockId === block.id) {
808
+ outgoingRefs.push(ref);
809
+ }
810
+ if (ref.targetBlockId === block.id) {
811
+ incomingRefs.push(ref);
812
+ }
813
+ if (ref.bidirectional) {
814
+ if (ref.targetBlockId === block.id && ref.sourceBlockId !== block.id) {
815
+ outgoingRefs.push(ref);
816
+ }
817
+ if (ref.sourceBlockId === block.id && ref.targetBlockId !== block.id) {
818
+ incomingRefs.push(ref);
819
+ }
820
+ }
821
+ }
822
+ return {
823
+ data,
824
+ block,
825
+ outgoingRefs,
826
+ incomingRefs,
827
+ onNavigate,
828
+ theme: themeContext,
829
+ layout: layoutHints,
830
+ container: containerContext
831
+ };
832
+ }
833
+ var defaultConfig = {
834
+ enabled: true,
835
+ duration: 300,
836
+ easing: "ease-out",
837
+ staggerDelay: 50
838
+ };
839
+ var AnimationContext = createContext(defaultConfig);
840
+ function AnimationProvider({
841
+ config,
842
+ children
843
+ }) {
844
+ const state = useMemo(
845
+ () => ({
846
+ ...defaultConfig,
847
+ ...config
848
+ }),
849
+ [config]
850
+ );
851
+ return /* @__PURE__ */ jsx(AnimationContext, { value: state, children });
852
+ }
853
+ function useAnimation() {
854
+ return useContext(AnimationContext);
855
+ }
856
+
857
+ // src/animation/useBlockAnimation.ts
858
+ function usePrefersReducedMotion() {
859
+ const [prefersReduced, setPrefersReduced] = useState(false);
860
+ useEffect(() => {
861
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
862
+ return;
863
+ }
864
+ const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
865
+ setPrefersReduced(mql.matches);
866
+ const handler = (e) => {
867
+ setPrefersReduced(e.matches);
868
+ };
869
+ mql.addEventListener("change", handler);
870
+ return () => {
871
+ mql.removeEventListener("change", handler);
872
+ };
873
+ }, []);
874
+ return prefersReduced;
875
+ }
876
+ function useBlockAnimation(index) {
877
+ const config = useAnimation();
878
+ const prefersReduced = usePrefersReducedMotion();
879
+ const ref = useRef(null);
880
+ const [isVisible, setIsVisible] = useState(false);
881
+ const disabled = !config.enabled || prefersReduced;
882
+ useEffect(() => {
883
+ if (disabled) {
884
+ setIsVisible(true);
885
+ return;
886
+ }
887
+ const element = ref.current;
888
+ if (!element) {
889
+ return;
890
+ }
891
+ if (typeof IntersectionObserver === "undefined") {
892
+ setIsVisible(true);
893
+ return;
894
+ }
895
+ const observer = new IntersectionObserver(
896
+ (entries) => {
897
+ const entry = entries[0];
898
+ if (entry && entry.isIntersecting) {
899
+ setIsVisible(true);
900
+ observer.unobserve(element);
901
+ }
902
+ },
903
+ { threshold: 0.1 }
904
+ );
905
+ observer.observe(element);
906
+ return () => {
907
+ observer.disconnect();
908
+ };
909
+ }, [disabled]);
910
+ const style = useMemo(() => {
911
+ if (disabled) {
912
+ return {};
913
+ }
914
+ const delay = index * config.staggerDelay;
915
+ return isVisible ? {
916
+ opacity: 1,
917
+ transform: "translateY(0)",
918
+ transition: `opacity ${config.duration}ms ${config.easing} ${delay}ms, transform ${config.duration}ms ${config.easing} ${delay}ms`
919
+ } : {
920
+ opacity: 0,
921
+ transform: "translateY(10px)",
922
+ transition: `opacity ${config.duration}ms ${config.easing} ${delay}ms, transform ${config.duration}ms ${config.easing} ${delay}ms`
923
+ };
924
+ }, [disabled, isVisible, index, config.duration, config.easing, config.staggerDelay]);
925
+ return { ref, style, isVisible };
926
+ }
927
+ var containerStyle = {
928
+ display: "flex",
929
+ flexWrap: "wrap",
930
+ gap: "4px",
931
+ marginTop: "4px"
932
+ };
933
+ var badgeBaseStyle = {
934
+ display: "inline-flex",
935
+ alignItems: "center",
936
+ padding: "1px 6px",
937
+ borderRadius: "9999px",
938
+ fontSize: "11px",
939
+ lineHeight: "1.4",
940
+ fontFamily: "system-ui, -apple-system, sans-serif",
941
+ cursor: "pointer",
942
+ border: "1px solid",
943
+ textDecoration: "none",
944
+ transition: "opacity 150ms ease"
945
+ };
946
+ var outgoingBadgeStyle = {
947
+ ...badgeBaseStyle,
948
+ backgroundColor: "#eff6ff",
949
+ borderColor: "#bfdbfe",
950
+ color: "#1d4ed8"
951
+ };
952
+ var incomingBadgeStyle = {
953
+ ...badgeBaseStyle,
954
+ backgroundColor: "#f0fdf4",
955
+ borderColor: "#bbf7d0",
956
+ color: "#15803d"
957
+ };
958
+ function ReferenceIndicator({
959
+ blockId
960
+ }) {
961
+ const { incomingRefs, outgoingRefs } = useReferences(blockId);
962
+ const { navigateTo } = useNavigation();
963
+ const handleClick = useCallback(
964
+ (targetBlockId, ref) => {
965
+ navigateTo(targetBlockId, ref);
966
+ },
967
+ [navigateTo]
968
+ );
969
+ if (outgoingRefs.length === 0 && incomingRefs.length === 0) {
970
+ return null;
971
+ }
972
+ return /* @__PURE__ */ jsxs(
973
+ "nav",
974
+ {
975
+ "aria-label": `References for block ${blockId}`,
976
+ style: containerStyle,
977
+ children: [
978
+ outgoingRefs.map((ref) => {
979
+ const targetId = ref.sourceBlockId === blockId ? ref.targetBlockId : ref.sourceBlockId;
980
+ const label = ref.label ?? targetId;
981
+ return /* @__PURE__ */ jsxs(
982
+ "button",
983
+ {
984
+ type: "button",
985
+ style: outgoingBadgeStyle,
986
+ onClick: () => handleClick(targetId, ref),
987
+ "aria-label": `Navigate to referenced block: ${label}`,
988
+ title: `Go to: ${label} (${ref.type})`,
989
+ children: [
990
+ "\u2192 ",
991
+ label
992
+ ]
993
+ },
994
+ `out-${ref.id}`
995
+ );
996
+ }),
997
+ incomingRefs.map((ref) => {
998
+ const sourceId = ref.targetBlockId === blockId ? ref.sourceBlockId : ref.targetBlockId;
999
+ const label = ref.label ?? sourceId;
1000
+ return /* @__PURE__ */ jsxs(
1001
+ "button",
1002
+ {
1003
+ type: "button",
1004
+ style: incomingBadgeStyle,
1005
+ onClick: () => handleClick(sourceId, ref),
1006
+ "aria-label": `Navigate to referencing block: ${label}`,
1007
+ title: `Referenced by: ${label} (${ref.type})`,
1008
+ children: [
1009
+ "\u2190 ",
1010
+ label
1011
+ ]
1012
+ },
1013
+ `in-${ref.id}`
1014
+ );
1015
+ })
1016
+ ]
1017
+ }
1018
+ );
1019
+ }
1020
+ function BlockDispatch({ block, layout, container }) {
1021
+ const { registry, references, theme, onNavigate } = useRuntime();
1022
+ const { incomingRefs, outgoingRefs } = useReferences(block.id);
1023
+ const hasRefs = incomingRefs.length > 0 || outgoingRefs.length > 0;
1024
+ if (block.type.startsWith("ui:")) {
1025
+ const componentName = block.type.slice(3);
1026
+ const schema = componentSchemas.get(componentName);
1027
+ if (schema) {
1028
+ const result = schema.safeParse(block.data);
1029
+ if (!result.success) {
1030
+ console.warn(
1031
+ `[GlyphJS] Schema validation failed for block "${block.id}" (${block.type}):`,
1032
+ result.error.issues
1033
+ );
1034
+ }
1035
+ }
1036
+ }
1037
+ let content;
1038
+ const overrideDef = registry.getOverride(block.type);
1039
+ if (overrideDef) {
1040
+ const Override = overrideDef;
1041
+ content = /* @__PURE__ */ jsx(Override, { block, layout });
1042
+ } else {
1043
+ const definition = registry.getRenderer(block.type);
1044
+ if (definition) {
1045
+ const handleNavigate = (ref) => {
1046
+ onNavigate(ref, block);
1047
+ };
1048
+ const props = resolveComponentProps(
1049
+ block,
1050
+ definition,
1051
+ references,
1052
+ handleNavigate,
1053
+ theme,
1054
+ layout,
1055
+ container
1056
+ );
1057
+ const Renderer = definition.render;
1058
+ content = /* @__PURE__ */ jsx(Renderer, { ...props });
1059
+ } else {
1060
+ const BuiltIn = builtInRenderers[block.type];
1061
+ if (BuiltIn) {
1062
+ content = /* @__PURE__ */ jsx(BuiltIn, { block, layout });
1063
+ } else {
1064
+ content = /* @__PURE__ */ jsx(FallbackRenderer, { block });
1065
+ }
1066
+ }
1067
+ }
1068
+ return /* @__PURE__ */ jsxs("div", { "data-glyph-block": block.id, children: [
1069
+ content,
1070
+ hasRefs && /* @__PURE__ */ jsx(ReferenceIndicator, { blockId: block.id })
1071
+ ] });
1072
+ }
1073
+ function BlockRenderer({
1074
+ block,
1075
+ layout,
1076
+ index = 0,
1077
+ container
1078
+ }) {
1079
+ const { onDiagnostic } = useRuntime();
1080
+ const { ref, style } = useBlockAnimation(index);
1081
+ return /* @__PURE__ */ jsx("div", { ref, style, "data-glyph-block-anim": block.id, children: /* @__PURE__ */ jsx(ErrorBoundary, { blockId: block.id, blockType: block.type, onDiagnostic, children: /* @__PURE__ */ jsx(BlockDispatch, { block, layout, container }) }) });
1082
+ }
1083
+ var defaultLayout = {
1084
+ mode: "document",
1085
+ spacing: "normal"
1086
+ };
1087
+ var LayoutContext = createContext(defaultLayout);
1088
+ function LayoutProvider({
1089
+ layout,
1090
+ children
1091
+ }) {
1092
+ return /* @__PURE__ */ jsx(LayoutContext, { value: layout, children });
1093
+ }
1094
+ function useLayout() {
1095
+ return useContext(LayoutContext);
1096
+ }
1097
+ var spacingMap = {
1098
+ compact: "0.5rem",
1099
+ normal: "1rem",
1100
+ relaxed: "2rem"
1101
+ };
1102
+ function DocumentLayout({
1103
+ blocks,
1104
+ layout,
1105
+ renderBlock
1106
+ }) {
1107
+ const gap = spacingMap[layout.spacing ?? "normal"];
1108
+ const containerStyle2 = {
1109
+ maxWidth: layout.maxWidth ?? "none",
1110
+ margin: "0 auto",
1111
+ display: "flex",
1112
+ flexDirection: "column",
1113
+ gap
1114
+ };
1115
+ return /* @__PURE__ */ jsx("div", { style: containerStyle2, "data-glyph-layout": "document", children: blocks.map((block, index) => /* @__PURE__ */ jsx("div", { children: renderBlock(block, index) }, block.id)) });
1116
+ }
1117
+ function DashboardLayout({
1118
+ blocks,
1119
+ layout,
1120
+ renderBlock
1121
+ }) {
1122
+ const columns = layout.columns ?? 2;
1123
+ const containerStyle2 = {
1124
+ display: "grid",
1125
+ gridTemplateColumns: `repeat(${String(columns)}, 1fr)`,
1126
+ gap: "1rem"
1127
+ };
1128
+ return /* @__PURE__ */ jsx("div", { style: containerStyle2, "data-glyph-layout": "dashboard", children: blocks.map((block, index) => {
1129
+ const override = layout.blockLayout?.[block.id];
1130
+ const cellStyle = {};
1131
+ if (override) {
1132
+ if (override.gridColumn) {
1133
+ cellStyle.gridColumn = override.gridColumn;
1134
+ }
1135
+ if (override.gridRow) {
1136
+ cellStyle.gridRow = override.gridRow;
1137
+ }
1138
+ if (override.span) {
1139
+ cellStyle.gridColumn = `span ${String(override.span)}`;
1140
+ }
1141
+ }
1142
+ return /* @__PURE__ */ jsx("div", { style: cellStyle, children: renderBlock(block, index) }, block.id);
1143
+ }) });
1144
+ }
1145
+ function PresentationLayout({
1146
+ blocks,
1147
+ renderBlock
1148
+ }) {
1149
+ const [currentIndex, setCurrentIndex] = useState(0);
1150
+ const total = blocks.length;
1151
+ const goNext = useCallback(() => {
1152
+ setCurrentIndex((i) => Math.min(i + 1, total - 1));
1153
+ }, [total]);
1154
+ const goPrev = useCallback(() => {
1155
+ setCurrentIndex((i) => Math.max(i - 1, 0));
1156
+ }, []);
1157
+ useEffect(() => {
1158
+ function handleKeyDown(e) {
1159
+ switch (e.key) {
1160
+ case "ArrowRight":
1161
+ case "ArrowDown":
1162
+ case " ":
1163
+ e.preventDefault();
1164
+ goNext();
1165
+ break;
1166
+ case "ArrowLeft":
1167
+ case "ArrowUp":
1168
+ e.preventDefault();
1169
+ goPrev();
1170
+ break;
1171
+ }
1172
+ }
1173
+ window.addEventListener("keydown", handleKeyDown);
1174
+ return () => {
1175
+ window.removeEventListener("keydown", handleKeyDown);
1176
+ };
1177
+ }, [goNext, goPrev]);
1178
+ if (total === 0) {
1179
+ return null;
1180
+ }
1181
+ const currentBlock = blocks[currentIndex];
1182
+ const containerStyle2 = {
1183
+ position: "relative",
1184
+ width: "100vw",
1185
+ height: "100vh",
1186
+ overflow: "hidden"
1187
+ };
1188
+ const slideStyle = {
1189
+ width: "100%",
1190
+ height: "100%",
1191
+ display: "flex",
1192
+ alignItems: "center",
1193
+ justifyContent: "center",
1194
+ padding: "2rem",
1195
+ boxSizing: "border-box"
1196
+ };
1197
+ const indicatorStyle = {
1198
+ position: "absolute",
1199
+ bottom: "1rem",
1200
+ right: "1rem",
1201
+ fontSize: "0.875rem",
1202
+ color: "#718096",
1203
+ fontFamily: "sans-serif",
1204
+ userSelect: "none"
1205
+ };
1206
+ return /* @__PURE__ */ jsxs("div", { style: containerStyle2, "data-glyph-layout": "presentation", children: [
1207
+ /* @__PURE__ */ jsx("div", { style: slideStyle, children: currentBlock ? renderBlock(currentBlock, currentIndex) : null }),
1208
+ /* @__PURE__ */ jsxs("div", { style: indicatorStyle, children: [
1209
+ String(currentIndex + 1),
1210
+ " / ",
1211
+ String(total)
1212
+ ] })
1213
+ ] });
1214
+ }
1215
+ var severityColors = {
1216
+ error: "#dc2626",
1217
+ warning: "#d97706",
1218
+ info: "#2563eb"
1219
+ };
1220
+ var severityBackgrounds = {
1221
+ error: "#fef2f2",
1222
+ warning: "#fffbeb",
1223
+ info: "#eff6ff"
1224
+ };
1225
+ var severityIcons = {
1226
+ error: "\u2716",
1227
+ // heavy multiplication sign
1228
+ warning: "\u26A0",
1229
+ // warning sign
1230
+ info: "\u2139"
1231
+ // information source
1232
+ };
1233
+ function formatPosition(diagnostic) {
1234
+ if (!diagnostic.position) return "";
1235
+ const { start } = diagnostic.position;
1236
+ return `${String(start.line)}:${String(start.column)}`;
1237
+ }
1238
+ var overlayStyle = {
1239
+ position: "fixed",
1240
+ bottom: "16px",
1241
+ right: "16px",
1242
+ maxWidth: "480px",
1243
+ maxHeight: "60vh",
1244
+ overflowY: "auto",
1245
+ backgroundColor: "#ffffff",
1246
+ border: "1px solid #e5e7eb",
1247
+ borderRadius: "8px",
1248
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
1249
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
1250
+ fontSize: "13px",
1251
+ zIndex: 9999
1252
+ };
1253
+ var headerStyle = {
1254
+ display: "flex",
1255
+ alignItems: "center",
1256
+ justifyContent: "space-between",
1257
+ padding: "10px 14px",
1258
+ borderBottom: "1px solid #e5e7eb",
1259
+ backgroundColor: "#f9fafb",
1260
+ borderRadius: "8px 8px 0 0"
1261
+ };
1262
+ var closeButtonStyle = {
1263
+ background: "none",
1264
+ border: "none",
1265
+ cursor: "pointer",
1266
+ fontSize: "16px",
1267
+ color: "#6b7280",
1268
+ padding: "2px 6px",
1269
+ borderRadius: "4px",
1270
+ lineHeight: 1
1271
+ };
1272
+ var itemStyle = {
1273
+ padding: "8px 14px",
1274
+ borderBottom: "1px solid #f3f4f6"
1275
+ };
1276
+ var itemHeaderStyle = {
1277
+ display: "flex",
1278
+ alignItems: "center",
1279
+ gap: "6px"
1280
+ };
1281
+ var codeStyle = {
1282
+ color: "#6b7280",
1283
+ fontSize: "11px"
1284
+ };
1285
+ var messageStyle = {
1286
+ marginTop: "2px",
1287
+ color: "#1f2937",
1288
+ lineHeight: "1.4"
1289
+ };
1290
+ var positionStyle = {
1291
+ color: "#9ca3af",
1292
+ fontSize: "11px",
1293
+ marginTop: "2px"
1294
+ };
1295
+ function DiagnosticsOverlay({
1296
+ diagnostics
1297
+ }) {
1298
+ const [dismissed, setDismissed] = useState(false);
1299
+ useEffect(() => {
1300
+ setDismissed(false);
1301
+ }, [diagnostics]);
1302
+ const handleDismiss = useCallback(() => {
1303
+ setDismissed(true);
1304
+ }, []);
1305
+ useEffect(() => {
1306
+ function handleKeyDown(e) {
1307
+ if (e.key === "Escape") {
1308
+ setDismissed(true);
1309
+ }
1310
+ }
1311
+ document.addEventListener("keydown", handleKeyDown);
1312
+ return () => {
1313
+ document.removeEventListener("keydown", handleKeyDown);
1314
+ };
1315
+ }, []);
1316
+ const summary = useMemo(() => {
1317
+ let errors = 0;
1318
+ let warnings = 0;
1319
+ let infos = 0;
1320
+ for (const d of diagnostics) {
1321
+ if (d.severity === "error") errors++;
1322
+ else if (d.severity === "warning") warnings++;
1323
+ else infos++;
1324
+ }
1325
+ const parts = [];
1326
+ if (errors > 0) parts.push(`${String(errors)} error${errors > 1 ? "s" : ""}`);
1327
+ if (warnings > 0)
1328
+ parts.push(`${String(warnings)} warning${warnings > 1 ? "s" : ""}`);
1329
+ if (infos > 0) parts.push(`${String(infos)} info`);
1330
+ return parts.join(", ");
1331
+ }, [diagnostics]);
1332
+ if (diagnostics.length === 0 || dismissed) {
1333
+ return null;
1334
+ }
1335
+ return /* @__PURE__ */ jsxs(
1336
+ "div",
1337
+ {
1338
+ style: overlayStyle,
1339
+ "data-glyph-diagnostics-overlay": true,
1340
+ role: "complementary",
1341
+ "aria-label": "Diagnostics overlay",
1342
+ children: [
1343
+ /* @__PURE__ */ jsxs("div", { style: headerStyle, children: [
1344
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, color: "#1f2937" }, children: summary }),
1345
+ /* @__PURE__ */ jsx(
1346
+ "button",
1347
+ {
1348
+ type: "button",
1349
+ style: closeButtonStyle,
1350
+ onClick: handleDismiss,
1351
+ "aria-label": "Dismiss diagnostics",
1352
+ children: "\u2715"
1353
+ }
1354
+ )
1355
+ ] }),
1356
+ /* @__PURE__ */ jsx("div", { children: diagnostics.map((diagnostic, index) => {
1357
+ const color = severityColors[diagnostic.severity];
1358
+ const bg = severityBackgrounds[diagnostic.severity];
1359
+ const icon = severityIcons[diagnostic.severity];
1360
+ const pos = formatPosition(diagnostic);
1361
+ return /* @__PURE__ */ jsxs(
1362
+ "div",
1363
+ {
1364
+ style: { ...itemStyle, backgroundColor: bg },
1365
+ children: [
1366
+ /* @__PURE__ */ jsxs("div", { style: itemHeaderStyle, children: [
1367
+ /* @__PURE__ */ jsx("span", { style: { color, fontWeight: 700 }, children: icon }),
1368
+ /* @__PURE__ */ jsx("span", { style: { color, fontWeight: 600 }, children: diagnostic.severity }),
1369
+ diagnostic.code && /* @__PURE__ */ jsxs("span", { style: codeStyle, children: [
1370
+ "[",
1371
+ diagnostic.code,
1372
+ "]"
1373
+ ] })
1374
+ ] }),
1375
+ /* @__PURE__ */ jsx("div", { style: messageStyle, children: diagnostic.message }),
1376
+ pos && /* @__PURE__ */ jsxs("div", { style: positionStyle, children: [
1377
+ "at ",
1378
+ pos
1379
+ ] })
1380
+ ]
1381
+ },
1382
+ `${diagnostic.code}-${String(index)}`
1383
+ );
1384
+ }) })
1385
+ ]
1386
+ }
1387
+ );
1388
+ }
1389
+ var severityColors2 = {
1390
+ error: "#dc2626",
1391
+ warning: "#d97706",
1392
+ info: "#2563eb"
1393
+ };
1394
+ var severityBackgrounds2 = {
1395
+ error: "#fef2f2",
1396
+ warning: "#fffbeb",
1397
+ info: "#eff6ff"
1398
+ };
1399
+ var severityIcons2 = {
1400
+ error: "\u2716",
1401
+ warning: "\u26A0",
1402
+ info: "\u2139"
1403
+ };
1404
+ function highestSeverity(diagnostics) {
1405
+ let has = "info";
1406
+ for (const d of diagnostics) {
1407
+ if (d.severity === "error") return "error";
1408
+ if (d.severity === "warning") has = "warning";
1409
+ }
1410
+ return has;
1411
+ }
1412
+ function formatPosition2(diagnostic) {
1413
+ if (!diagnostic.position) return "";
1414
+ const { start } = diagnostic.position;
1415
+ return `${String(start.line)}:${String(start.column)}`;
1416
+ }
1417
+ var wrapperStyle = {
1418
+ position: "relative",
1419
+ display: "inline-block"
1420
+ };
1421
+ var detailsStyle = {
1422
+ position: "absolute",
1423
+ top: "100%",
1424
+ left: 0,
1425
+ marginTop: "4px",
1426
+ minWidth: "280px",
1427
+ maxWidth: "400px",
1428
+ backgroundColor: "#ffffff",
1429
+ border: "1px solid #e5e7eb",
1430
+ borderRadius: "6px",
1431
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.12)",
1432
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
1433
+ fontSize: "12px",
1434
+ zIndex: 9998,
1435
+ overflow: "hidden"
1436
+ };
1437
+ var detailItemStyle = {
1438
+ padding: "6px 10px",
1439
+ borderBottom: "1px solid #f3f4f6"
1440
+ };
1441
+ function BlockDiagnosticIndicator({
1442
+ diagnostics
1443
+ }) {
1444
+ const [expanded, setExpanded] = useState(false);
1445
+ const toggle = useCallback(() => {
1446
+ setExpanded((prev) => !prev);
1447
+ }, []);
1448
+ if (diagnostics.length === 0) {
1449
+ return null;
1450
+ }
1451
+ const severity = highestSeverity(diagnostics);
1452
+ const color = severityColors2[severity];
1453
+ const icon = severityIcons2[severity];
1454
+ const badgeStyle = {
1455
+ display: "inline-flex",
1456
+ alignItems: "center",
1457
+ justifyContent: "center",
1458
+ width: "20px",
1459
+ height: "20px",
1460
+ borderRadius: "50%",
1461
+ backgroundColor: color,
1462
+ color: "#ffffff",
1463
+ fontSize: "11px",
1464
+ fontWeight: 700,
1465
+ cursor: "pointer",
1466
+ border: "none",
1467
+ lineHeight: 1,
1468
+ padding: 0
1469
+ };
1470
+ return /* @__PURE__ */ jsxs("span", { style: wrapperStyle, "data-glyph-diagnostic-indicator": true, children: [
1471
+ /* @__PURE__ */ jsx(
1472
+ "button",
1473
+ {
1474
+ type: "button",
1475
+ style: badgeStyle,
1476
+ onClick: toggle,
1477
+ "aria-label": `${String(diagnostics.length)} diagnostic${diagnostics.length > 1 ? "s" : ""}`,
1478
+ "aria-expanded": expanded,
1479
+ title: `${String(diagnostics.length)} diagnostic${diagnostics.length > 1 ? "s" : ""}`,
1480
+ children: icon
1481
+ }
1482
+ ),
1483
+ expanded && /* @__PURE__ */ jsx("div", { style: detailsStyle, role: "tooltip", children: diagnostics.map((diagnostic, index) => {
1484
+ const dColor = severityColors2[diagnostic.severity];
1485
+ const bg = severityBackgrounds2[diagnostic.severity];
1486
+ const dIcon = severityIcons2[diagnostic.severity];
1487
+ const pos = formatPosition2(diagnostic);
1488
+ return /* @__PURE__ */ jsxs(
1489
+ "div",
1490
+ {
1491
+ style: { ...detailItemStyle, backgroundColor: bg },
1492
+ children: [
1493
+ /* @__PURE__ */ jsxs(
1494
+ "div",
1495
+ {
1496
+ style: {
1497
+ display: "flex",
1498
+ alignItems: "center",
1499
+ gap: "4px"
1500
+ },
1501
+ children: [
1502
+ /* @__PURE__ */ jsx("span", { style: { color: dColor, fontWeight: 700 }, children: dIcon }),
1503
+ /* @__PURE__ */ jsx("span", { style: { color: dColor, fontWeight: 600 }, children: diagnostic.severity }),
1504
+ diagnostic.code && /* @__PURE__ */ jsxs("span", { style: { color: "#6b7280", fontSize: "10px" }, children: [
1505
+ "[",
1506
+ diagnostic.code,
1507
+ "]"
1508
+ ] })
1509
+ ]
1510
+ }
1511
+ ),
1512
+ /* @__PURE__ */ jsx(
1513
+ "div",
1514
+ {
1515
+ style: {
1516
+ color: "#1f2937",
1517
+ marginTop: "2px",
1518
+ lineHeight: "1.4"
1519
+ },
1520
+ children: diagnostic.message
1521
+ }
1522
+ ),
1523
+ pos && /* @__PURE__ */ jsxs("div", { style: { color: "#9ca3af", fontSize: "10px", marginTop: "2px" }, children: [
1524
+ "at ",
1525
+ pos
1526
+ ] })
1527
+ ]
1528
+ },
1529
+ `${diagnostic.code}-${String(index)}`
1530
+ );
1531
+ }) })
1532
+ ] });
1533
+ }
1534
+ function ContainerMeasure({ children, onMeasure }) {
1535
+ const ref = useRef(null);
1536
+ useEffect(() => {
1537
+ const el = ref.current;
1538
+ if (!el) return;
1539
+ const observer = new ResizeObserver((entries) => {
1540
+ for (const entry of entries) {
1541
+ if (entry.contentRect.width > 0) {
1542
+ onMeasure(entry.contentRect.width);
1543
+ }
1544
+ }
1545
+ });
1546
+ onMeasure(el.clientWidth);
1547
+ observer.observe(el);
1548
+ return () => observer.disconnect();
1549
+ }, [onMeasure]);
1550
+ return /* @__PURE__ */ jsx("div", { ref, style: { width: "100%" }, children });
1551
+ }
1552
+
1553
+ // src/container/breakpoints.ts
1554
+ var COMPACT_UP = 500;
1555
+ var COMPACT_DOWN = 484;
1556
+ var WIDE_UP = 900;
1557
+ var WIDE_DOWN = 884;
1558
+ function resolveTier(width, previous) {
1559
+ if (width === 0) return "wide";
1560
+ switch (previous) {
1561
+ case "compact":
1562
+ if (width >= WIDE_UP) return "wide";
1563
+ if (width >= COMPACT_UP) return "standard";
1564
+ return "compact";
1565
+ case "standard":
1566
+ if (width >= WIDE_UP) return "wide";
1567
+ if (width < COMPACT_DOWN) return "compact";
1568
+ return "standard";
1569
+ case "wide":
1570
+ if (width < COMPACT_DOWN) return "compact";
1571
+ if (width < WIDE_DOWN) return "standard";
1572
+ return "wide";
1573
+ }
1574
+ }
1575
+ function GlyphDocument({
1576
+ ir,
1577
+ className,
1578
+ animation,
1579
+ diagnostics
1580
+ }) {
1581
+ const { layout, blocks } = ir;
1582
+ const [containerWidth, setContainerWidth] = useState(0);
1583
+ const tierRef = useRef("wide");
1584
+ const containerTier = useMemo(() => {
1585
+ const next = resolveTier(containerWidth, tierRef.current);
1586
+ tierRef.current = next;
1587
+ return next;
1588
+ }, [containerWidth]);
1589
+ const container = useMemo(
1590
+ () => ({ width: containerWidth, tier: containerTier }),
1591
+ [containerWidth, containerTier]
1592
+ );
1593
+ const renderBlock = useCallback(
1594
+ (block, index) => /* @__PURE__ */ jsx(
1595
+ BlockRenderer,
1596
+ {
1597
+ block,
1598
+ layout,
1599
+ index,
1600
+ container
1601
+ },
1602
+ block.id
1603
+ ),
1604
+ [layout, container]
1605
+ );
1606
+ let content;
1607
+ switch (layout.mode) {
1608
+ case "dashboard":
1609
+ content = /* @__PURE__ */ jsx(DashboardLayout, { blocks, layout, renderBlock });
1610
+ break;
1611
+ case "presentation":
1612
+ content = /* @__PURE__ */ jsx(PresentationLayout, { blocks, renderBlock });
1613
+ break;
1614
+ case "document":
1615
+ default:
1616
+ content = /* @__PURE__ */ jsx(DocumentLayout, { blocks, layout, renderBlock });
1617
+ break;
1618
+ }
1619
+ return /* @__PURE__ */ jsx(AnimationProvider, { config: animation, children: /* @__PURE__ */ jsx(LayoutProvider, { layout, children: /* @__PURE__ */ jsx(ContainerMeasure, { onMeasure: setContainerWidth, children: /* @__PURE__ */ jsxs("div", { className, "data-glyph-document": ir.id, children: [
1620
+ content,
1621
+ diagnostics && diagnostics.length > 0 && /* @__PURE__ */ jsx(DiagnosticsOverlay, { diagnostics })
1622
+ ] }) }) }) });
1623
+ }
1624
+ function createGlyphRuntime(config) {
1625
+ const registry = new PluginRegistry();
1626
+ if (config.components) {
1627
+ registry.registerAll(config.components);
1628
+ }
1629
+ if (config.overrides) {
1630
+ registry.setOverrides(config.overrides);
1631
+ }
1632
+ let currentTheme = config.theme;
1633
+ let registryVersion = 0;
1634
+ const listeners = /* @__PURE__ */ new Set();
1635
+ function notify() {
1636
+ for (const listener of listeners) {
1637
+ listener();
1638
+ }
1639
+ }
1640
+ function WrappedDocument({ ir, className }) {
1641
+ const [theme, setThemeState] = useState(currentTheme);
1642
+ const [, setVersion] = useState(registryVersion);
1643
+ const forceUpdate = useCallback(() => {
1644
+ setThemeState(currentTheme);
1645
+ setVersion(registryVersion);
1646
+ }, []);
1647
+ useMemo(() => {
1648
+ listeners.add(forceUpdate);
1649
+ return () => {
1650
+ listeners.delete(forceUpdate);
1651
+ };
1652
+ }, [forceUpdate]);
1653
+ return /* @__PURE__ */ jsx(
1654
+ RuntimeProvider,
1655
+ {
1656
+ registry,
1657
+ references: ir.references,
1658
+ theme,
1659
+ onDiagnostic: config.onDiagnostic,
1660
+ onNavigate: config.onNavigate,
1661
+ children: /* @__PURE__ */ jsx(GlyphDocument, { ir, className, animation: config.animation })
1662
+ }
1663
+ );
1664
+ }
1665
+ return {
1666
+ GlyphDocument: WrappedDocument,
1667
+ registerComponent(definition) {
1668
+ registry.registerComponent(definition);
1669
+ registryVersion++;
1670
+ notify();
1671
+ },
1672
+ setTheme(theme) {
1673
+ currentTheme = theme;
1674
+ notify();
1675
+ }
1676
+ };
1677
+ }
1678
+ function useIsClient() {
1679
+ const [isClient, setIsClient] = useState(false);
1680
+ useEffect(() => {
1681
+ setIsClient(true);
1682
+ }, []);
1683
+ return isClient;
1684
+ }
1685
+ function SSRPlaceholder({
1686
+ width = "100%",
1687
+ height = 300,
1688
+ className,
1689
+ children
1690
+ }) {
1691
+ const isClient = useIsClient();
1692
+ if (!isClient) {
1693
+ const style = {
1694
+ width,
1695
+ height,
1696
+ // Subtle background so the reserved space is visible in the layout
1697
+ backgroundColor: "var(--glyph-ssr-placeholder-bg, #f0f0f0)"
1698
+ };
1699
+ return /* @__PURE__ */ jsx(
1700
+ "div",
1701
+ {
1702
+ className,
1703
+ style,
1704
+ "data-ssr-placeholder": "true",
1705
+ "aria-hidden": "true"
1706
+ }
1707
+ );
1708
+ }
1709
+ return /* @__PURE__ */ jsx(Fragment, { children });
1710
+ }
1711
+
1712
+ export { AnimationContext, AnimationProvider, BlockDiagnosticIndicator, BlockRenderer, PluginRegistry as ComponentRegistry, ContainerMeasure, DashboardLayout, DiagnosticsOverlay, DocumentLayout, ErrorBoundary, FallbackRenderer, GlyphBlockquote, GlyphCodeBlock, GlyphDocument, GlyphHeading, GlyphImage, GlyphList, GlyphParagraph, GlyphRawHtml, GlyphThematicBreak, InlineRenderer, LayoutProvider, PluginRegistry, PresentationLayout, ReferenceIndicator, RuntimeProvider, SSRPlaceholder, ThemeProvider, builtInRenderers, createGlyphRuntime, createResolveVar, darkTheme, isDarkTheme, lightTheme, mergeThemeDefaults, resolveComponentProps, resolveTheme, resolveTier, useAnimation, useBlockAnimation, useGlyphTheme, useIsClient, useLayout, useNavigation, useReferences, useRuntime, validateComponentDefinition };
1713
+ //# sourceMappingURL=index.js.map
1714
+ //# sourceMappingURL=index.js.map