@demlik/tea 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/cache/index.d.ts +151 -0
  2. package/dist/cache/index.js +53 -0
  3. package/dist/cache/index.js.map +1 -0
  4. package/dist/chunk-2YFLOCWX.js +15 -0
  5. package/dist/chunk-2YFLOCWX.js.map +1 -0
  6. package/dist/chunk-CMG2KMWX.js +458 -0
  7. package/dist/chunk-CMG2KMWX.js.map +1 -0
  8. package/dist/chunk-PYBQREFU.js +95 -0
  9. package/dist/chunk-PYBQREFU.js.map +1 -0
  10. package/dist/chunk-PYOCUBQJ.js +196 -0
  11. package/dist/chunk-PYOCUBQJ.js.map +1 -0
  12. package/dist/chunk-Y5JM3UUH.js +126 -0
  13. package/dist/chunk-Y5JM3UUH.js.map +1 -0
  14. package/dist/chunk-ZRUST6BK.js +62 -0
  15. package/dist/chunk-ZRUST6BK.js.map +1 -0
  16. package/dist/circuit-breaker/index.d.ts +196 -0
  17. package/dist/circuit-breaker/index.js +53 -0
  18. package/dist/circuit-breaker/index.js.map +1 -0
  19. package/dist/deadline/index.d.ts +113 -0
  20. package/dist/deadline/index.js +20 -0
  21. package/dist/deadline/index.js.map +1 -0
  22. package/dist/debounce/index.d.ts +114 -0
  23. package/dist/debounce/index.js +56 -0
  24. package/dist/debounce/index.js.map +1 -0
  25. package/dist/devtools/index.d.ts +137 -0
  26. package/dist/devtools/index.js +217 -0
  27. package/dist/devtools/index.js.map +1 -0
  28. package/dist/devtools/styles.css +218 -0
  29. package/dist/do/index.d.ts +135 -0
  30. package/dist/do/index.js +54 -0
  31. package/dist/do/index.js.map +1 -0
  32. package/dist/extension/index.d.ts +207 -0
  33. package/dist/extension/index.js +26 -0
  34. package/dist/extension/index.js.map +1 -0
  35. package/dist/extension/react.d.ts +121 -0
  36. package/dist/extension/react.js +69 -0
  37. package/dist/extension/react.js.map +1 -0
  38. package/dist/extension/subs/index.d.ts +44 -0
  39. package/dist/extension/subs/index.js +83 -0
  40. package/dist/extension/subs/index.js.map +1 -0
  41. package/dist/extension/test-utils.d.ts +128 -0
  42. package/dist/extension/test-utils.js +269 -0
  43. package/dist/extension/test-utils.js.map +1 -0
  44. package/dist/idempotency/index.d.ts +131 -0
  45. package/dist/idempotency/index.js +58 -0
  46. package/dist/idempotency/index.js.map +1 -0
  47. package/dist/index.d.ts +364 -0
  48. package/dist/index.js +3 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/machine-viz/index.d.ts +78 -0
  51. package/dist/machine-viz/index.js +139 -0
  52. package/dist/machine-viz/index.js.map +1 -0
  53. package/dist/mem/index.d.ts +45 -0
  54. package/dist/mem/index.js +19 -0
  55. package/dist/mem/index.js.map +1 -0
  56. package/dist/node/index.d.ts +134 -0
  57. package/dist/node/index.js +89 -0
  58. package/dist/node/index.js.map +1 -0
  59. package/dist/pbt/arbitraries/index.d.ts +135 -0
  60. package/dist/pbt/arbitraries/index.js +3 -0
  61. package/dist/pbt/arbitraries/index.js.map +1 -0
  62. package/dist/pbt/index.d.ts +28 -0
  63. package/dist/pbt/index.js +18 -0
  64. package/dist/pbt/index.js.map +1 -0
  65. package/dist/pbt/runners/index.d.ts +117 -0
  66. package/dist/pbt/runners/index.js +4 -0
  67. package/dist/pbt/runners/index.js.map +1 -0
  68. package/dist/rate-limit/index.d.ts +115 -0
  69. package/dist/rate-limit/index.js +45 -0
  70. package/dist/rate-limit/index.js.map +1 -0
  71. package/dist/react/index.d.ts +72 -0
  72. package/dist/react/index.js +33 -0
  73. package/dist/react/index.js.map +1 -0
  74. package/dist/recorder/index.d.ts +287 -0
  75. package/dist/recorder/index.js +157 -0
  76. package/dist/recorder/index.js.map +1 -0
  77. package/dist/retry-backoff/index.d.ts +131 -0
  78. package/dist/retry-backoff/index.js +37 -0
  79. package/dist/retry-backoff/index.js.map +1 -0
  80. package/dist/subs/index.d.ts +57 -0
  81. package/dist/subs/index.js +4 -0
  82. package/dist/subs/index.js.map +1 -0
  83. package/dist/testing/index.d.ts +220 -0
  84. package/dist/testing/index.js +105 -0
  85. package/dist/testing/index.js.map +1 -0
  86. package/dist/throttle/index.d.ts +125 -0
  87. package/dist/throttle/index.js +54 -0
  88. package/dist/throttle/index.js.map +1 -0
  89. package/dist/trace-replay/index.d.ts +52 -0
  90. package/dist/trace-replay/index.js +57 -0
  91. package/dist/trace-replay/index.js.map +1 -0
  92. package/dist/types-CwBC5wMn.d.ts +5 -0
  93. package/dist/work-queue/index.d.ts +56 -0
  94. package/dist/work-queue/index.js +143 -0
  95. package/dist/work-queue/index.js.map +1 -0
  96. package/package.json +184 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/debounce/index.ts"],"names":[],"mappings":";AA8GO,SAAS,QAAA,CACd,EAAA,EACA,EAAA,EACA,IAAA,EACc;AACd,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,IAAW,KAAA;AACjC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,IAAA;AAMnC,EAAA,IAAI,MAAA,GAA+C,IAAA;AAOnD,EAAA,IAAI,WAAA,GAAwB,IAAA;AAK5B,EAAA,IAAI,YAAA,GAAe,KAAA;AAEnB,EAAA,SAAS,WAAA,GAAoB;AAC3B,IAAA,IAAI,WAAW,IAAA,EAAM;AACnB,MAAA,YAAA,CAAa,MAAM,CAAA;AACnB,MAAA,MAAA,GAAS,IAAA;AAAA,IACX;AAAA,EACF;AAKA,EAAA,SAAS,cAAA,GAAuB;AAC9B,IAAA,MAAA,GAAS,IAAA;AAQT,IAAA,IAAI,QAAA,IAAY,gBAAgB,IAAA,EAAM;AACpC,MAAA,MAAM,IAAA,GAAO,WAAA;AACb,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ;AAEA,IAAA,YAAA,GAAe,KAAA;AAAA,EACjB;AAEA,EAAA,MAAM,SAAA,GAAY,IAAI,IAAA,KAAkB;AAItC,IAAA,MAAM,eAAe,MAAA,KAAW,IAAA;AAChC,IAAA,IAAI,OAAA,IAAW,YAAA,IAAgB,CAAC,YAAA,EAAc;AAC5C,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ,WAAW,QAAA,EAAU;AAGnB,MAAA,WAAA,GAAc,IAAA;AAAA,IAChB;AAKA,IAAA,WAAA,EAAY;AACZ,IAAA,MAAA,GAAS,UAAA,CAAW,gBAAgB,EAAE,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,SAAA,CAAU,SAAS,MAAY;AAC7B,IAAA,WAAA,EAAY;AACZ,IAAA,WAAA,GAAc,IAAA;AACd,IAAA,YAAA,GAAe,KAAA;AAAA,EACjB,CAAA;AAEA,EAAA,SAAA,CAAU,QAAQ,MAAY;AAI5B,IAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,MAAA,MAAM,IAAA,GAAO,WAAA;AACb,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,WAAA,EAAY;AAEZ,MAAA,YAAA,GAAe,KAAA;AACf,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ,CAAA,MAAO;AAGL,MAAA,WAAA,EAAY;AACZ,MAAA,YAAA,GAAe,KAAA;AAAA,IACjB;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,SAAA;AACT","file":"index.js","sourcesContent":["/**\n * @demlik/tea/debounce — a timer-based call transformer that coalesces a burst\n * of calls into a single fire.\n *\n * Sibling of `@demlik/tea/throttle` — the two share a SYMMETRIC surface\n * (`Debounced<A>` / `Throttled<A>`, both callable + `.cancel()` + `.flush()`,\n * both `{ leading?, trailing? }`) so they read as a pair. They are kept in\n * separate files with NO cross-import: each is a self-contained timer, and the\n * coalesce-vs-drop policies are different enough that sharing code would hide\n * the distinction. Read one to understand the other; do not couple them.\n *\n * WHAT THIS IS NOT — and why it lives outside `update`:\n *\n * - NOT a reducer / Transitions cell. Reducers are PURE (invariant 2 — same\n * inputs → same outputs, no `Date.now`, no timers). A debounce holds a live\n * `setTimeout` handle and mutable pending-args state; folding it into\n * `update` would smuggle a timer into the pure core.\n * - NOT a Sub. A Sub is a continuous SOURCE of Msgs the substrate reconciles\n * by id every transition (invariant 4). A debounce is a SINK transformer:\n * it wraps a `void`-returning handler (the canonical one is\n * `Runtime.dispatch`) so bursty HOST events coalesce BEFORE they ever\n * become a Msg.\n *\n * It is a higher-order function at the host boundary (pattern 14 — TEA owns\n * state, JS owns timing). The host attaches the wrapped function to a noisy\n * event source (`scroll`, `resize`, `input`, a ResizeObserver) and the\n * substrate downstream only ever sees the coalesced Msg:\n *\n * const onResized = debounce(\n * () => runtime.dispatch({ type: \"BrowserResizedWindow\" }),\n * 150,\n * );\n * window.addEventListener(\"resize\", onResized);\n * // cleanup: onResized.cancel(); window.removeEventListener(\"resize\", onResized);\n *\n * NOT exported from the package root — reached via the `@demlik/tea/debounce`\n * subpath, same one-shape-per-package rule as `rate-limit`, `deadline`, and\n * `work-queue`.\n *\n * Uses the universal `setTimeout` / `clearTimeout` timer idiom (same discipline\n * as `subs/from-timeout.ts`): exactly one live handle at a time, every arm\n * clears the previous, every fire / cancel clears the handle and nulls it so a\n * stale callback can never run.\n */\n\n/**\n * A debounced wrapper around `fn`. Callable with the SAME argument tuple as\n * `fn` — `(...args: A) => void` — plus two control methods that share names and\n * shapes with `Throttled<A>` so the two modules read as a symmetric pair.\n */\nexport interface Debounced<A extends readonly unknown[]> {\n /** Schedule (or re-schedule) a fire of the wrapped `fn`. See `debounce`. */\n (...args: A): void;\n /**\n * Drop any pending trailing fire WITHOUT invoking `fn`. Clears the timer and\n * forgets the captured args. Idempotent — calling it with nothing pending is\n * a no-op. The leading-edge latch (if `leading` is enabled) also resets, so\n * the next call after `cancel` is treated as a fresh burst.\n *\n * The host-cleanup partner of `removeEventListener`: cancel the pending fire\n * when the component unmounts / the listener detaches, so a queued\n * `dispatch` can't land after teardown.\n */\n cancel(): void;\n /**\n * Fire any pending trailing call IMMEDIATELY with its captured args, then\n * clear the timer. No-op when nothing is pending. Use to force the last\n * coalesced call out early — e.g. flush a debounced save on `beforeunload`,\n * or flush a debounced search when the user presses Enter.\n */\n flush(): void;\n}\n\n/**\n * Wrap `fn` so a BURST of calls collapses to a single invocation.\n *\n * Trailing edge (the default): each call (re)starts a `ms` timer; `fn` fires\n * once with the LAST args after `ms` of quiet. Every call within `ms` of the\n * previous resets the timer — the classic \"wait until the user stops typing\"\n * behavior.\n *\n * @param fn The handler to coalesce. Typically `(...) => runtime.dispatch(...)`,\n * but any `(...args: A) => void` works. The return value is\n * ignored — debounce is a fire transformer, not a value pipe.\n * @param ms Quiet window in milliseconds. Calls closer together than `ms`\n * coalesce; a gap of at least `ms` lets the trailing fire through.\n * @param opts.leading Fire on the FIRST call of a burst (the leading edge),\n * with that first call's args. Default `false`.\n * @param opts.trailing Fire on the trailing edge with the LAST call's args.\n * Default `true`.\n *\n * Edge combinations (matching lodash's settled semantics, the de-facto\n * standard the ecosystem rediscovered):\n *\n * - `{ trailing: true }` (default): one fire, after the burst, last args.\n * - `{ leading: true, trailing: false }`: one fire, at the burst START,\n * first args. Subsequent calls within the window are swallowed; the timer\n * only re-opens the \"can lead again\" latch after `ms` of quiet.\n * - `{ leading: true, trailing: true }`: fires at the start AND end of a\n * burst — BUT the trailing fire is SUPPRESSED for a burst of exactly ONE\n * call (the leading fire already covered it, so a lone call doesn't\n * double-fire). This matches lodash and is the behavior tests pin.\n * - `{ leading: false, trailing: false }`: never fires. Degenerate but legal;\n * we don't throw — the caller asked for a no-op transformer.\n *\n * WHY a default of trailing-only: a debounce's whole job is \"act after the\n * activity settles.\" The trailing edge IS that semantic — fire with the final\n * state of the burst (the last keystroke, the final scroll position). Leading\n * is the opt-in for \"respond instantly, then go quiet.\"\n */\nexport function debounce<A extends readonly unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n opts?: { leading?: boolean; trailing?: boolean },\n): Debounced<A> {\n const leading = opts?.leading ?? false;\n const trailing = opts?.trailing ?? true;\n\n // Exactly one live handle at a time (the from-timeout discipline). `null`\n // when no fire is pending — the single source of truth for \"is a timer\n // armed?\". Every arm clears the previous handle before opening a new one, so\n // we never leak overlapping timers across a burst.\n let handle: ReturnType<typeof setTimeout> | null = null;\n // The most recent args, held only while a trailing fire is pending. Captured\n // on every call (the trailing edge fires with the LAST args), cleared the\n // instant we fire / cancel so a stale tuple can never leak into a later\n // burst. `null` (not `undefined`) is the empty sentinel because `A` may\n // itself contain `undefined` elements — a present-but-empty tuple is still a\n // pending call, distinct from \"nothing pending\".\n let pendingArgs: A | null = null;\n // Leading-edge latch: true once a leading fire has happened for the current\n // burst, reset to false after `ms` of quiet (the timer's expiry) or on\n // `cancel`. Gates the \"fire on first call of a burst, then stay quiet\" rule\n // so mid-burst calls don't re-trigger the leading fire.\n let leadingFired = false;\n\n function clearHandle(): void {\n if (handle !== null) {\n clearTimeout(handle);\n handle = null;\n }\n }\n\n // Timer expiry: the burst has gone quiet for `ms`. Fire the trailing edge if\n // one is owed, then reset all burst state so the NEXT call starts a fresh\n // burst (re-arming the leading latch, clearing pending args).\n function onTimerExpired(): void {\n handle = null;\n // Suppress the trailing fire for a lone leading-only-eligible call:\n // when `leadingFired` is true AND trailing is enabled, a burst of exactly\n // one call already fired on the leading edge — `pendingArgs` was cleared\n // at that point, so there is nothing to fire here. A burst of 2+ calls\n // re-captured `pendingArgs` after the leading fire, so the trailing fire\n // lands with the last args. The `pendingArgs !== null` guard encodes\n // exactly that distinction; no separate call-count needed.\n if (trailing && pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n fn(...args);\n }\n // Burst over — re-open the leading latch for the next burst.\n leadingFired = false;\n }\n\n const debounced = (...args: A): void => {\n // Leading edge: the FIRST call of a burst (no timer armed yet, latch open).\n // Fire immediately with this call's args; do NOT stash them as pending so a\n // lone call won't also fire on the trailing edge.\n const isBurstStart = handle === null;\n if (leading && isBurstStart && !leadingFired) {\n leadingFired = true;\n fn(...args);\n } else if (trailing) {\n // Capture the latest args for the trailing fire. Overwrites any prior\n // pending tuple — the trailing edge fires with the LAST args, by design.\n pendingArgs = args;\n }\n\n // (Re)arm the quiet-window timer. Clearing the previous handle is what\n // makes the window SLIDE: every call within `ms` pushes the trailing fire\n // out to `ms` after THIS call.\n clearHandle();\n handle = setTimeout(onTimerExpired, ms);\n };\n\n debounced.cancel = (): void => {\n clearHandle();\n pendingArgs = null;\n leadingFired = false;\n };\n\n debounced.flush = (): void => {\n // Force the pending trailing fire out now. Mirrors `onTimerExpired`'s fire\n // branch but is caller-driven rather than timer-driven, so it also tears\n // down the armed timer (a timer-driven expiry already nulled `handle`).\n if (pendingArgs !== null) {\n const args = pendingArgs;\n pendingArgs = null;\n clearHandle();\n // A flushed trailing fire ends the burst, same as a natural expiry.\n leadingFired = false;\n fn(...args);\n } else {\n // Nothing pending — still tear down a lingering leading-only timer so a\n // post-flush call leads cleanly.\n clearHandle();\n leadingFired = false;\n }\n };\n\n return debounced;\n}\n"]}
@@ -0,0 +1,137 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type MsgLogEntry = {
4
+ /** Display timestamp (whatever format you produced). */
5
+ ts: string;
6
+ /** Visual category — drives the row icon. Defaults to "msg" via CSS. */
7
+ kind?: "msg" | "sub" | "cmd" | "ok" | "fail";
8
+ /** Row text — usually the Msg tag plus a short detail. */
9
+ text: string;
10
+ };
11
+ interface MsgLogProps {
12
+ history: readonly MsgLogEntry[];
13
+ className?: string;
14
+ }
15
+ declare function MsgLog({ history, className }: MsgLogProps): react_jsx_runtime.JSX.Element;
16
+
17
+ /**
18
+ * A single differing cell between two states.
19
+ *
20
+ * - `kind: "added"` — the path exists in `actual` but not `expected`
21
+ * (`expected` is `undefined`).
22
+ * - `kind: "removed"` — the path exists in `expected` but not `actual`
23
+ * (`actual` is `undefined`).
24
+ * - `kind: "changed"` — the path exists on both sides with differing leaf
25
+ * values (or differing kinds, e.g. object vs array).
26
+ */
27
+ interface StateChange {
28
+ /** Access path from the state root, e.g. `state.items[1].status`. */
29
+ path: string;
30
+ /** How the cell differs between the two states. */
31
+ kind: "added" | "removed" | "changed";
32
+ /** The value on the `expected` side (`undefined` when `added`). */
33
+ expected: unknown;
34
+ /** The value on the `actual` side (`undefined` when `removed`). */
35
+ actual: unknown;
36
+ }
37
+ /**
38
+ * Deep-walk two states and return EVERY differing cell, in deterministic
39
+ * pre-order (the node before its children; children in array-index order, then
40
+ * sorted-union key order for objects). The empty array means the two states are
41
+ * deeply equal.
42
+ *
43
+ * Equality + path grammar match `../trace-replay`'s `firstDivergence` exactly:
44
+ * - `Object.is` leaf equality, so `NaN` equals `NaN` and `+0` ≠ `-0`.
45
+ * - arrays index by `[i]`; a length mismatch is NOT reported at the array
46
+ * node — instead each surplus/missing element surfaces as its own
47
+ * added/removed child, which is what a devtools list wants to show.
48
+ * - objects walk the sorted union of both sides' keys; a key on only one
49
+ * side becomes an `added`/`removed` child.
50
+ * - the root path is `"state"`.
51
+ *
52
+ * Scope is JSON-shaped state (the same substrate trace-replay targets):
53
+ * primitives, null/undefined, arrays, plain objects. A differing kind at a node
54
+ * (object vs array, number vs string) is reported as a single `changed` at that
55
+ * node — the walk does not descend into mismatched kinds.
56
+ */
57
+ declare function diffState(expected: unknown, actual: unknown): StateChange[];
58
+ /**
59
+ * Pretty multi-line text for a {@link diffState} result. One `±`-prefixed line
60
+ * per change, in the array's (pre-order, deterministic) order:
61
+ *
62
+ * - `~ state.total: 121.5 → 135` (changed: old → new)
63
+ * - `- state.coupon: "X"` (removed: only the old value)
64
+ * - `+ state.tax: 8` (added: only the new value)
65
+ *
66
+ * Deterministic given a deterministic `changes` array. An empty array yields
67
+ * the empty string.
68
+ */
69
+ declare function formatDiff(changes: StateChange[]): string;
70
+
71
+ interface StateDiffProps {
72
+ /**
73
+ * The baseline / "before" state — what you expected. In a regression view
74
+ * this is a trace's recorded `finalState`; in a time-travel view it's the
75
+ * previous model.
76
+ */
77
+ expected: unknown;
78
+ /**
79
+ * The "after" state to compare against `expected` — the recomputed or
80
+ * current model.
81
+ */
82
+ actual: unknown;
83
+ /** Optional className appended to the container. Use to override layout. */
84
+ className?: string;
85
+ }
86
+ /**
87
+ * Presentational diff of two states. Renders {@link diffState}'s output as a
88
+ * color-coded list (added / removed / changed), reusing the devtools `tea-dt-*`
89
+ * class convention so it inherits the same design tokens as `<StateInspector>`
90
+ * and `<MsgLog>` (see ./styles.css). No state, no subscription — pass the two
91
+ * states as props, exactly like the rest of /devtools.
92
+ *
93
+ * When the states are deeply equal the list is empty; a tiny "no changes" hint
94
+ * renders so an operator can tell "equal" from "not yet computed".
95
+ */
96
+ declare function StateDiff({ expected, actual, className }: StateDiffProps): react_jsx_runtime.JSX.Element;
97
+
98
+ interface StateInspectorProps {
99
+ /**
100
+ * The machine state to display. Render-derived shape — pass whatever you
101
+ * want surfaced to the inspector (often a hand-picked subset of the model).
102
+ */
103
+ state: unknown;
104
+ /**
105
+ * When this number changes, the inspector flashes. Pass `model.msgCount` or
106
+ * any other monotonic counter you already have. Omit to disable flashing.
107
+ */
108
+ flashKey?: number;
109
+ /**
110
+ * Optional className appended to the container. Use to override layout.
111
+ */
112
+ className?: string;
113
+ }
114
+ declare function StateInspector({ state, flashKey, className }: StateInspectorProps): react_jsx_runtime.JSX.Element;
115
+
116
+ /**
117
+ * Wrap a `dispatch` so every Msg it sees lands in a bounded history buffer.
118
+ *
119
+ * `derive` lets you map a Msg to a richer entry (e.g. assign `kind: "sub"` to
120
+ * subscription-triggered messages). Default: every msg becomes a `"msg"` row
121
+ * tagged with `msg.type` (or `String(msg)` if it has no `type`). The substrate
122
+ * uses `type` as the Msg discriminant — using `_tag` here would render every
123
+ * Msg as `[object Object]` in the inspector.
124
+ *
125
+ * `max` caps the buffer — oldest entries fall off (LIFO, newest first).
126
+ *
127
+ * `now` is the timestamp source for the default `derive`. Defaults to
128
+ * `nowTs` (real wall-clock). Tests inject a deterministic shim so
129
+ * snapshots don't drift; production code never sets it.
130
+ */
131
+ declare function useMsgHistory<M>(dispatch: (msg: M) => void | Promise<void>, opts?: {
132
+ max?: number;
133
+ derive?: (msg: M) => MsgLogEntry;
134
+ now?: () => string;
135
+ }): readonly [readonly MsgLogEntry[], (msg: M) => void, () => void];
136
+
137
+ export { MsgLog, type MsgLogEntry, type MsgLogProps, type StateChange, StateDiff, type StateDiffProps, StateInspector, type StateInspectorProps, diffState, formatDiff, useMsgHistory };
@@ -0,0 +1,217 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useRef, useEffect, useState, useCallback } from 'react';
3
+
4
+ // src/devtools/msg-log.tsx
5
+ function MsgLog({ history, className }) {
6
+ return /* @__PURE__ */ jsx("div", { className: `tea-dt-log${className ? ` ${className}` : ""}`, children: history.map((r, i) => /* @__PURE__ */ jsxs(
7
+ "div",
8
+ {
9
+ className: `tea-dt-row tea-dt-${r.kind ?? "msg"}`,
10
+ children: [
11
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-t", children: r.ts }),
12
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-m", children: r.text })
13
+ ]
14
+ },
15
+ `${r.ts}-${i}`
16
+ )) });
17
+ }
18
+
19
+ // src/devtools/state-diff.ts
20
+ function typeOf(v) {
21
+ if (v === null) return "null";
22
+ if (v === void 0) return "undefined";
23
+ if (Array.isArray(v)) return "array";
24
+ if (typeof v === "object") return "object";
25
+ return "primitive";
26
+ }
27
+ function diffState(expected, actual) {
28
+ const out = [];
29
+ walk(expected, actual, "state", out);
30
+ return out;
31
+ }
32
+ function walk(expected, actual, path, out) {
33
+ if (Object.is(expected, actual)) return;
34
+ const te = typeOf(expected);
35
+ const ta = typeOf(actual);
36
+ if (te === "undefined") {
37
+ out.push({ path, kind: "added", expected, actual });
38
+ return;
39
+ }
40
+ if (ta === "undefined") {
41
+ out.push({ path, kind: "removed", expected, actual });
42
+ return;
43
+ }
44
+ if (te !== ta) {
45
+ out.push({ path, kind: "changed", expected, actual });
46
+ return;
47
+ }
48
+ if (te === "array") {
49
+ const ae = expected;
50
+ const aa = actual;
51
+ const len = Math.max(ae.length, aa.length);
52
+ for (let i = 0; i < len; i++) {
53
+ walk(ae[i], aa[i], `${path}[${i}]`, out);
54
+ }
55
+ return;
56
+ }
57
+ if (te === "object") {
58
+ const oe = expected;
59
+ const oa = actual;
60
+ const keys = [.../* @__PURE__ */ new Set([...Object.keys(oe), ...Object.keys(oa)])].sort();
61
+ for (const key of keys) {
62
+ walk(oe[key], oa[key], `${path}.${key}`, out);
63
+ }
64
+ return;
65
+ }
66
+ out.push({ path, kind: "changed", expected, actual });
67
+ }
68
+ function fmtValue(v) {
69
+ if (v === void 0) return "undefined";
70
+ if (typeof v === "number" && Number.isNaN(v)) return "NaN";
71
+ if (typeof v === "bigint") return `${v}n`;
72
+ if (typeof v === "string") return JSON.stringify(v);
73
+ if (typeof v === "function" || typeof v === "symbol") return String(v);
74
+ try {
75
+ return JSON.stringify(v) ?? String(v);
76
+ } catch {
77
+ return String(v);
78
+ }
79
+ }
80
+ function formatDiff(changes) {
81
+ return changes.map(formatChange).join("\n");
82
+ }
83
+ function formatChange(c) {
84
+ switch (c.kind) {
85
+ case "changed":
86
+ return `~ ${c.path}: ${fmtValue(c.expected)} \u2192 ${fmtValue(c.actual)}`;
87
+ case "removed":
88
+ return `- ${c.path}: ${fmtValue(c.expected)}`;
89
+ case "added":
90
+ return `+ ${c.path}: ${fmtValue(c.actual)}`;
91
+ }
92
+ }
93
+ function changeRow(c) {
94
+ switch (c.kind) {
95
+ case "changed":
96
+ return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-changed", children: [
97
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "~" }),
98
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
99
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmt(c.expected) }),
100
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-arrow", children: "\u2192" }),
101
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmt(c.actual) })
102
+ ] }, c.path);
103
+ case "removed":
104
+ return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-removed", children: [
105
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "-" }),
106
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
107
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmt(c.expected) })
108
+ ] }, c.path);
109
+ case "added":
110
+ return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-added", children: [
111
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "+" }),
112
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
113
+ /* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmt(c.actual) })
114
+ ] }, c.path);
115
+ }
116
+ }
117
+ function fmt(v) {
118
+ if (v === void 0) return "undefined";
119
+ if (typeof v === "number" && Number.isNaN(v)) return "NaN";
120
+ if (typeof v === "bigint") return `${v}n`;
121
+ if (typeof v === "string") return JSON.stringify(v);
122
+ if (typeof v === "function" || typeof v === "symbol") return String(v);
123
+ try {
124
+ return JSON.stringify(v) ?? String(v);
125
+ } catch {
126
+ return String(v);
127
+ }
128
+ }
129
+ function StateDiff({ expected, actual, className }) {
130
+ const changes = diffState(expected, actual);
131
+ return /* @__PURE__ */ jsx(
132
+ "div",
133
+ {
134
+ className: `tea-dt-diff${className ? ` ${className}` : ""}`,
135
+ title: changes.length === 0 ? "no changes" : formatDiff(changes),
136
+ children: changes.length === 0 ? /* @__PURE__ */ jsx("div", { className: "tea-dt-diff-empty", children: "no changes" }) : changes.map(changeRow)
137
+ }
138
+ );
139
+ }
140
+ function escapeHtml(s) {
141
+ return s.replace(
142
+ /[&<>"]/g,
143
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]
144
+ );
145
+ }
146
+ function fmtJson(obj, indent = 0) {
147
+ if (obj === null) return '<span class="tea-dt-b">null</span>';
148
+ if (typeof obj === "number") return `<span class="tea-dt-n">${obj}</span>`;
149
+ if (typeof obj === "boolean") return `<span class="tea-dt-b">${obj}</span>`;
150
+ if (typeof obj === "string") return `<span class="tea-dt-s">"${escapeHtml(obj)}"</span>`;
151
+ if (Array.isArray(obj)) {
152
+ if (obj.length === 0) return '<span class="tea-dt-br">[]</span>';
153
+ return '<span class="tea-dt-br">[</span>' + obj.map((v) => fmtJson(v, indent + 1)).join('<span class="tea-dt-br">, </span>') + '<span class="tea-dt-br">]</span>';
154
+ }
155
+ if (typeof obj === "object") {
156
+ const keys = Object.keys(obj);
157
+ if (keys.length === 0) return '<span class="tea-dt-br">{}</span>';
158
+ const pad = "&nbsp;&nbsp;".repeat(indent + 1);
159
+ const lines = keys.map((k, i) => {
160
+ const v = obj[k];
161
+ const comma = i < keys.length - 1 ? '<span class="tea-dt-br">,</span>' : "";
162
+ return `${pad}<span class="tea-dt-k">${escapeHtml(k)}</span><span class="tea-dt-br">:</span> ${fmtJson(v, indent + 1)}${comma}`;
163
+ });
164
+ return [
165
+ '<span class="tea-dt-br">{</span>',
166
+ ...lines,
167
+ `${"&nbsp;&nbsp;".repeat(indent)}<span class="tea-dt-br">}</span>`
168
+ ].join("<br/>");
169
+ }
170
+ return escapeHtml(String(obj));
171
+ }
172
+ function StateInspector({ state, flashKey, className }) {
173
+ const boxRef = useRef(null);
174
+ useEffect(() => {
175
+ if (flashKey === void 0 || !boxRef.current) return;
176
+ boxRef.current.classList.remove("tea-dt-flash");
177
+ void boxRef.current.offsetWidth;
178
+ boxRef.current.classList.add("tea-dt-flash");
179
+ }, [flashKey]);
180
+ return /* @__PURE__ */ jsx("div", { ref: boxRef, className: `tea-dt-state${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx(
181
+ "div",
182
+ {
183
+ className: "tea-dt-json",
184
+ dangerouslySetInnerHTML: { __html: fmtJson(state) }
185
+ }
186
+ ) });
187
+ }
188
+ function nowTs() {
189
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false }).slice(3);
190
+ }
191
+ function useMsgHistory(dispatch, opts = {}) {
192
+ const max = opts.max ?? 60;
193
+ const now = opts.now ?? nowTs;
194
+ const derive = opts.derive ?? ((msg) => {
195
+ const tag = msg && typeof msg === "object" && "type" in msg ? String(msg.type) : String(msg);
196
+ return { ts: now(), kind: "msg", text: tag };
197
+ });
198
+ const [history, setHistory] = useState([]);
199
+ const dispatchRef = useRef(dispatch);
200
+ dispatchRef.current = dispatch;
201
+ const deriveRef = useRef(derive);
202
+ deriveRef.current = derive;
203
+ const wrapped = useCallback(
204
+ (msg) => {
205
+ const entry = deriveRef.current(msg);
206
+ setHistory((prev) => [entry, ...prev].slice(0, max));
207
+ void dispatchRef.current(msg);
208
+ },
209
+ [max]
210
+ );
211
+ const clear = useCallback(() => setHistory([]), []);
212
+ return [history, wrapped, clear];
213
+ }
214
+
215
+ export { MsgLog, StateDiff, StateInspector, diffState, formatDiff, useMsgHistory };
216
+ //# sourceMappingURL=index.js.map
217
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/devtools/msg-log.tsx","../../src/devtools/state-diff.ts","../../src/devtools/state-diff-view.tsx","../../src/devtools/state-inspector.tsx","../../src/devtools/use-msg-history.ts"],"names":["jsxs","jsx","useRef"],"mappings":"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","file":"index.js","sourcesContent":["export type MsgLogEntry = {\n /** Display timestamp (whatever format you produced). */\n ts: string;\n /** Visual category — drives the row icon. Defaults to \"msg\" via CSS. */\n kind?: \"msg\" | \"sub\" | \"cmd\" | \"ok\" | \"fail\";\n /** Row text — usually the Msg tag plus a short detail. */\n text: string;\n};\n\nexport interface MsgLogProps {\n history: readonly MsgLogEntry[];\n className?: string;\n}\n\nexport function MsgLog({ history, className }: MsgLogProps) {\n return (\n <div className={`tea-dt-log${className ? ` ${className}` : \"\"}`}>\n {history.map((r, i) => (\n <div\n // biome-ignore lint/suspicious/noArrayIndexKey: log rows are stable while visible\n key={`${r.ts}-${i}`}\n className={`tea-dt-row tea-dt-${r.kind ?? \"msg\"}`}\n >\n <span className=\"tea-dt-t\">{r.ts}</span>\n <span className=\"tea-dt-m\">{r.text}</span>\n </div>\n ))}\n </div>\n );\n}\n","// ---------------------------------------------------------------------------\n// state-diff — the RICHER cousin of trace-replay's first-divergence walk.\n//\n// `../trace-replay` answers \"did the state drift, and where FIRST?\" — it stops\n// at the first differing cell because a regression check only needs to point at\n// one. This module answers the fuller devtools question: \"show me EVERY cell\n// that differs,\" classified as added / removed / changed, ready to render.\n//\n// Same substrate, same path grammar (root `state`, `foo[2]` for arrays,\n// `foo.bar` for objects, sorted-union key order, `Object.is` leaf equality) so\n// a path printed here is the SAME string trace-replay would print for that\n// cell. The two tools are deliberately consistent: a `divergence.path` from a\n// replay result indexes straight into this module's `StateChange[]`.\n//\n// Pure + zero-dep on purpose — runs in Node, in tests, in a Worker. The React\n// view (`./state-diff.tsx`) is the only React-touching layer.\n// ---------------------------------------------------------------------------\n\n/**\n * A single differing cell between two states.\n *\n * - `kind: \"added\"` — the path exists in `actual` but not `expected`\n * (`expected` is `undefined`).\n * - `kind: \"removed\"` — the path exists in `expected` but not `actual`\n * (`actual` is `undefined`).\n * - `kind: \"changed\"` — the path exists on both sides with differing leaf\n * values (or differing kinds, e.g. object vs array).\n */\nexport interface StateChange {\n /** Access path from the state root, e.g. `state.items[1].status`. */\n path: string;\n /** How the cell differs between the two states. */\n kind: \"added\" | \"removed\" | \"changed\";\n /** The value on the `expected` side (`undefined` when `added`). */\n expected: unknown;\n /** The value on the `actual` side (`undefined` when `removed`). */\n actual: unknown;\n}\n\n/** Coarse type tag used by the walk to decide how to recurse. Mirrors trace-replay. */\nfunction typeOf(\n v: unknown,\n): \"null\" | \"undefined\" | \"array\" | \"object\" | \"primitive\" {\n if (v === null) return \"null\";\n if (v === undefined) return \"undefined\";\n if (Array.isArray(v)) return \"array\";\n if (typeof v === \"object\") return \"object\";\n return \"primitive\";\n}\n\n/**\n * Deep-walk two states and return EVERY differing cell, in deterministic\n * pre-order (the node before its children; children in array-index order, then\n * sorted-union key order for objects). The empty array means the two states are\n * deeply equal.\n *\n * Equality + path grammar match `../trace-replay`'s `firstDivergence` exactly:\n * - `Object.is` leaf equality, so `NaN` equals `NaN` and `+0` ≠ `-0`.\n * - arrays index by `[i]`; a length mismatch is NOT reported at the array\n * node — instead each surplus/missing element surfaces as its own\n * added/removed child, which is what a devtools list wants to show.\n * - objects walk the sorted union of both sides' keys; a key on only one\n * side becomes an `added`/`removed` child.\n * - the root path is `\"state\"`.\n *\n * Scope is JSON-shaped state (the same substrate trace-replay targets):\n * primitives, null/undefined, arrays, plain objects. A differing kind at a node\n * (object vs array, number vs string) is reported as a single `changed` at that\n * node — the walk does not descend into mismatched kinds.\n */\nexport function diffState(expected: unknown, actual: unknown): StateChange[] {\n const out: StateChange[] = [];\n walk(expected, actual, \"state\", out);\n return out;\n}\n\nfunction walk(\n expected: unknown,\n actual: unknown,\n path: string,\n out: StateChange[],\n): void {\n // Identity / primitive fast path. Equal cells contribute nothing. `Object.is`\n // so NaN equals NaN and +0 ≠ -0 — same intent as trace-replay's leaf check.\n if (Object.is(expected, actual)) return;\n\n const te = typeOf(expected);\n const ta = typeOf(actual);\n\n // A present/absent pair at this node is an add or a remove — the structural\n // signal a viewer color-codes differently from a value change.\n if (te === \"undefined\") {\n out.push({ path, kind: \"added\", expected, actual });\n return;\n }\n if (ta === \"undefined\") {\n out.push({ path, kind: \"removed\", expected, actual });\n return;\n }\n\n // Different kinds at a present node (object vs array, number vs string,\n // value vs null) — one `changed` at the node; do not descend into mismatched\n // shapes, mirroring trace-replay's \"this node IS the divergence\".\n if (te !== ta) {\n out.push({ path, kind: \"changed\", expected, actual });\n return;\n }\n\n if (te === \"array\") {\n const ae = expected as unknown[];\n const aa = actual as unknown[];\n // Walk the union of indices so surplus elements on EITHER side surface as\n // their own added/removed children (vs trace-replay, which reports a length\n // mismatch at the array node and stops). A viewer wants the per-element\n // detail.\n const len = Math.max(ae.length, aa.length);\n for (let i = 0; i < len; i++) {\n walk(ae[i], aa[i], `${path}[${i}]`, out);\n }\n return;\n }\n\n if (te === \"object\") {\n const oe = expected as Record<string, unknown>;\n const oa = actual as Record<string, unknown>;\n // Sorted union of keys — deterministic regardless of insertion order, so\n // two states with the same data in different key order produce no changes,\n // and the reported order is reproducible run-to-run.\n const keys = [...new Set([...Object.keys(oe), ...Object.keys(oa)])].sort();\n for (const key of keys) {\n walk(oe[key], oa[key], `${path}.${key}`, out);\n }\n return;\n }\n\n // Two present primitives (string/number/boolean/bigint/symbol) or two\n // differing nulls that failed the Object.is fast path — a leaf change.\n out.push({ path, kind: \"changed\", expected, actual });\n}\n\n/** Render a single value compactly for the text diff. Strings get quotes so\n * `\"3\"` is visibly distinct from `3`; everything else is JSON-ish, with the\n * `undefined`/`NaN`/`BigInt` cases JSON.stringify mishandles spelled out. */\nfunction fmtValue(v: unknown): string {\n if (v === undefined) return \"undefined\";\n if (typeof v === \"number\" && Number.isNaN(v)) return \"NaN\";\n if (typeof v === \"bigint\") return `${v}n`;\n if (typeof v === \"string\") return JSON.stringify(v);\n if (typeof v === \"function\" || typeof v === \"symbol\") return String(v);\n try {\n return JSON.stringify(v) ?? String(v);\n } catch {\n // Circular / non-serializable — fall back to a coarse tag.\n return String(v);\n }\n}\n\n/**\n * Pretty multi-line text for a {@link diffState} result. One `±`-prefixed line\n * per change, in the array's (pre-order, deterministic) order:\n *\n * - `~ state.total: 121.5 → 135` (changed: old → new)\n * - `- state.coupon: \"X\"` (removed: only the old value)\n * - `+ state.tax: 8` (added: only the new value)\n *\n * Deterministic given a deterministic `changes` array. An empty array yields\n * the empty string.\n */\nexport function formatDiff(changes: StateChange[]): string {\n return changes.map(formatChange).join(\"\\n\");\n}\n\n/** One `±`-prefixed line for a single change. Split out so the `map` callback\n * is total (Biome's useIterableCallbackReturn) without a dead default branch. */\nfunction formatChange(c: StateChange): string {\n switch (c.kind) {\n case \"changed\":\n return `~ ${c.path}: ${fmtValue(c.expected)} → ${fmtValue(c.actual)}`;\n case \"removed\":\n return `- ${c.path}: ${fmtValue(c.expected)}`;\n case \"added\":\n return `+ ${c.path}: ${fmtValue(c.actual)}`;\n }\n}\n","import { diffState, formatDiff, type StateChange } from \"./state-diff\";\n\nexport interface StateDiffProps {\n /**\n * The baseline / \"before\" state — what you expected. In a regression view\n * this is a trace's recorded `finalState`; in a time-travel view it's the\n * previous model.\n */\n expected: unknown;\n /**\n * The \"after\" state to compare against `expected` — the recomputed or\n * current model.\n */\n actual: unknown;\n /** Optional className appended to the container. Use to override layout. */\n className?: string;\n}\n\n/** Per-kind row glyph + value rendering. Lives here (not CSS ::before like the\n * MsgLog rows) because the diff prints the path AND the value(s), so the glyph\n * is part of structured content the screen reader should read in order. */\nfunction changeRow(c: StateChange) {\n // `path` is unique within one diff (each cell appears at most once), so it is\n // a stable React key without needing the array index.\n switch (c.kind) {\n case \"changed\":\n return (\n <div key={c.path} className=\"tea-dt-diff-row tea-dt-diff-changed\">\n <span className=\"tea-dt-diff-mark\">~</span>\n <span className=\"tea-dt-diff-path\">{c.path}</span>\n <span className=\"tea-dt-diff-old\">{fmt(c.expected)}</span>\n <span className=\"tea-dt-diff-arrow\">→</span>\n <span className=\"tea-dt-diff-new\">{fmt(c.actual)}</span>\n </div>\n );\n case \"removed\":\n return (\n <div key={c.path} className=\"tea-dt-diff-row tea-dt-diff-removed\">\n <span className=\"tea-dt-diff-mark\">-</span>\n <span className=\"tea-dt-diff-path\">{c.path}</span>\n <span className=\"tea-dt-diff-old\">{fmt(c.expected)}</span>\n </div>\n );\n case \"added\":\n return (\n <div key={c.path} className=\"tea-dt-diff-row tea-dt-diff-added\">\n <span className=\"tea-dt-diff-mark\">+</span>\n <span className=\"tea-dt-diff-path\">{c.path}</span>\n <span className=\"tea-dt-diff-new\">{fmt(c.actual)}</span>\n </div>\n );\n }\n}\n\n/** Compact value render for a row cell. Strings get quotes; objects/arrays a\n * JSON-ish one-liner — the diff already names the leaf path, so a deep render\n * isn't needed here. */\nfunction fmt(v: unknown): string {\n if (v === undefined) return \"undefined\";\n if (typeof v === \"number\" && Number.isNaN(v)) return \"NaN\";\n if (typeof v === \"bigint\") return `${v}n`;\n if (typeof v === \"string\") return JSON.stringify(v);\n if (typeof v === \"function\" || typeof v === \"symbol\") return String(v);\n try {\n return JSON.stringify(v) ?? String(v);\n } catch {\n return String(v);\n }\n}\n\n/**\n * Presentational diff of two states. Renders {@link diffState}'s output as a\n * color-coded list (added / removed / changed), reusing the devtools `tea-dt-*`\n * class convention so it inherits the same design tokens as `<StateInspector>`\n * and `<MsgLog>` (see ./styles.css). No state, no subscription — pass the two\n * states as props, exactly like the rest of /devtools.\n *\n * When the states are deeply equal the list is empty; a tiny \"no changes\" hint\n * renders so an operator can tell \"equal\" from \"not yet computed\".\n */\nexport function StateDiff({ expected, actual, className }: StateDiffProps) {\n const changes = diffState(expected, actual);\n return (\n <div\n className={`tea-dt-diff${className ? ` ${className}` : \"\"}`}\n // The plain-text diff is the accessible/copyable fallback for the\n // color-coded rows — same content `formatDiff` produces in Node/tests.\n title={changes.length === 0 ? \"no changes\" : formatDiff(changes)}\n >\n {changes.length === 0 ? (\n <div className=\"tea-dt-diff-empty\">no changes</div>\n ) : (\n changes.map(changeRow)\n )}\n </div>\n );\n}\n","import { useEffect, useRef } from \"react\";\n\nfunction escapeHtml(s: string): string {\n return s.replace(\n /[&<>\"]/g,\n (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[c] as string,\n );\n}\n\nfunction fmtJson(obj: unknown, indent = 0): string {\n if (obj === null) return '<span class=\"tea-dt-b\">null</span>';\n if (typeof obj === \"number\") return `<span class=\"tea-dt-n\">${obj}</span>`;\n if (typeof obj === \"boolean\") return `<span class=\"tea-dt-b\">${obj}</span>`;\n if (typeof obj === \"string\") return `<span class=\"tea-dt-s\">\"${escapeHtml(obj)}\"</span>`;\n if (Array.isArray(obj)) {\n if (obj.length === 0) return '<span class=\"tea-dt-br\">[]</span>';\n return (\n '<span class=\"tea-dt-br\">[</span>' +\n obj.map((v) => fmtJson(v, indent + 1)).join('<span class=\"tea-dt-br\">, </span>') +\n '<span class=\"tea-dt-br\">]</span>'\n );\n }\n if (typeof obj === \"object\") {\n const keys = Object.keys(obj as object);\n if (keys.length === 0) return '<span class=\"tea-dt-br\">{}</span>';\n const pad = \"&nbsp;&nbsp;\".repeat(indent + 1);\n const lines = keys.map((k, i) => {\n const v = (obj as Record<string, unknown>)[k];\n const comma = i < keys.length - 1 ? '<span class=\"tea-dt-br\">,</span>' : \"\";\n return `${pad}<span class=\"tea-dt-k\">${escapeHtml(k)}</span><span class=\"tea-dt-br\">:</span> ${fmtJson(v, indent + 1)}${comma}`;\n });\n return [\n '<span class=\"tea-dt-br\">{</span>',\n ...lines,\n `${\"&nbsp;&nbsp;\".repeat(indent)}<span class=\"tea-dt-br\">}</span>`,\n ].join(\"<br/>\");\n }\n return escapeHtml(String(obj));\n}\n\nexport interface StateInspectorProps {\n /**\n * The machine state to display. Render-derived shape — pass whatever you\n * want surfaced to the inspector (often a hand-picked subset of the model).\n */\n state: unknown;\n /**\n * When this number changes, the inspector flashes. Pass `model.msgCount` or\n * any other monotonic counter you already have. Omit to disable flashing.\n */\n flashKey?: number;\n /**\n * Optional className appended to the container. Use to override layout.\n */\n className?: string;\n}\n\nexport function StateInspector({ state, flashKey, className }: StateInspectorProps) {\n const boxRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n if (flashKey === undefined || !boxRef.current) return;\n boxRef.current.classList.remove(\"tea-dt-flash\");\n void boxRef.current.offsetWidth;\n boxRef.current.classList.add(\"tea-dt-flash\");\n }, [flashKey]);\n\n return (\n <div ref={boxRef} className={`tea-dt-state${className ? ` ${className}` : \"\"}`}>\n <div\n className=\"tea-dt-json\"\n // biome-ignore lint/security/noDangerouslySetInnerHtml: formatter output\n dangerouslySetInnerHTML={{ __html: fmtJson(state) }}\n />\n </div>\n );\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport type { MsgLogEntry } from \"./msg-log\";\n\nfunction nowTs(): string {\n return new Date().toLocaleTimeString(\"en-GB\", { hour12: false }).slice(3);\n}\n\n/**\n * Wrap a `dispatch` so every Msg it sees lands in a bounded history buffer.\n *\n * `derive` lets you map a Msg to a richer entry (e.g. assign `kind: \"sub\"` to\n * subscription-triggered messages). Default: every msg becomes a `\"msg\"` row\n * tagged with `msg.type` (or `String(msg)` if it has no `type`). The substrate\n * uses `type` as the Msg discriminant — using `_tag` here would render every\n * Msg as `[object Object]` in the inspector.\n *\n * `max` caps the buffer — oldest entries fall off (LIFO, newest first).\n *\n * `now` is the timestamp source for the default `derive`. Defaults to\n * `nowTs` (real wall-clock). Tests inject a deterministic shim so\n * snapshots don't drift; production code never sets it.\n */\nexport function useMsgHistory<M>(\n dispatch: (msg: M) => void | Promise<void>,\n opts: {\n max?: number;\n derive?: (msg: M) => MsgLogEntry;\n now?: () => string;\n } = {},\n): readonly [readonly MsgLogEntry[], (msg: M) => void, () => void] {\n const max = opts.max ?? 60;\n const now = opts.now ?? nowTs;\n const derive =\n opts.derive ??\n ((msg: M): MsgLogEntry => {\n const tag =\n msg && typeof msg === \"object\" && \"type\" in msg\n ? String((msg as { type: unknown }).type)\n : String(msg);\n return { ts: now(), kind: \"msg\", text: tag };\n });\n\n const [history, setHistory] = useState<readonly MsgLogEntry[]>([]);\n // Stable ref to `dispatch` so the wrapper identity stays stable across\n // renders — important when this is passed to keyboard handlers and the like.\n const dispatchRef = useRef(dispatch);\n dispatchRef.current = dispatch;\n const deriveRef = useRef(derive);\n deriveRef.current = derive;\n\n const wrapped = useCallback(\n (msg: M) => {\n const entry = deriveRef.current(msg);\n setHistory((prev) => [entry, ...prev].slice(0, max));\n // Fire-and-forget — `dispatch` may return a Promise; we don't await it\n // here because callers historically treat dispatch as void.\n void dispatchRef.current(msg);\n },\n [max],\n );\n\n const clear = useCallback(() => setHistory([]), []);\n\n return [history, wrapped, clear] as const;\n}\n"]}
@@ -0,0 +1,218 @@
1
+ /*
2
+ * @demlik/tea/devtools — visual defaults. All classes are .tea-dt-* so they
3
+ * won't collide with host styles. Override anything by re-declaring with
4
+ * higher specificity, or pass your own className via the `className` prop.
5
+ *
6
+ * Colors are wired to the consumer's design tokens (shadcn slots + Radix
7
+ * accents) so the inspector inherits whatever theme is active. Overrides
8
+ * can still re-declare the --tea-dt-* vars on .tea-dt-state / .tea-dt-log.
9
+ *
10
+ * Token map:
11
+ * fg / key → --foreground (primary text)
12
+ * string → --teal-11 (accent: distinguishable green)
13
+ * number → --amber-11 (accent: warm)
14
+ * bool / null → --indigo-11 (accent: cool)
15
+ * bracket / ts → --muted-foreground (de-emphasised)
16
+ * border → --border
17
+ * log message → --foreground
18
+ * ok → --teal-11
19
+ * fail → --crimson-11
20
+ * cmd → --amber-11
21
+ * sub → --indigo-11
22
+ */
23
+
24
+ .tea-dt-state {
25
+ --tea-dt-fg: var(--foreground);
26
+ --tea-dt-key: var(--foreground);
27
+ --tea-dt-string: var(--teal-11);
28
+ --tea-dt-number: var(--amber-11);
29
+ --tea-dt-bool: var(--indigo-11);
30
+ --tea-dt-bracket: var(--muted-foreground);
31
+ --tea-dt-border: var(--border);
32
+ --tea-dt-flash: color-mix(in oklab, var(--teal-9) 18%, transparent);
33
+
34
+ font-family: var(
35
+ --font-mono,
36
+ "JetBrains Mono",
37
+ ui-monospace,
38
+ "SF Mono",
39
+ Menlo,
40
+ Consolas,
41
+ monospace
42
+ );
43
+ font-size: 12px;
44
+ color: var(--tea-dt-fg);
45
+ border: 1px dashed var(--tea-dt-border);
46
+ border-radius: var(--radius, 0.25rem);
47
+ padding: 16px 18px;
48
+ margin-bottom: 12px;
49
+ background: transparent;
50
+ }
51
+ .tea-dt-state.tea-dt-flash {
52
+ animation: tea-dt-flash 0.6s ease-out;
53
+ }
54
+ @keyframes tea-dt-flash {
55
+ 0% {
56
+ background: var(--tea-dt-flash);
57
+ border-color: var(--tea-dt-string);
58
+ }
59
+ 100% {
60
+ background: transparent;
61
+ border-color: var(--tea-dt-border);
62
+ }
63
+ }
64
+ .tea-dt-json .tea-dt-k {
65
+ color: var(--tea-dt-key);
66
+ }
67
+ .tea-dt-json .tea-dt-s {
68
+ color: var(--tea-dt-string);
69
+ }
70
+ .tea-dt-json .tea-dt-n {
71
+ color: var(--tea-dt-number);
72
+ }
73
+ .tea-dt-json .tea-dt-b {
74
+ color: var(--tea-dt-bool);
75
+ }
76
+ .tea-dt-json .tea-dt-br {
77
+ color: var(--tea-dt-bracket);
78
+ }
79
+
80
+ .tea-dt-log {
81
+ --tea-dt-log-bg: transparent;
82
+ --tea-dt-log-border: var(--border);
83
+ --tea-dt-log-ts: var(--muted-foreground);
84
+ --tea-dt-log-msg: var(--foreground);
85
+ --tea-dt-color-msg: var(--teal-11);
86
+ --tea-dt-color-sub: var(--indigo-11);
87
+ --tea-dt-color-cmd: var(--amber-11);
88
+ --tea-dt-color-ok: var(--teal-11);
89
+ --tea-dt-color-fail: var(--crimson-11);
90
+
91
+ max-height: 220px;
92
+ overflow-y: auto;
93
+ border: 1px solid var(--tea-dt-log-border);
94
+ border-radius: var(--radius, 0.25rem);
95
+ background: var(--tea-dt-log-bg);
96
+ padding: 14px 16px;
97
+ font-family: var(
98
+ --font-mono,
99
+ "JetBrains Mono",
100
+ ui-monospace,
101
+ "SF Mono",
102
+ Menlo,
103
+ Consolas,
104
+ monospace
105
+ );
106
+ font-size: 11.5px;
107
+ line-height: 1.7;
108
+ scrollbar-width: thin;
109
+ scrollbar-color: var(--tea-dt-log-border) transparent;
110
+ }
111
+ .tea-dt-log::-webkit-scrollbar {
112
+ width: 6px;
113
+ }
114
+ .tea-dt-log::-webkit-scrollbar-thumb {
115
+ background: var(--tea-dt-log-border);
116
+ }
117
+ .tea-dt-row {
118
+ display: grid;
119
+ grid-template-columns: 60px 1fr;
120
+ gap: 12px;
121
+ }
122
+ .tea-dt-row .tea-dt-t {
123
+ color: var(--tea-dt-log-ts);
124
+ }
125
+ .tea-dt-row .tea-dt-m {
126
+ color: var(--tea-dt-log-msg);
127
+ }
128
+ .tea-dt-row.tea-dt-msg .tea-dt-m::before {
129
+ content: "↓ ";
130
+ color: var(--tea-dt-color-msg);
131
+ }
132
+ .tea-dt-row.tea-dt-sub .tea-dt-m::before {
133
+ content: "≈ ";
134
+ color: var(--tea-dt-color-sub);
135
+ }
136
+ .tea-dt-row.tea-dt-cmd .tea-dt-m::before {
137
+ content: "→ ";
138
+ color: var(--tea-dt-color-cmd);
139
+ }
140
+ .tea-dt-row.tea-dt-ok .tea-dt-m::before {
141
+ content: "✓ ";
142
+ color: var(--tea-dt-color-ok);
143
+ }
144
+ .tea-dt-row.tea-dt-fail .tea-dt-m::before {
145
+ content: "✗ ";
146
+ color: var(--tea-dt-color-fail);
147
+ }
148
+
149
+ /*
150
+ * State diff (<StateDiff>). Same token strategy as .tea-dt-state / .tea-dt-log:
151
+ * accents come from the consumer's Radix scales, overridable by re-declaring
152
+ * the --tea-dt-diff-* vars.
153
+ * added → --teal-11 (new cell)
154
+ * removed → --crimson-11 (gone cell)
155
+ * changed → --amber-11 (drifted leaf)
156
+ * path → --foreground
157
+ * old val → --muted-foreground (de-emphasised; the "before")
158
+ */
159
+ .tea-dt-diff {
160
+ --tea-dt-diff-added: var(--teal-11);
161
+ --tea-dt-diff-removed: var(--crimson-11);
162
+ --tea-dt-diff-changed: var(--amber-11);
163
+ --tea-dt-diff-path: var(--foreground);
164
+ --tea-dt-diff-old: var(--muted-foreground);
165
+ --tea-dt-diff-border: var(--border);
166
+
167
+ border: 1px solid var(--tea-dt-diff-border);
168
+ border-radius: var(--radius, 0.25rem);
169
+ padding: 14px 16px;
170
+ font-family: var(
171
+ --font-mono,
172
+ "JetBrains Mono",
173
+ ui-monospace,
174
+ "SF Mono",
175
+ Menlo,
176
+ Consolas,
177
+ monospace
178
+ );
179
+ font-size: 11.5px;
180
+ line-height: 1.7;
181
+ background: transparent;
182
+ }
183
+ .tea-dt-diff-empty {
184
+ color: var(--tea-dt-diff-old);
185
+ font-style: italic;
186
+ }
187
+ .tea-dt-diff-row {
188
+ display: flex;
189
+ gap: 8px;
190
+ align-items: baseline;
191
+ flex-wrap: wrap;
192
+ }
193
+ .tea-dt-diff-mark {
194
+ width: 1ch;
195
+ flex: none;
196
+ text-align: center;
197
+ }
198
+ .tea-dt-diff-path {
199
+ color: var(--tea-dt-diff-path);
200
+ }
201
+ .tea-dt-diff-old {
202
+ color: var(--tea-dt-diff-old);
203
+ }
204
+ .tea-dt-diff-arrow {
205
+ color: var(--tea-dt-diff-old);
206
+ }
207
+ .tea-dt-diff-changed .tea-dt-diff-mark,
208
+ .tea-dt-diff-changed .tea-dt-diff-new {
209
+ color: var(--tea-dt-diff-changed);
210
+ }
211
+ .tea-dt-diff-added .tea-dt-diff-mark,
212
+ .tea-dt-diff-added .tea-dt-diff-new {
213
+ color: var(--tea-dt-diff-added);
214
+ }
215
+ .tea-dt-diff-removed .tea-dt-diff-mark,
216
+ .tea-dt-diff-removed .tea-dt-diff-old {
217
+ color: var(--tea-dt-diff-removed);
218
+ }