nimbus-docs 0.1.7 → 0.1.8

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
1
+ import { Component, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { clsx } from "clsx";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -30,8 +30,14 @@ var DiagramRegistry = class {
30
30
  this.notify();
31
31
  };
32
32
  }
33
+ /**
34
+ * Pause everything when at least one diagram is unpaused; resume
35
+ * everything otherwise. A naive per-entry toggle would *invert* mixed
36
+ * states — resuming the diagrams a reader had deliberately paused.
37
+ */
33
38
  toggleAll() {
34
- for (const entry of this.entries.values()) entry.toggle();
39
+ const anyUnpaused = [...this.entries.values()].some((e) => !e.userPaused());
40
+ for (const entry of this.entries.values()) entry.setPaused(anyUnpaused);
35
41
  }
36
42
  count = () => this.entries.size;
37
43
  getVersion = () => this.version;
@@ -71,6 +77,10 @@ function reducer(state, event) {
71
77
  ...state,
72
78
  userPaused: !state.userPaused
73
79
  };
80
+ case "SET_PAUSED": return state.userPaused === event.paused ? state : {
81
+ ...state,
82
+ userPaused: event.paused
83
+ };
74
84
  }
75
85
  }
76
86
  function derivePhase(state, reducedMotionMode, pauseWhenOffscreen) {
@@ -260,7 +270,13 @@ function DiagramRoot(props) {
260
270
  };
261
271
  }, []);
262
272
  const toggle = useCallback(() => dispatch({ type: "TOGGLE" }), []);
263
- const reset = useCallback(() => setResetKey((k) => k + 1), []);
273
+ const reset = useCallback(() => {
274
+ setResetKey((k) => k + 1);
275
+ dispatch({
276
+ type: "SET_PAUSED",
277
+ paused: false
278
+ });
279
+ }, []);
264
280
  const handleKeyDown = useCallback((event) => {
265
281
  if (!keyboard) return;
266
282
  if (isInteractiveTarget(event.target)) return;
@@ -276,12 +292,18 @@ function DiagramRoot(props) {
276
292
  toggle,
277
293
  reset
278
294
  ]);
295
+ const userPausedRef = useRef(state.userPaused);
296
+ userPausedRef.current = state.userPaused;
279
297
  useEffect(() => {
280
298
  return diagramRegistry.register({
281
299
  id,
282
- toggle
300
+ setPaused: (paused) => dispatch({
301
+ type: "SET_PAUSED",
302
+ paused
303
+ }),
304
+ userPaused: () => userPausedRef.current
283
305
  });
284
- }, [id, toggle]);
306
+ }, [id]);
285
307
  const phase = derivePhase(state, reducedMotionMode, pauseWhenOffscreen);
286
308
  const playing = phase === "playing";
287
309
  const reducedMotionEffective = reducedMotionMode === "respect" ? state.reducedMotion : false;
