@bitcobblers/wod-wiki-lang 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/react.cjs ADDED
@@ -0,0 +1,842 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var wodWikiCore = require('@bitcobblers/wod-wiki-core');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/react/hooks/useRuntimeExecution.ts
8
+
9
+ // src/runtime/INowProvider.ts
10
+ var wallClockNow = {
11
+ now: () => /* @__PURE__ */ new Date(),
12
+ nowMs: () => Date.now()
13
+ };
14
+
15
+ // src/runtime/events/TickEvent.ts
16
+ var TickEvent = class _TickEvent {
17
+ constructor(data, now = wallClockNow) {
18
+ this.now = now;
19
+ const nowMs = this.now.nowMs();
20
+ _TickEvent._counter++;
21
+ this.timestamp = new Date(nowMs + _TickEvent._counter);
22
+ this.data = data;
23
+ }
24
+ now;
25
+ name = "tick";
26
+ timestamp;
27
+ data;
28
+ static _counter = 0;
29
+ };
30
+
31
+ // src/react/hooks/useRuntimeExecution.ts
32
+ var EXECUTION_TICK_RATE_MS = 20;
33
+ var useRuntimeExecution = (runtime) => {
34
+ const [status, setStatus] = react.useState("idle");
35
+ const [elapsedTime, setElapsedTime] = react.useState(0);
36
+ const [stepCount, setStepCount] = react.useState(0);
37
+ const [startTime, setStartTime] = react.useState(null);
38
+ const intervalRef = react.useRef(null);
39
+ const startTimeRef = react.useRef(null);
40
+ const executeStep = react.useCallback(() => {
41
+ if (!runtime) return;
42
+ try {
43
+ const tickEvent = new TickEvent(void 0, runtime.nowProvider);
44
+ runtime.handle(tickEvent);
45
+ setStepCount((prev) => prev + 1);
46
+ } catch (error) {
47
+ stop();
48
+ setStatus("error");
49
+ console.error("Runtime execution error:", error);
50
+ }
51
+ }, [runtime]);
52
+ react.useEffect(() => {
53
+ if (!runtime) return;
54
+ const unsubscribe = runtime.subscribeToStack((snapshot) => {
55
+ if (snapshot.blocks.length === 0 && (status === "running" || status === "paused")) {
56
+ if (intervalRef.current) {
57
+ clearInterval(intervalRef.current);
58
+ intervalRef.current = null;
59
+ }
60
+ setStatus("completed");
61
+ }
62
+ });
63
+ return unsubscribe;
64
+ }, [runtime, status]);
65
+ const start = react.useCallback(() => {
66
+ if (!runtime) {
67
+ console.warn("Cannot start execution: runtime is null");
68
+ return;
69
+ }
70
+ if (status === "running") {
71
+ console.warn("Cannot start execution: already running");
72
+ return;
73
+ }
74
+ if (status === "paused") {
75
+ runtime.handle({
76
+ name: "timer:resume",
77
+ timestamp: /* @__PURE__ */ new Date(),
78
+ data: {}
79
+ });
80
+ }
81
+ setStatus("running");
82
+ const now = runtime?.nowProvider?.nowMs() ?? wallClockNow.nowMs();
83
+ startTimeRef.current = now - elapsedTime;
84
+ if (elapsedTime === 0) {
85
+ setStartTime(now);
86
+ }
87
+ intervalRef.current = setInterval(executeStep, EXECUTION_TICK_RATE_MS);
88
+ }, [runtime, status, elapsedTime, executeStep]);
89
+ const pause = react.useCallback(() => {
90
+ if (intervalRef.current) {
91
+ clearInterval(intervalRef.current);
92
+ intervalRef.current = null;
93
+ }
94
+ if (runtime) {
95
+ runtime.handle({
96
+ name: "timer:pause",
97
+ timestamp: /* @__PURE__ */ new Date(),
98
+ data: {}
99
+ });
100
+ }
101
+ setStatus("paused");
102
+ }, [runtime]);
103
+ const stop = react.useCallback(() => {
104
+ if (intervalRef.current) {
105
+ clearInterval(intervalRef.current);
106
+ intervalRef.current = null;
107
+ }
108
+ setStatus("idle");
109
+ }, []);
110
+ const reset = react.useCallback(() => {
111
+ if (intervalRef.current) {
112
+ clearInterval(intervalRef.current);
113
+ intervalRef.current = null;
114
+ }
115
+ setElapsedTime(0);
116
+ setStepCount(0);
117
+ setStatus("idle");
118
+ setStartTime(null);
119
+ startTimeRef.current = null;
120
+ }, [runtime]);
121
+ const step = react.useCallback(() => {
122
+ if (!runtime) {
123
+ console.warn("Cannot step: runtime is null");
124
+ return;
125
+ }
126
+ if (status === "running") {
127
+ console.warn("Cannot step while running");
128
+ return;
129
+ }
130
+ setStatus("paused");
131
+ executeStep();
132
+ }, [runtime, status, executeStep]);
133
+ react.useEffect(() => {
134
+ if (status !== "running" || !startTimeRef.current) return;
135
+ const updateInterval = setInterval(() => {
136
+ setElapsedTime((runtime?.nowProvider?.nowMs() ?? wallClockNow.nowMs()) - startTimeRef.current);
137
+ }, 100);
138
+ return () => clearInterval(updateInterval);
139
+ }, [status]);
140
+ react.useEffect(() => {
141
+ return () => {
142
+ if (intervalRef.current) {
143
+ clearInterval(intervalRef.current);
144
+ }
145
+ };
146
+ }, []);
147
+ react.useEffect(() => {
148
+ reset();
149
+ }, [runtime, reset]);
150
+ return react.useMemo(() => ({
151
+ status,
152
+ elapsedTime,
153
+ stepCount,
154
+ startTime,
155
+ start,
156
+ pause,
157
+ stop,
158
+ reset,
159
+ step
160
+ }), [status, elapsedTime, stepCount, startTime, start, pause, stop, reset, step]);
161
+ };
162
+
163
+ // src/utils/formatTime.ts
164
+ var formatTimeMMSS = (ms) => {
165
+ if (ms === void 0 || ms === null || !Number.isFinite(ms)) {
166
+ return "--:--";
167
+ }
168
+ const isNegative = ms < 0;
169
+ const absMs = Math.abs(ms);
170
+ const totalSeconds = Math.floor(absMs / 1e3);
171
+ const minutes = Math.floor(totalSeconds / 60);
172
+ const seconds = totalSeconds % 60;
173
+ const sign = isNegative ? "-" : "";
174
+ return `${sign}${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
175
+ };
176
+ var formatTimeHHMMSS = (ms) => {
177
+ if (ms === void 0 || ms === null || !Number.isFinite(ms)) {
178
+ return "--:--:--";
179
+ }
180
+ const isNegative = ms < 0;
181
+ const absMs = Math.abs(ms);
182
+ const totalSeconds = Math.floor(absMs / 1e3);
183
+ const hours = Math.floor(totalSeconds / 3600);
184
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
185
+ const seconds = totalSeconds % 60;
186
+ const sign = isNegative ? "-" : "";
187
+ return `${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
188
+ };
189
+ var formatDurationSmart = (ms) => {
190
+ if (ms === void 0 || ms === null || !Number.isFinite(ms)) {
191
+ return "--:--";
192
+ }
193
+ const absMs = Math.abs(ms);
194
+ const totalSeconds = Math.floor(absMs / 1e3);
195
+ if (totalSeconds >= 3600) {
196
+ return formatTimeHHMMSS(ms);
197
+ }
198
+ return formatTimeMMSS(ms);
199
+ };
200
+
201
+ // src/utils/timeUtils.ts
202
+ var calculateDuration = (spans, now) => {
203
+ if (!spans || !Array.isArray(spans)) return 0;
204
+ return spans.reduce((total, span) => {
205
+ if ("started" in span) {
206
+ const start2 = span.started;
207
+ const end = span.ended ?? now;
208
+ return total + Math.max(0, end - start2);
209
+ }
210
+ const start = span.start instanceof Date ? span.start.getTime() : span.start || 0;
211
+ const stop = span.stop ? span.stop instanceof Date ? span.stop.getTime() : span.stop : now;
212
+ return total + Math.max(0, stop - start);
213
+ }, 0);
214
+ };
215
+
216
+ // src/react/hooks/useBlockMemory.ts
217
+ function useBlockMemory(block, type) {
218
+ const tag = type;
219
+ const getVal = () => {
220
+ const loc = block?.getMemoryByTag(tag)[0];
221
+ if (!loc) return void 0;
222
+ if (tag === "round") return loc.metrics[0];
223
+ return loc.metrics[0]?.value;
224
+ };
225
+ const [value, setValue] = react.useState(getVal);
226
+ react.useEffect(() => {
227
+ if (!block) {
228
+ setValue(void 0);
229
+ return;
230
+ }
231
+ setValue(getVal());
232
+ const loc = block.getMemoryByTag(tag)[0];
233
+ if (loc) {
234
+ const unsubscribe = loc.subscribe((newFrags) => {
235
+ if (tag === "round") {
236
+ setValue(newFrags[0]);
237
+ } else {
238
+ setValue(newFrags[0]?.value);
239
+ }
240
+ });
241
+ return unsubscribe;
242
+ }
243
+ return void 0;
244
+ }, [block, type]);
245
+ return value;
246
+ }
247
+ function useTimerState(block) {
248
+ return useBlockMemory(block, "time");
249
+ }
250
+ function useRoundState(block) {
251
+ return useBlockMemory(block, "round");
252
+ }
253
+ function useDisplayState(block) {
254
+ return useBlockMemory(block, "display");
255
+ }
256
+ function useTimerDisplay(block) {
257
+ const timer = useTimerState(block);
258
+ const getNow = () => {
259
+ if (typeof window.__chromecast_senderClockTimeMs === "function") {
260
+ return window.__chromecast_senderClockTimeMs();
261
+ }
262
+ return Date.now();
263
+ };
264
+ const [now, setNow] = react.useState(getNow());
265
+ const isRunning = react.useMemo(() => {
266
+ if (!timer || timer.spans.length === 0) return false;
267
+ const lastSpan = timer.spans[timer.spans.length - 1];
268
+ return lastSpan.ended === void 0;
269
+ }, [timer]);
270
+ react.useEffect(() => {
271
+ if (!isRunning) return;
272
+ let animationFrameId;
273
+ let running = true;
274
+ const tick = () => {
275
+ if (running) {
276
+ setNow(getNow());
277
+ animationFrameId = requestAnimationFrame(tick);
278
+ }
279
+ };
280
+ animationFrameId = requestAnimationFrame(tick);
281
+ return () => {
282
+ running = false;
283
+ cancelAnimationFrame(animationFrameId);
284
+ };
285
+ }, [isRunning]);
286
+ return react.useMemo(() => {
287
+ if (!timer) return null;
288
+ const elapsed = calculateDuration(timer.spans, now);
289
+ const remaining = timer.durationMs !== void 0 ? Math.max(0, timer.durationMs - elapsed) : void 0;
290
+ const isComplete = timer.durationMs !== void 0 && elapsed >= timer.durationMs;
291
+ const displayMs = timer.direction === "down" && remaining !== void 0 ? remaining : elapsed;
292
+ return {
293
+ elapsed,
294
+ remaining,
295
+ isRunning,
296
+ direction: timer.direction,
297
+ isComplete,
298
+ formatted: formatDurationSmart(displayMs)
299
+ };
300
+ }, [timer, now, isRunning]);
301
+ }
302
+ function useRoundDisplay(block) {
303
+ const round = useRoundState(block);
304
+ const display = useDisplayState(block);
305
+ return react.useMemo(() => {
306
+ if (!round) return null;
307
+ const label = display?.roundDisplay ?? (round.total !== void 0 ? `Round ${round.current} of ${round.total}` : `Round ${round.current}`);
308
+ const progress = round.total !== void 0 ? Math.min(1, (round.current - 1) / round.total) : void 0;
309
+ return {
310
+ current: round.current,
311
+ total: round.total,
312
+ label,
313
+ progress
314
+ };
315
+ }, [round, display?.roundDisplay]);
316
+ }
317
+ function useFragmentSource(block) {
318
+ const [version, setVersion] = react.useState(0);
319
+ react.useEffect(() => {
320
+ if (!block) return;
321
+ const loc = block.getMemoryByTag("metric:display")[0];
322
+ if (!loc) return;
323
+ return loc.subscribe(() => setVersion((v) => v + 1));
324
+ }, [block]);
325
+ return react.useMemo(() => {
326
+ if (!block) return void 0;
327
+ const locs = block.getMemoryByTag("metric:display");
328
+ if (locs.length === 0) return void 0;
329
+ const allFragments = wodWikiCore.MetricContainer.empty(block.key.toString());
330
+ for (const loc of locs) allFragments.merge(loc.metrics);
331
+ return allFragments;
332
+ }, [block, version]);
333
+ }
334
+ var ScriptRuntimeContext = react.createContext(void 0);
335
+ var ScriptRuntimeProvider = ({
336
+ runtime,
337
+ children
338
+ }) => {
339
+ return /* @__PURE__ */ jsxRuntime.jsx(ScriptRuntimeContext.Provider, { value: runtime, children });
340
+ };
341
+ function useScriptRuntime() {
342
+ const runtime = react.useContext(ScriptRuntimeContext);
343
+ if (!runtime) {
344
+ throw new Error("useScriptRuntime must be used within ScriptRuntimeProvider");
345
+ }
346
+ return runtime;
347
+ }
348
+
349
+ // src/react/hooks/useOutputStatements.ts
350
+ function useOutputStatements(runtime) {
351
+ const contextRuntime = useScriptRuntime();
352
+ const resolvedRuntime = runtime !== void 0 ? runtime : contextRuntime;
353
+ const [outputs, setOutputs] = react.useState(() => {
354
+ return resolvedRuntime?.getOutputStatements?.() ?? [];
355
+ });
356
+ react.useEffect(() => {
357
+ if (!resolvedRuntime) {
358
+ setOutputs([]);
359
+ return;
360
+ }
361
+ setOutputs(resolvedRuntime.getOutputStatements());
362
+ const unsubscribe = resolvedRuntime.subscribeToOutput?.(() => {
363
+ setOutputs(resolvedRuntime.getOutputStatements());
364
+ });
365
+ return unsubscribe;
366
+ }, [resolvedRuntime]);
367
+ const data = react.useMemo(() => {
368
+ const byId = /* @__PURE__ */ new Map();
369
+ const byBlockKey = /* @__PURE__ */ new Map();
370
+ const segments = [];
371
+ const metrics = [];
372
+ for (const output of outputs) {
373
+ byId.set(output.id, output);
374
+ const blockOutputs = byBlockKey.get(output.sourceBlockKey) ?? [];
375
+ blockOutputs.push(output);
376
+ byBlockKey.set(output.sourceBlockKey, blockOutputs);
377
+ switch (output.outputType) {
378
+ case "segment":
379
+ segments.push(output);
380
+ break;
381
+ case "metric":
382
+ metrics.push(output);
383
+ break;
384
+ }
385
+ }
386
+ const completions = segments.filter((s) => s.completionReason !== void 0);
387
+ return { outputs, byId, byBlockKey, segments, completions, metrics };
388
+ }, [outputs]);
389
+ return data;
390
+ }
391
+ function useLiveAnalytics(runtime) {
392
+ const contextRuntime = useScriptRuntime();
393
+ const resolvedRuntime = runtime !== void 0 ? runtime : contextRuntime;
394
+ const [analytics, setAnalytics] = react.useState(
395
+ () => resolvedRuntime?.getLiveAnalytics?.() ?? []
396
+ );
397
+ react.useEffect(() => {
398
+ if (!resolvedRuntime) {
399
+ setAnalytics([]);
400
+ return;
401
+ }
402
+ setAnalytics(resolvedRuntime.getLiveAnalytics?.() ?? []);
403
+ const unsubscribe = resolvedRuntime.subscribeToOutput?.(() => {
404
+ setAnalytics(resolvedRuntime.getLiveAnalytics?.() ?? []);
405
+ });
406
+ return unsubscribe;
407
+ }, [resolvedRuntime]);
408
+ return analytics;
409
+ }
410
+ function useOutputStatement(runtime, id) {
411
+ const { byId } = useOutputStatements(runtime);
412
+ if (id === null) return void 0;
413
+ return byId.get(id);
414
+ }
415
+ function useBlockOutputs(runtime, blockKey) {
416
+ const { byBlockKey } = useOutputStatements(runtime);
417
+ if (!blockKey) return [];
418
+ return byBlockKey.get(blockKey) ?? [];
419
+ }
420
+ function useOutputHierarchy(runtime) {
421
+ const { outputs } = useOutputStatements(runtime);
422
+ return react.useMemo(() => {
423
+ const depthMap = /* @__PURE__ */ new Map();
424
+ for (const output of outputs) {
425
+ depthMap.set(output.id, output.stackLevel);
426
+ }
427
+ return depthMap;
428
+ }, [outputs]);
429
+ }
430
+ function useMemorySubscription(memoryRef) {
431
+ const [value, setValue] = react.useState(memoryRef?.get());
432
+ react.useEffect(() => {
433
+ if (!memoryRef) {
434
+ setValue(void 0);
435
+ return;
436
+ }
437
+ setValue(memoryRef.get());
438
+ const unsubscribe = memoryRef.subscribe((newValue) => {
439
+ setValue(newValue);
440
+ });
441
+ return unsubscribe;
442
+ }, [memoryRef]);
443
+ return value;
444
+ }
445
+ function useStackSnapshot() {
446
+ const runtime = useScriptRuntime();
447
+ const [snapshot, setSnapshot] = react.useState({
448
+ type: "initial",
449
+ blocks: runtime.stack.blocks,
450
+ depth: runtime.stack.count,
451
+ clockTime: runtime.clock.currentDate
452
+ });
453
+ react.useEffect(() => {
454
+ const unsubscribe = runtime.subscribeToStack((s) => {
455
+ setSnapshot(s);
456
+ });
457
+ return unsubscribe;
458
+ }, [runtime]);
459
+ return snapshot;
460
+ }
461
+ function useSnapshotBlocks() {
462
+ const snapshot = useStackSnapshot();
463
+ return snapshot.blocks;
464
+ }
465
+ function useSnapshotCurrentBlock() {
466
+ const snapshot = useStackSnapshot();
467
+ return snapshot.blocks.length > 0 ? snapshot.blocks[snapshot.blocks.length - 1] : void 0;
468
+ }
469
+ function useNextPreview() {
470
+ const blocks = useSnapshotBlocks();
471
+ const [version, setVersion] = react.useState(0);
472
+ react.useEffect(() => {
473
+ const unsubscribes = [];
474
+ for (const block of blocks) {
475
+ const nextLocs = block.getMemoryByTag("metric:next");
476
+ for (const loc of nextLocs) {
477
+ const unsub = loc.subscribe(() => {
478
+ setVersion((v) => v + 1);
479
+ });
480
+ unsubscribes.push(unsub);
481
+ }
482
+ }
483
+ return () => {
484
+ unsubscribes.forEach((fn) => fn());
485
+ };
486
+ }, [blocks]);
487
+ return react.useMemo(() => {
488
+ for (const block of blocks) {
489
+ const nextLocs = block.getMemoryByTag("metric:next");
490
+ if (nextLocs.length === 0) continue;
491
+ const metrics = wodWikiCore.MetricContainer.empty(block.key.toString());
492
+ for (const loc of nextLocs) metrics.merge(loc.metrics);
493
+ if (metrics.length > 0) {
494
+ return { metrics, block };
495
+ }
496
+ }
497
+ return null;
498
+ }, [blocks, version]);
499
+ }
500
+ function useStackTimers() {
501
+ const blocks = useSnapshotBlocks();
502
+ const [version, setVersion] = react.useState(0);
503
+ react.useEffect(() => {
504
+ const unsubscribes = [];
505
+ for (const block of blocks) {
506
+ const timerLoc = block.getMemoryByTag("time")[0];
507
+ if (timerLoc) {
508
+ const unsub = timerLoc.subscribe(() => {
509
+ setVersion((v) => v + 1);
510
+ });
511
+ unsubscribes.push(unsub);
512
+ }
513
+ }
514
+ return () => {
515
+ unsubscribes.forEach((fn) => fn());
516
+ };
517
+ }, [blocks]);
518
+ return react.useMemo(() => {
519
+ const entries = [];
520
+ for (const block of blocks) {
521
+ const timerLoc = block.getMemoryByTag("time")[0];
522
+ if (!timerLoc) continue;
523
+ const timer = timerLoc.metrics[0]?.value;
524
+ if (!timer) continue;
525
+ entries.push({
526
+ block,
527
+ timer,
528
+ isPinned: timer.role === "primary"
529
+ });
530
+ }
531
+ return entries;
532
+ }, [blocks, version]);
533
+ }
534
+ function usePrimaryTimer() {
535
+ const timers = useStackTimers();
536
+ return react.useMemo(() => {
537
+ if (timers.length === 0) return void 0;
538
+ const leaf = timers[0];
539
+ if (leaf.block.blockType === "Rest") return leaf;
540
+ const pinned = [...timers].reverse().find((t) => t.isPinned);
541
+ if (pinned) return pinned;
542
+ return leaf;
543
+ }, [timers]);
544
+ }
545
+ function useSecondaryTimers() {
546
+ const timers = useStackTimers();
547
+ const primary = usePrimaryTimer();
548
+ return react.useMemo(() => {
549
+ if (!primary) return [];
550
+ return timers.filter((t) => t.block !== primary.block);
551
+ }, [timers, primary]);
552
+ }
553
+ function useActiveControls() {
554
+ const blocks = useSnapshotBlocks();
555
+ const [version, setVersion] = react.useState(0);
556
+ react.useEffect(() => {
557
+ const unsubscribes = [];
558
+ for (const block of blocks) {
559
+ const controlsLocs = block.getMemoryByTag("controls");
560
+ for (const loc of controlsLocs) {
561
+ const unsub = loc.subscribe(() => {
562
+ setVersion((v) => v + 1);
563
+ });
564
+ unsubscribes.push(unsub);
565
+ }
566
+ }
567
+ return () => {
568
+ unsubscribes.forEach((fn) => fn());
569
+ };
570
+ }, [blocks]);
571
+ return react.useMemo(() => {
572
+ const pinned = [];
573
+ const topButtons = [];
574
+ for (let i = 0; i < blocks.length; i++) {
575
+ const controlsLocs = blocks[i].getMemoryByTag("controls");
576
+ if (controlsLocs.length === 0) continue;
577
+ const buttons = controlsLocs[0].metrics.map((f) => f.value).filter(Boolean);
578
+ const isTop = i === blocks.length - 1;
579
+ for (const btn of buttons) {
580
+ if (btn.isPinned) {
581
+ pinned.push(btn);
582
+ } else if (isTop) {
583
+ topButtons.push(btn);
584
+ }
585
+ }
586
+ }
587
+ const seen = /* @__PURE__ */ new Set();
588
+ const result = [];
589
+ for (const btn of pinned) {
590
+ if (!seen.has(btn.id)) {
591
+ seen.add(btn.id);
592
+ result.push(btn);
593
+ }
594
+ }
595
+ for (const btn of topButtons) {
596
+ if (!seen.has(btn.id)) {
597
+ seen.add(btn.id);
598
+ result.push(btn);
599
+ }
600
+ }
601
+ return result;
602
+ }, [blocks, version]);
603
+ }
604
+ function useStackFragmentSources() {
605
+ const blocks = useSnapshotBlocks();
606
+ const [version, setVersion] = react.useState(0);
607
+ react.useEffect(() => {
608
+ const unsubscribes = [];
609
+ for (const block of blocks) {
610
+ const displayLocs = block.getMemoryByTag("metric:display");
611
+ for (const loc of displayLocs) {
612
+ const unsub = loc.subscribe(() => {
613
+ setVersion((v) => v + 1);
614
+ });
615
+ unsubscribes.push(unsub);
616
+ }
617
+ }
618
+ return () => {
619
+ unsubscribes.forEach((fn) => fn());
620
+ };
621
+ }, [blocks]);
622
+ return react.useMemo(() => {
623
+ if (blocks.length === 0) return void 0;
624
+ const entries = [];
625
+ const orderedBlocks = [...blocks].reverse();
626
+ orderedBlocks.forEach((block, index) => {
627
+ if (block.blockType === "Root" && !block.label) return;
628
+ const displayLocs = block.getMemoryByTag("metric:display");
629
+ let source;
630
+ if (displayLocs.length === 0) {
631
+ if (!block.label) return;
632
+ source = wodWikiCore.MetricContainer.empty(block.key.toString());
633
+ } else {
634
+ const mergedMetrics = wodWikiCore.MetricContainer.empty(block.key.toString());
635
+ for (const loc of displayLocs) mergedMetrics.merge(loc.metrics);
636
+ source = mergedMetrics;
637
+ }
638
+ const isLeaf = index === orderedBlocks.length - 1;
639
+ const metricGroups = displayLocs.length > 1 ? displayLocs.map((loc) => loc.metrics) : void 0;
640
+ entries.push({
641
+ source,
642
+ block,
643
+ depth: index,
644
+ isLeaf,
645
+ label: block.label,
646
+ metricGroups
647
+ });
648
+ });
649
+ return entries.length > 0 ? entries : void 0;
650
+ }, [blocks, version]);
651
+ }
652
+ function useStackDisplayRows() {
653
+ const blocks = useSnapshotBlocks();
654
+ const [version, setVersion] = react.useState(0);
655
+ react.useEffect(() => {
656
+ const unsubscribes = [];
657
+ for (const block of blocks) {
658
+ const displayLocs = block.getMemoryByTag("metric:display");
659
+ for (const loc of displayLocs) {
660
+ const unsub = loc.subscribe(() => {
661
+ setVersion((v) => v + 1);
662
+ });
663
+ unsubscribes.push(unsub);
664
+ }
665
+ }
666
+ return () => {
667
+ unsubscribes.forEach((fn) => fn());
668
+ };
669
+ }, [blocks]);
670
+ return react.useMemo(() => {
671
+ if (blocks.length === 0) return void 0;
672
+ const entries = [];
673
+ const orderedBlocks = [...blocks].reverse();
674
+ orderedBlocks.forEach((block, index) => {
675
+ if (block.blockType === "Root" && !block.label) return;
676
+ const displayLocs = block.getMemoryByTag("metric:display");
677
+ const displayRows = displayLocs.map((loc) => loc.metrics);
678
+ if (displayRows.length === 0 && !block.label) return;
679
+ const isLeaf = index === orderedBlocks.length - 1;
680
+ const metricGroups = displayLocs.length > 1 ? displayLocs.map((loc) => loc.metrics) : void 0;
681
+ entries.push({
682
+ block,
683
+ displayRows,
684
+ label: block.label,
685
+ depth: index,
686
+ isLeaf,
687
+ metricGroups
688
+ });
689
+ });
690
+ return entries.length > 0 ? entries : void 0;
691
+ }, [blocks, version]);
692
+ }
693
+
694
+ // src/runtime/browserRuntimeNow.ts
695
+ function getRuntimeNowMs() {
696
+ if (typeof window !== "undefined") {
697
+ const senderClock = window.__chromecast_senderClockTimeMs;
698
+ if (typeof senderClock === "function") {
699
+ try {
700
+ const value = senderClock();
701
+ if (Number.isFinite(value)) {
702
+ return value;
703
+ }
704
+ } catch {
705
+ }
706
+ }
707
+ }
708
+ return Date.now();
709
+ }
710
+ function createBrowserRuntimeNow() {
711
+ return {
712
+ now: () => new Date(getRuntimeNowMs()),
713
+ nowMs: () => getRuntimeNowMs()
714
+ };
715
+ }
716
+
717
+ // src/react/hooks/useTimerElapsed.ts
718
+ function useTimerElapsed(blockKey) {
719
+ const runtime = useScriptRuntime();
720
+ const block = react.useMemo(() => {
721
+ return runtime.stack.blocks.find((b) => b.key.toString() === blockKey);
722
+ }, [runtime, blockKey]);
723
+ const [timerState, setTimerState] = react.useState(() => {
724
+ return block?.getMemoryByTag("time")[0]?.metrics[0]?.value;
725
+ });
726
+ react.useEffect(() => {
727
+ const unsubscribe = runtime.stack.subscribe(() => {
728
+ const foundBlock = runtime.stack.blocks.find((b) => b.key.toString() === blockKey);
729
+ if (foundBlock) {
730
+ const timerLoc = foundBlock.getMemoryByTag("time")[0];
731
+ setTimerState(timerLoc?.metrics[0]?.value);
732
+ } else {
733
+ setTimerState(void 0);
734
+ }
735
+ });
736
+ return unsubscribe;
737
+ }, [runtime, blockKey]);
738
+ react.useEffect(() => {
739
+ if (!block) {
740
+ setTimerState(void 0);
741
+ return;
742
+ }
743
+ const timerLoc = block.getMemoryByTag("time")[0];
744
+ setTimerState(timerLoc?.metrics[0]?.value);
745
+ if (timerLoc) {
746
+ const unsubscribe = timerLoc.subscribe((newFrags) => {
747
+ setTimerState(newFrags[0]?.value);
748
+ });
749
+ return unsubscribe;
750
+ }
751
+ }, [block]);
752
+ const timeSpans = timerState?.spans || [];
753
+ const isRunning = timeSpans.length > 0 && timeSpans[timeSpans.length - 1].ended === void 0;
754
+ const [now, setNow] = react.useState(getRuntimeNowMs());
755
+ const elapsed = react.useMemo(() => {
756
+ if (timeSpans.length === 0) return 0;
757
+ return calculateDuration(timeSpans, now);
758
+ }, [timeSpans, now]);
759
+ react.useEffect(() => {
760
+ if (!isRunning) return;
761
+ let animationFrameId;
762
+ const update = () => {
763
+ setNow(getRuntimeNowMs());
764
+ animationFrameId = requestAnimationFrame(update);
765
+ };
766
+ update();
767
+ return () => cancelAnimationFrame(animationFrameId);
768
+ }, [isRunning]);
769
+ return {
770
+ elapsed,
771
+ isRunning,
772
+ timeSpans
773
+ };
774
+ }
775
+ function cn(...classes) {
776
+ return classes.filter(Boolean).join(" ");
777
+ }
778
+ var BlockTimerDisplay = ({
779
+ block,
780
+ className,
781
+ showRounds = true,
782
+ compact = false
783
+ }) => {
784
+ const timer = useTimerDisplay(block);
785
+ const round = useRoundDisplay(block);
786
+ if (!timer) {
787
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex items-center justify-center p-4 text-muted-foreground", className), children: "No timer active" });
788
+ }
789
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col items-center", className), children: [
790
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
791
+ "font-mono font-bold tabular-nums",
792
+ compact ? "text-3xl" : "text-5xl md:text-6xl",
793
+ timer.isRunning ? "text-primary" : "text-foreground"
794
+ ), children: timer.formatted }),
795
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
796
+ "text-xs uppercase tracking-wider mt-2",
797
+ timer.isRunning ? "text-success" : "text-muted-foreground"
798
+ ), children: timer.isComplete ? "Complete" : timer.isRunning ? "Running" : "Paused" }),
799
+ timer.remaining !== void 0 && timer.remaining > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-muted-foreground mt-1", children: [
800
+ Math.ceil(timer.remaining / 1e3),
801
+ "s remaining"
802
+ ] }),
803
+ showRounds && round && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(
804
+ "mt-3 px-3 py-1 rounded-full text-sm",
805
+ "bg-secondary text-secondary-foreground"
806
+ ), children: round.label })
807
+ ] });
808
+ };
809
+
810
+ exports.BlockTimerDisplay = BlockTimerDisplay;
811
+ exports.ScriptRuntimeProvider = ScriptRuntimeProvider;
812
+ exports.createBrowserRuntimeNow = createBrowserRuntimeNow;
813
+ exports.getRuntimeNowMs = getRuntimeNowMs;
814
+ exports.runtimeNow = wallClockNow;
815
+ exports.useActiveControls = useActiveControls;
816
+ exports.useBlockMemory = useBlockMemory;
817
+ exports.useBlockOutputs = useBlockOutputs;
818
+ exports.useDisplayState = useDisplayState;
819
+ exports.useFragmentSource = useFragmentSource;
820
+ exports.useLiveAnalytics = useLiveAnalytics;
821
+ exports.useMemorySubscription = useMemorySubscription;
822
+ exports.useNextPreview = useNextPreview;
823
+ exports.useOutputHierarchy = useOutputHierarchy;
824
+ exports.useOutputStatement = useOutputStatement;
825
+ exports.useOutputStatements = useOutputStatements;
826
+ exports.usePrimaryTimer = usePrimaryTimer;
827
+ exports.useRoundDisplay = useRoundDisplay;
828
+ exports.useRoundState = useRoundState;
829
+ exports.useRuntimeExecution = useRuntimeExecution;
830
+ exports.useScriptRuntime = useScriptRuntime;
831
+ exports.useSecondaryTimers = useSecondaryTimers;
832
+ exports.useSnapshotBlocks = useSnapshotBlocks;
833
+ exports.useSnapshotCurrentBlock = useSnapshotCurrentBlock;
834
+ exports.useStackDisplayRows = useStackDisplayRows;
835
+ exports.useStackFragmentSources = useStackFragmentSources;
836
+ exports.useStackSnapshot = useStackSnapshot;
837
+ exports.useStackTimers = useStackTimers;
838
+ exports.useTimerDisplay = useTimerDisplay;
839
+ exports.useTimerElapsed = useTimerElapsed;
840
+ exports.useTimerState = useTimerState;
841
+ //# sourceMappingURL=react.cjs.map
842
+ //# sourceMappingURL=react.cjs.map