@@ -324,7 +346,8 @@ function DiagramRoot(props) {
324
346
  "data-tab-visible": String(state.tabVisible),
325
347
  "data-reduced-motion": String(reducedMotionEffective),
326
348
  "aria-label": label,
327
- role: "region",
349
+ role: label ? "region" : void 0,
350
+ tabIndex: keyboard ? 0 : void 0,
328
351
  onKeyDown: handleKeyDown,
329
352
  className: cn("flex flex-col", className),
330
353
  children: [/* @__PURE__ */ jsx("span", {
@@ -349,14 +372,27 @@ const Diagram = DiagramRoot;
349
372
  * automatically when any gate fails.
350
373
  *
351
374
  * Steps may carry a `when(ctx)` predicate that filters them out for a
352
- * given cycle. Mode toggles, branches, alternating loops anything
353
- * cycle-dependent composes via the predicate plus user-supplied
354
- * `context`.
375
+ * given cycle, and a `data` payload surfaced while the step holds. Mode
376
+ * toggles, branches, alternating loops anything cycle-dependent
377
+ * composes via the predicate plus user-supplied `context`.
378
+ *
379
+ * Two run modes:
380
+ * - `autoplay: true` (default) — ambient looping animation. Freezes under
381
+ * reduced motion (`data` reads `null`).
382
+ * - `autoplay: false` — idle until `start()`. Started runs are
383
+ * user-initiated *function*, not decoration: they keep advancing under
384
+ * reduced motion (gate your CSS transition durations card-side), and a
385
+ * `start()` while the page is paused arms as soon as play resumes.
386
+ *
387
+ * The scheduler depends on the current step's *values* (id, hold), not
388
+ * on `steps` / `context` identity — inline literals are safe and won't
389
+ * re-arm the hold timer under frequent re-renders.
355
390
  */
356
- function usePhase({ steps, context, loop = true }) {
391
+ function usePhase({ steps, context, loop = true, autoplay = true }) {
357
392
  const ctx = useDiagramOrDefault("usePhase");
358
393
  const [cycle, setCycle] = useState(0);
359
394
  const [index, setIndex] = useState(0);
395
+ const [running, setRunning] = useState(autoplay);
360
396
  const sequence = useMemo(() => {
361
397
  const seq = [];
362
398
  let prev = "";
@@ -379,53 +415,77 @@ function usePhase({ steps, context, loop = true }) {
379
415
  steps
380
416
  ]);
381
417
  const currentStep = sequence[index] ?? sequence[sequence.length - 1] ?? null;
382
- const currentId = currentStep?.id ?? "";
418
+ const currentId = running ? currentStep?.id ?? "" : "";
419
+ const currentHold = currentStep?.hold ?? null;
420
+ const advanceRef = useRef(() => {});
421
+ const sequenceRef = useRef(sequence);
422
+ const runningRef = useRef(running);
383
423
  useEffect(() => {
384
- if (!ctx.playing) return;
385
- if (ctx.reducedMotion) return;
386
- if (!currentStep || sequence.length === 0) return;
387
- const t = setTimeout(() => {
424
+ advanceRef.current = () => {
388
425
  if (index + 1 >= sequence.length) {
389
426
  if (loop) {
390
427
  setCycle((c) => c + 1);
391
428
  setIndex(0);
429
+ } else if (!autoplay) {
430
+ setCycle((c) => c + 1);
431
+ setIndex(0);
432
+ setRunning(false);
392
433
  }
393
434
  } else setIndex((i) => i + 1);
394
- }, currentStep.hold);
435
+ };
436
+ sequenceRef.current = sequence;
437
+ runningRef.current = running;
438
+ });
439
+ const hasSteps = steps.length > 0;
440
+ useEffect(() => {
441
+ if (!running) return;
442
+ if (!ctx.playing) return;
443
+ if (ctx.reducedMotion && autoplay) return;
444
+ if (currentHold === null) {
445
+ if (!hasSteps) return;
446
+ const t = setTimeout(() => advanceRef.current(), 0);
447
+ return () => clearTimeout(t);
448
+ }
449
+ const t = setTimeout(() => advanceRef.current(), currentHold);
395
450
  return () => clearTimeout(t);
396
451
  }, [
397
452
  index,
398
453
  cycle,
454
+ running,
455
+ autoplay,
399
456
  ctx.playing,
400
457
  ctx.reducedMotion,
401
- currentStep,
402
- sequence.length,
403
- loop
458
+ currentId,
459
+ currentHold,
460
+ hasSteps
404
461
  ]);
462
+ const start = useCallback(() => {
463
+ if (runningRef.current) return;
464
+ setIndex(0);
465
+ setRunning(true);
466
+ }, []);
467
+ const advance = useCallback(() => advanceRef.current(), []);
468
+ const goto = useCallback((id) => {
469
+ const i = sequenceRef.current.findIndex((s) => s.id === id);
470
+ if (i < 0) return;
471
+ setIndex(i);
472
+ setRunning(true);
473
+ }, []);
474
+ const reset = useCallback(() => {
475
+ setCycle(0);
476
+ setIndex(0);
477
+ setRunning(autoplay);
478
+ }, [autoplay]);
405
479
  return {
406
480
  current: currentId,
407
481
  index,
408
482
  cycle,
409
- advance: useCallback(() => {
410
- if (index + 1 >= sequence.length) {
411
- if (loop) {
412
- setCycle((c) => c + 1);
413
- setIndex(0);
414
- }
415
- } else setIndex((i) => i + 1);
416
- }, [
417
- index,
418
- sequence.length,
419
- loop
420
- ]),
421
- goto: useCallback((id) => {
422
- const i = sequence.findIndex((s) => s.id === id);
423
- if (i >= 0) setIndex(i);
424
- }, [sequence]),
425
- reset: useCallback(() => {
426
- setCycle(0);
427
- setIndex(0);
428
- }, [])
483
+ data: running && currentStep && !(ctx.reducedMotion && autoplay) ? currentStep.data ?? null : null,
484
+ running,
485
+ start,
486
+ advance,
487
+ goto,
488
+ reset
429
489
  };
430
490
  }
431
491
 
@@ -434,8 +494,7 @@ function usePhase({ steps, context, loop = true }) {
434
494
  /**
435
495
  * DOM rect + optional selector callback. The selector receives the
436
496
  * observed element and its rect, and returns whatever derived geometry
437
- * the author needs (NodeRect, EdgeData, etc.). Subsumes momito's
438
- * `useWeldedMeasure` pattern.
497
+ * the author needs (NodeRect, EdgeData, etc.).
439
498
  *
440
499
  * For multi-element measurements (e.g. measuring a container plus 5
441
500
  * child nodes), pass the container ref and read the other refs from
@@ -452,9 +511,9 @@ function useMeasure(ref, selector, options = {}) {
452
511
  const selectorRef = useRef(selector);
453
512
  selectorRef.current = selector;
454
513
  const sampleRef = useRef(() => {});
455
- useLayoutEffect(() => {
456
- const el = ref.current;
457
- if (!el) return;
514
+ const boundElRef = useRef(null);
515
+ const unbindRef = useRef(null);
516
+ const bind = (el) => {
458
517
  const sample = (r) => {
459
518
  setRect(r);
460
519
  const s = selectorRef.current;
@@ -463,18 +522,39 @@ function useMeasure(ref, selector, options = {}) {
463
522
  sampleRef.current = () => {
464
523
  sample(el.getBoundingClientRect());
465
524
  };
525
+ let cancelled = false;
526
+ el.ownerDocument.fonts?.ready.then(() => {
527
+ if (!cancelled) sampleRef.current();
528
+ });
466
529
  const win = el.ownerDocument.defaultView ?? window;
467
530
  if (typeof win.ResizeObserver === "undefined") {
468
531
  sampleRef.current();
469
- return;
532
+ return () => {
533
+ cancelled = true;
534
+ };
470
535
  }
471
- const ro = new win.ResizeObserver((entries) => {
472
- for (const entry of entries) sample(entry.contentRect);
536
+ const ro = new win.ResizeObserver(() => {
537
+ sample(el.getBoundingClientRect());
473
538
  });
474
539
  ro.observe(el);
475
540
  sampleRef.current();
476
- return () => ro.disconnect();
477
- }, [ref]);
541
+ return () => {
542
+ cancelled = true;
543
+ ro.disconnect();
544
+ };
545
+ };
546
+ useLayoutEffect(() => {
547
+ const el = ref.current;
548
+ if (el === boundElRef.current) return;
549
+ unbindRef.current?.();
550
+ boundElRef.current = el;
551
+ unbindRef.current = el ? bind(el) : null;
552
+ });
553
+ useLayoutEffect(() => () => {
554
+ unbindRef.current?.();
555
+ unbindRef.current = null;
556
+ boundElRef.current = null;
557
+ }, []);
478
558
  useLayoutEffect(() => {
479
559
  sampleRef.current();
480
560
  }, deps);
@@ -538,5 +618,165 @@ function useTabIndicator(containerRef, getTab, activeId) {
538
618
  }
539
619
 
540
620
  //#endregion
541
- export { Diagram, cn, diagramRegistry, useDiagram, useDiagramOrDefault, useMeasure, usePhase, useTabIndicator };
621
+ //#region src/react/hooks/use-ref-setters.ts
622
+ /**
623
+ * Stable per-id ref callbacks for a refs map — the ergonomic half of the
624
+ * "refs map + setter factory" pattern. Instead of N individual `useRef`s
625
+ * and N inline `ref={(el) => …}` arrows (which churn refs every render),
626
+ * keep one map and hand each element a cached callback:
627
+ *
628
+ * ```tsx
629
+ * const nodeRefs = useRef<Partial<Record<NodeId, HTMLDivElement | null>>>({});
630
+ * const setNode = useRefSetters(nodeRefs);
631
+ *
632
+ * <div ref={setNode("prompt")}>Prompt</div>
633
+ * <div ref={setNode("model")}>Model</div>
634
+ * ```
635
+ *
636
+ * Measurement stays caller-side (read `refs.current` inside a `useMeasure`
637
+ * selector) — this hook owns only the ref plumbing.
638
+ */
639
+ function useRefSetters(refs) {
640
+ const cache = useRef(/* @__PURE__ */ new Map());
641
+ return (id) => {
642
+ let cb = cache.current.get(id);
643
+ if (!cb) {
644
+ cb = (el) => {
645
+ refs.current[id] = el;
646
+ };
647
+ cache.current.set(id, cb);
648
+ }
649
+ return cb;
650
+ };
651
+ }
652
+
653
+ //#endregion
654
+ //#region src/react/hooks/use-media-query.ts
655
+ /**
656
+ * Reactive `window.matchMedia` subscription, SSR-safe.
657
+ *
658
+ * The server snapshot returns `defaultValue` (default `false`), so static
659
+ * builds render the no-match branch and hydrate without mismatch warnings;
660
+ * the first client render corrects it if the query matches.
661
+ *
662
+ * ```ts
663
+ * const isMobile = useMediaQuery("(max-width: 640px)");
664
+ * ```
665
+ */
666
+ function useMediaQuery(query, defaultValue = false) {
667
+ return useSyncExternalStore(useCallback((onChange) => {
668
+ const mq = window.matchMedia(query);
669
+ mq.addEventListener("change", onChange);
670
+ return () => mq.removeEventListener("change", onChange);
671
+ }, [query]), () => window.matchMedia(query).matches, () => defaultValue);
672
+ }
673
+
674
+ //#endregion
675
+ //#region src/react/geometry.ts
676
+ /** Anchor point on a rect edge. `frac` runs 0→1 from top/left. */
677
+ function edgePoint(rect, side, frac = .5) {
678
+ if (side === "top") return {
679
+ x: rect.l + rect.w * frac,
680
+ y: rect.t
681
+ };
682
+ if (side === "bottom") return {
683
+ x: rect.l + rect.w * frac,
684
+ y: rect.t + rect.h
685
+ };
686
+ if (side === "left") return {
687
+ x: rect.l,
688
+ y: rect.t + rect.h * frac
689
+ };
690
+ return {
691
+ x: rect.l + rect.w,
692
+ y: rect.t + rect.h * frac
693
+ };
694
+ }
695
+ function straightPath(from, to, arrowOffset) {
696
+ const dx = Math.abs(to.x - from.x);
697
+ if (Math.abs(to.y - from.y) < 2) {
698
+ const dir = to.x > from.x ? 1 : -1;
699
+ return `M ${from.x},${from.y} L ${to.x - dir * arrowOffset},${to.y}`;
700
+ }
701
+ if (dx < 2) {
702
+ const dir = to.y > from.y ? 1 : -1;
703
+ return `M ${from.x},${from.y} L ${to.x},${to.y - dir * arrowOffset}`;
704
+ }
705
+ return `M ${from.x},${from.y} L ${to.x},${to.y}`;
706
+ }
707
+ function elbowPath(from, to, r, arrowOffset) {
708
+ const dx = to.x - from.x;
709
+ const dy = to.y - from.y;
710
+ const corner = Math.abs(dx) > Math.abs(dy) ? {
711
+ x: to.x,
712
+ y: from.y
713
+ } : {
714
+ x: from.x,
715
+ y: to.y
716
+ };
717
+ const dx1 = Math.sign(corner.x - from.x);
718
+ const dy1 = Math.sign(corner.y - from.y);
719
+ const dx2 = Math.sign(to.x - corner.x);
720
+ const dy2 = Math.sign(to.y - corner.y);
721
+ const seg1Len = Math.abs(corner.x - from.x) + Math.abs(corner.y - from.y);
722
+ const seg2Len = Math.abs(to.x - corner.x) + Math.abs(to.y - corner.y);
723
+ const rr = Math.max(2, Math.min(r, seg1Len / 2, seg2Len / 2));
724
+ const preX = corner.x - dx1 * rr;
725
+ const preY = corner.y - dy1 * rr;
726
+ const postX = corner.x + dx2 * rr;
727
+ const postY = corner.y + dy2 * rr;
728
+ const endX = to.x - dx2 * arrowOffset;
729
+ const endY = to.y - dy2 * arrowOffset;
730
+ return `M ${from.x},${from.y} L ${preX},${preY} Q ${corner.x},${corner.y} ${postX},${postY} L ${endX},${endY}`;
731
+ }
732
+ function vSplitPath(from, to, r) {
733
+ if (Math.abs(from.x - to.x) < 1) return `M ${from.x},${from.y} L ${to.x},${to.y}`;
734
+ const midY = (from.y + to.y) / 2;
735
+ const sx = Math.sign(to.x - from.x);
736
+ const sy1 = Math.sign(midY - from.y);
737
+ const sy2 = Math.sign(to.y - midY);
738
+ const rr = Math.min(r, Math.abs(midY - from.y), Math.abs(to.x - from.x) / 2, Math.abs(to.y - midY));
739
+ return [
740
+ `M ${from.x},${from.y}`,
741
+ `L ${from.x},${midY - rr * sy1}`,
742
+ `Q ${from.x},${midY} ${from.x + rr * sx},${midY}`,
743
+ `L ${to.x - rr * sx},${midY}`,
744
+ `Q ${to.x},${midY} ${to.x},${midY + rr * sy2}`,
745
+ `L ${to.x},${to.y}`
746
+ ].join(" ");
747
+ }
748
+ /** Build an SVG path string between two points using the given route. */
749
+ function routeEdge(from, to, route = "auto", options = {}) {
750
+ const arrowOffset = options.arrowOffset ?? 6;
751
+ if (route === "vSplit") return vSplitPath(from, to, options.radius ?? 6);
752
+ if (route === "straight") return straightPath(from, to, arrowOffset);
753
+ if (route === "elbow") return elbowPath(from, to, options.radius ?? 10, arrowOffset);
754
+ return Math.abs(from.x - to.x) < 2 || Math.abs(from.y - to.y) < 2 ? straightPath(from, to, arrowOffset) : elbowPath(from, to, options.radius ?? 10, arrowOffset);
755
+ }
756
+ /**
757
+ * Resolve declarative edge specs against a map of measured rects.
758
+ * Specs whose endpoints aren't in `rects` yet (unmounted nodes,
759
+ * first-paint gaps) are skipped rather than rendered degenerate.
760
+ */
761
+ function resolveEdges(specs, rects, options = {}) {
762
+ const out = [];
763
+ for (const spec of specs) {
764
+ const fromRect = rects[spec.from[0]];
765
+ const toRect = rects[spec.to[0]];
766
+ if (!fromRect || !toRect) continue;
767
+ const from = edgePoint(fromRect, spec.from[1], spec.from[2] ?? .5);
768
+ const to = edgePoint(toRect, spec.to[1], spec.to[2] ?? .5);
769
+ out.push({
770
+ id: spec.id,
771
+ from,
772
+ to,
773
+ d: routeEdge(from, to, spec.route ?? "auto", options),
774
+ ghost: spec.ghost
775
+ });
776
+ }
777
+ return out;
778
+ }
779
+
780
+ //#endregion
781
+ export { Diagram, cn, diagramRegistry, edgePoint, resolveEdges, routeEdge, useDiagram, useDiagramOrDefault, useMeasure, useMediaQuery, usePhase, useRefSetters, useTabIndicator };
542
782
  //# sourceMappingURL=react.js.map