@kestack/web-platform 0.0.0 → 0.0.2

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 (34) hide show
  1. package/README.md +6 -0
  2. package/dist/components/viewers/diff-viewer/diff-viewer.js +1 -1
  3. package/dist/components/viewers/source-explorer/source-explorer.js +1 -1
  4. package/dist/pwa/shared/config/config.js +1 -1
  5. package/dist/pwa/worker/requests/strategies/strategies.js +1 -1
  6. package/dist/recipes/devtools-dock/devtools-dock.d.ts +23 -0
  7. package/dist/recipes/devtools-dock/devtools-dock.d.ts.map +1 -0
  8. package/dist/recipes/devtools-dock/devtools-dock.js +343 -0
  9. package/dist/recipes/devtools-dock/devtools-dock.js.map +1 -0
  10. package/dist/recipes/devtools-dock/index.d.ts +2 -0
  11. package/dist/recipes/devtools-dock/index.js +2 -0
  12. package/dist/recipes/kabuki-devtools/kabuki-devtools.d.ts.map +1 -1
  13. package/dist/recipes/kabuki-devtools/kabuki-devtools.js +4 -1
  14. package/dist/recipes/kabuki-devtools/kabuki-devtools.js.map +1 -1
  15. package/dist/recipes/kabuki-devtools/map/canvas/canvas.d.ts.map +1 -1
  16. package/dist/recipes/kabuki-devtools/map/canvas/canvas.js +13 -0
  17. package/dist/recipes/kabuki-devtools/map/canvas/canvas.js.map +1 -1
  18. package/dist/recipes/kabuki-devtools/scene/tree.js +1 -1
  19. package/dist/recipes/kabuki-devtools/scene/tree.js.map +1 -1
  20. package/dist/recipes/kabuki-devtools/sheet/sheet.d.ts.map +1 -1
  21. package/dist/recipes/kabuki-devtools/sheet/sheet.js +84 -78
  22. package/dist/recipes/kabuki-devtools/sheet/sheet.js.map +1 -1
  23. package/dist/recipes/kabuki-devtools/timeline/timeline.d.ts +6 -1
  24. package/dist/recipes/kabuki-devtools/timeline/timeline.d.ts.map +1 -1
  25. package/dist/recipes/kabuki-devtools/timeline/timeline.js +202 -16
  26. package/dist/recipes/kabuki-devtools/timeline/timeline.js.map +1 -1
  27. package/package.json +17 -17
  28. package/src/recipes/devtools-dock/devtools-dock.tsx +505 -0
  29. package/src/recipes/devtools-dock/index.ts +2 -0
  30. package/src/recipes/kabuki-devtools/kabuki-devtools.tsx +4 -0
  31. package/src/recipes/kabuki-devtools/map/canvas/canvas.tsx +17 -1
  32. package/src/recipes/kabuki-devtools/scene/tree.ts +1 -1
  33. package/src/recipes/kabuki-devtools/sheet/sheet.tsx +95 -107
  34. package/src/recipes/kabuki-devtools/timeline/timeline.tsx +238 -19
package/README.md CHANGED
@@ -333,6 +333,12 @@ Owned shadcn copies on Base UI, one subpath per file, such as `@kestack/web-plat
333
333
  | ----------------- | -------------------------------------------------------------------------------------------------- |
334
334
  | `AccountSwitcher` | A menu of the Signed-in Accounts: switch, add an account, sign out the Active Account or everyone. |
335
335
 
336
+ ### `@kestack/web-platform/recipes/devtools-dock`
337
+
338
+ | Export | What it does |
339
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
340
+ | `DevtoolsDock` | Wraps a page with devtools opened from a corner button of one fixed size and place, or Ctrl+/: full screen, a resizable bar or side panel, or a floating window, kept across reloads; phones get only full screen and the bar. With `scrolls="window"`, a page that scrolls the window stays in the document's flow, the bar or panel fixed over it. |
341
+
336
342
  ### `@kestack/web-platform/recipes/kabuki-devtools`
337
343
 
338
344
  | Export | What it does |
@@ -1,8 +1,8 @@
1
1
  import { Button } from "../../ui/button.js";
2
2
  import { cn } from "../../lib/utils.js";
3
3
  import { Switch } from "../../ui/switch.js";
4
- import { useIsMobile } from "../../hooks/use-mobile.js";
5
4
  import { scrollbarStyles } from "../../lib/scrollStyles.js";
5
+ import { useIsMobile } from "../../hooks/use-mobile.js";
6
6
  import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "../../ui/dropdown-menu.js";
7
7
  import { collapseRows } from "./collapse.js";
8
8
  import { SourceViewer } from "../source-viewer/source-viewer.js";
@@ -2,8 +2,8 @@ import { Button } from "../../ui/button.js";
2
2
  import { lucide_exports } from "../../lib/lucide.js";
3
3
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../ui/dialog.js";
4
4
  import { cn } from "../../lib/utils.js";
5
- import { useIsMobile } from "../../hooks/use-mobile.js";
6
5
  import { scrollbarStyles } from "../../lib/scrollStyles.js";
6
+ import { useIsMobile } from "../../hooks/use-mobile.js";
7
7
  import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "../../ui/resizable.js";
8
8
  import { Spinner } from "../../ui/spinner.js";
9
9
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs.js";
@@ -1,6 +1,6 @@
1
1
  import { BuildId, PrecacheList } from "../build/build.js";
2
- import { StrategyRule } from "../strategy/strategy.js";
3
2
  import { WebAppManifest } from "../manifest/manifest.js";
3
+ import { StrategyRule } from "../strategy/strategy.js";
4
4
  import { PRESETS, PRESET_NAMES } from "./presets.js";
5
5
  import * as Schema from "effect/Schema";
6
6
  //#region src/pwa/shared/config/config.ts
@@ -1,6 +1,6 @@
1
1
  import { isRuntimeCacheName, runtimeCacheName } from "../../../shared/build/build.js";
2
- import { deleteCaches } from "../../global-scope/global-scope.js";
3
2
  import { findStrategyRule } from "../../../shared/strategy/strategy.js";
3
+ import { deleteCaches } from "../../global-scope/global-scope.js";
4
4
  import { cacheStore } from "./cache-store.js";
5
5
  import { strategies } from "./kinds.js";
6
6
  import * as Option from "effect/Option";
@@ -0,0 +1,23 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/recipes/devtools-dock/devtools-dock.d.ts
3
+ export type DockLayout = 'full' | 'bottom' | 'side' | 'floating';
4
+ export declare function DevtoolsDock({ devtools, children, label, status, storageKey, scrolls }: {
5
+ /** The devtools, drawn for the Layout they are in. */
6
+ readonly devtools: (layout: DockLayout) => ReactNode;
7
+ /** The page. */
8
+ readonly children: ReactNode;
9
+ readonly label?: string;
10
+ /** A mark on the button's corner, such as a dot for a page showing the past; it never changes the button's size. */
11
+ readonly status?: ReactNode;
12
+ /** Where the Dock keeps how it was left. */
13
+ readonly storageKey?: string;
14
+ /**
15
+ * What scrolls the page: `dock`, a box the height of the screen that the
16
+ * page fills, for an app that scrolls its own parts; or `window`, for a
17
+ * site whose page scrolls the window, which keeps its sticky headers and
18
+ * its scroll restoration.
19
+ */
20
+ readonly scrolls?: 'dock' | 'window';
21
+ }): import("react").JSX.Element;
22
+ //#endregion
23
+ //# sourceMappingURL=devtools-dock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-dock.d.ts","names":[],"sources":["../../../src/recipes/devtools-dock/devtools-dock.tsx"],"mappings":";;YA6CY;wBAyCI,eACd,UACA,UACA,OACA,QACA,YACA;;WAGS,WAAW,QAAQ,eAAe;;WAElC,UAAU;WACV;;WAEA,SAAS;;WAET;;;;;;;WAOA;oBACV,IAAA"}
@@ -0,0 +1,343 @@
1
+ import { cn } from "../../components/lib/utils.js";
2
+ import { useIsMobile } from "../../components/hooks/use-mobile.js";
3
+ import { ContextMenu, ContextMenuContent, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuTrigger } from "../../components/ui/context-menu.js";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { AppWindow, Maximize2, Network, PanelBottom, PanelRight, X } from "lucide-react";
7
+ //#region src/recipes/devtools-dock/devtools-dock.tsx
8
+ const INITIAL = {
9
+ open: false,
10
+ layout: "full",
11
+ bottom: 360,
12
+ side: 480,
13
+ floating: {
14
+ x: 48,
15
+ y: 48,
16
+ width: 720,
17
+ height: 480
18
+ }
19
+ };
20
+ const LAYOUTS = [
21
+ {
22
+ value: "full",
23
+ label: "Full screen",
24
+ Icon: Maximize2,
25
+ phone: true
26
+ },
27
+ {
28
+ value: "bottom",
29
+ label: "Bottom",
30
+ Icon: PanelBottom,
31
+ phone: true
32
+ },
33
+ {
34
+ value: "side",
35
+ label: "Right side",
36
+ Icon: PanelRight,
37
+ phone: false
38
+ },
39
+ {
40
+ value: "floating",
41
+ label: "Floating",
42
+ Icon: AppWindow,
43
+ phone: false
44
+ }
45
+ ];
46
+ /** On a phone, what a wide screen's Layout shows as. */
47
+ const ON_PHONE = {
48
+ full: "full",
49
+ bottom: "bottom",
50
+ side: "bottom",
51
+ floating: "full"
52
+ };
53
+ const MIN = 200;
54
+ const clamp = (value, low, high) => Math.min(Math.max(value, low), Math.max(low, high));
55
+ function DevtoolsDock({ devtools, children, label = "Devtools", status, storageKey = "devtools-dock", scrolls = "dock" }) {
56
+ const [kept, setKept] = useState(INITIAL);
57
+ useEffect(() => {
58
+ try {
59
+ const stored = localStorage.getItem(storageKey);
60
+ if (stored) setKept({
61
+ ...INITIAL,
62
+ ...JSON.parse(stored)
63
+ });
64
+ } catch {}
65
+ }, [storageKey]);
66
+ const keep = (change) => setKept((was) => {
67
+ const next = {
68
+ ...was,
69
+ ...typeof change === "function" ? change(was) : change
70
+ };
71
+ try {
72
+ localStorage.setItem(storageKey, JSON.stringify(next));
73
+ } catch {}
74
+ return next;
75
+ });
76
+ const phone = useIsMobile();
77
+ const offered = LAYOUTS.filter((each) => !phone || each.phone);
78
+ const { open } = kept;
79
+ const layout = phone ? ON_PHONE[kept.layout] : kept.layout;
80
+ const current = offered.find((each) => each.value === layout);
81
+ const next = offered[(offered.indexOf(current) + 1) % offered.length];
82
+ const toggle = useRef(() => {});
83
+ toggle.current = () => keep((was) => ({ open: !was.open }));
84
+ useEffect(() => {
85
+ const onKey = (event) => {
86
+ if (!((event.ctrlKey || event.metaKey) && event.key === "/")) return;
87
+ event.preventDefault();
88
+ toggle.current();
89
+ };
90
+ window.addEventListener("keydown", onKey);
91
+ return () => window.removeEventListener("keydown", onKey);
92
+ }, []);
93
+ const button = useRef(null);
94
+ const [reserve, setReserve] = useState(0);
95
+ useEffect(() => {
96
+ const at = button.current;
97
+ if (!at) return;
98
+ const measure = () => setReserve(window.innerWidth - at.getBoundingClientRect().left);
99
+ const observer = new ResizeObserver(measure);
100
+ observer.observe(at);
101
+ window.addEventListener("resize", measure);
102
+ return () => {
103
+ observer.disconnect();
104
+ window.removeEventListener("resize", measure);
105
+ };
106
+ }, []);
107
+ const clear = { "--dock-reserve": `${reserve}px` };
108
+ const docked = open && (layout === "bottom" || layout === "side");
109
+ const size = layout === "bottom" ? kept.bottom : kept.side;
110
+ const inFlow = scrolls === "window";
111
+ useEffect(() => {
112
+ if (!inFlow || !docked) return;
113
+ const side = layout === "bottom" ? "paddingBottom" : "paddingRight";
114
+ const body = document.body.style;
115
+ const before = body[side];
116
+ body[side] = `${size}px`;
117
+ return () => {
118
+ body[side] = before;
119
+ };
120
+ }, [
121
+ inFlow,
122
+ docked,
123
+ layout,
124
+ size
125
+ ]);
126
+ const aside = docked && /* @__PURE__ */ jsxs("aside", {
127
+ "aria-label": label,
128
+ className: cn("bg-background flex shrink-0 flex-col", layout === "bottom" ? "border-t" : "border-l", !inFlow ? "relative" : layout === "bottom" ? "fixed inset-x-0 bottom-0 z-40" : "fixed inset-y-0 right-0 z-40"),
129
+ style: {
130
+ ...clear,
131
+ [layout === "bottom" ? "height" : "width"]: size
132
+ },
133
+ children: [/* @__PURE__ */ jsx(Edge, {
134
+ along: layout === "bottom" ? "top" : "left",
135
+ onResize: (to) => keep(layout === "bottom" ? { bottom: clamp(window.innerHeight - to.y, MIN, window.innerHeight * .85) } : { side: clamp(window.innerWidth - to.x, 320, window.innerWidth * .85) })
136
+ }), /* @__PURE__ */ jsx("div", {
137
+ className: "min-h-0 flex-1",
138
+ children: devtools(layout)
139
+ })]
140
+ }, layout);
141
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
142
+ inFlow ? /* @__PURE__ */ jsxs(Fragment$1, { children: [children, aside] }) : /* @__PURE__ */ jsxs("div", {
143
+ className: cn("flex h-dvh w-full overflow-hidden", docked && layout === "bottom" ? "flex-col" : "flex-row"),
144
+ children: [/* @__PURE__ */ jsx("div", {
145
+ className: "relative min-h-0 min-w-0 flex-1",
146
+ children
147
+ }), aside]
148
+ }),
149
+ open && layout === "full" && /* @__PURE__ */ jsx("div", {
150
+ role: "dialog",
151
+ "aria-label": label,
152
+ className: "fixed inset-0 z-40",
153
+ style: clear,
154
+ children: devtools(layout)
155
+ }),
156
+ open && layout === "floating" && /* @__PURE__ */ jsx(Floating, {
157
+ label,
158
+ rect: kept.floating,
159
+ onChange: (floating) => keep({ floating }),
160
+ onClose: () => keep({ open: false }),
161
+ children: devtools(layout)
162
+ }),
163
+ /* @__PURE__ */ jsxs("div", {
164
+ ref: button,
165
+ className: cn("bg-background fixed right-3 bottom-[9px] z-50 flex h-9 items-center rounded-md border transition-shadow duration-150", !open && "shadow-[0_1px_2px_rgb(0_0_0/0.04),0_4px_16px_-4px_rgb(0_0_0/0.12)]"),
166
+ children: [/* @__PURE__ */ jsxs("button", {
167
+ type: "button",
168
+ "aria-pressed": open,
169
+ "aria-label": `${open ? "Close" : "Open"} ${label}`,
170
+ title: `${open ? "Close" : "Open"} ${label} (Ctrl /)`,
171
+ onClick: () => keep({ open: !open }),
172
+ className: "text-muted-foreground hover:text-foreground aria-pressed:text-foreground aria-pressed:bg-muted relative grid h-full w-9 place-items-center rounded-l-[5px] transition-colors duration-150 outline-offset-2 focus-visible:outline-2 [&_svg]:size-4",
173
+ children: [/* @__PURE__ */ jsx(Network, {}), status && /* @__PURE__ */ jsx("span", {
174
+ className: "pointer-events-none absolute top-1.5 right-1.5",
175
+ children: status
176
+ })]
177
+ }), /* @__PURE__ */ jsxs(ContextMenu, { children: [/* @__PURE__ */ jsx(ContextMenuTrigger, {
178
+ render: /* @__PURE__ */ jsx("button", {
179
+ type: "button",
180
+ "aria-label": `Devtools layout: ${current.label}`,
181
+ title: `${current.label}. Click for ${next.label.toLowerCase()}, right click to pick`,
182
+ onClick: () => keep({
183
+ layout: next.value,
184
+ open: true
185
+ }),
186
+ className: "text-muted-foreground hover:text-foreground grid h-full w-9 place-items-center rounded-r-[5px] border-l transition-colors duration-150 outline-offset-2 focus-visible:outline-2 [&_svg]:size-4"
187
+ }),
188
+ children: /* @__PURE__ */ jsx(current.Icon, {})
189
+ }), /* @__PURE__ */ jsx(ContextMenuContent, {
190
+ className: "w-44",
191
+ children: /* @__PURE__ */ jsxs(ContextMenuRadioGroup, {
192
+ value: layout,
193
+ onValueChange: (value) => keep({
194
+ layout: value,
195
+ open: true
196
+ }),
197
+ children: [/* @__PURE__ */ jsx(ContextMenuLabel, { children: "Layout" }), offered.map(({ value, label: name, Icon }) => /* @__PURE__ */ jsxs(ContextMenuRadioItem, {
198
+ value,
199
+ closeOnClick: true,
200
+ children: [/* @__PURE__ */ jsx(Icon, {}), name]
201
+ }, value))]
202
+ })
203
+ })] })]
204
+ })
205
+ ] });
206
+ }
207
+ /** Drag a pointer and follow it; the page keeps the cursor and selects nothing meanwhile. */
208
+ const drag = (cursor, onMove) => (event) => {
209
+ if (event.button !== 0) return;
210
+ event.preventDefault();
211
+ const target = event.currentTarget;
212
+ target.setPointerCapture(event.pointerId);
213
+ document.body.style.cursor = cursor;
214
+ document.body.style.userSelect = "none";
215
+ const move = (each) => onMove(each);
216
+ const up = () => {
217
+ document.body.style.cursor = "";
218
+ document.body.style.userSelect = "";
219
+ target.removeEventListener("pointermove", move);
220
+ target.removeEventListener("pointerup", up);
221
+ target.removeEventListener("pointercancel", up);
222
+ };
223
+ target.addEventListener("pointermove", move);
224
+ target.addEventListener("pointerup", up);
225
+ target.addEventListener("pointercancel", up);
226
+ };
227
+ /** The edge a docked panel is resized by. */
228
+ function Edge({ along, onResize }) {
229
+ return /* @__PURE__ */ jsx("div", {
230
+ role: "separator",
231
+ "aria-orientation": along === "top" ? "horizontal" : "vertical",
232
+ "aria-label": "Drag to resize",
233
+ onPointerDown: drag(along === "top" ? "row-resize" : "col-resize", (event) => onResize({
234
+ x: event.clientX,
235
+ y: event.clientY
236
+ })),
237
+ className: cn("hover:after:bg-primary/40 absolute z-20 touch-none after:absolute after:transition-[background-color] after:duration-150", along === "top" ? "inset-x-0 -top-1 h-2 cursor-row-resize after:inset-x-0 after:top-1 after:h-0.5" : "inset-y-0 -left-1 w-2 cursor-col-resize after:inset-y-0 after:left-1 after:w-0.5")
238
+ });
239
+ }
240
+ /** A window over the page, moved by its title bar and resized by any corner. */
241
+ function Floating({ label, rect, onChange, onClose, children }) {
242
+ const width = clamp(rect.width, MIN_WIDTH, window.innerWidth - 16);
243
+ const height = clamp(rect.height, MIN_HEIGHT, window.innerHeight - 16);
244
+ const x = clamp(rect.x, 8, window.innerWidth - width - 8);
245
+ const y = clamp(rect.y, 8, window.innerHeight - height - 8);
246
+ return /* @__PURE__ */ jsxs("div", {
247
+ role: "dialog",
248
+ "aria-label": label,
249
+ className: "fixed z-40",
250
+ style: {
251
+ left: x,
252
+ top: y,
253
+ width,
254
+ height
255
+ },
256
+ children: [/* @__PURE__ */ jsxs("div", {
257
+ className: "bg-background flex h-full flex-col overflow-hidden rounded-xl border shadow-[0_1px_2px_rgb(0_0_0/0.06),0_16px_48px_rgb(0_0_0/0.2)]",
258
+ children: [/* @__PURE__ */ jsxs("div", {
259
+ onPointerDown: (start) => {
260
+ const from = {
261
+ x: start.clientX - x,
262
+ y: start.clientY - y
263
+ };
264
+ drag("grabbing", (event) => onChange({
265
+ ...rect,
266
+ width,
267
+ height,
268
+ x: event.clientX - from.x,
269
+ y: event.clientY - from.y
270
+ }))(start);
271
+ },
272
+ className: "text-muted-foreground relative flex h-8 shrink-0 cursor-grab items-center justify-center border-b text-xs font-medium select-none active:cursor-grabbing",
273
+ children: [label, /* @__PURE__ */ jsx("button", {
274
+ type: "button",
275
+ "aria-label": `Close ${label}`,
276
+ title: `Close ${label} (Ctrl /)`,
277
+ onPointerDown: (event) => event.stopPropagation(),
278
+ onClick: onClose,
279
+ className: "text-muted-foreground hover:text-foreground hover:bg-muted absolute right-1.5 z-30 grid size-6 cursor-default place-items-center rounded-md transition-[background-color,color,scale] duration-150 ease-out outline-none focus-visible:ring-2 active:scale-[0.92] [&_svg]:size-3.5",
280
+ children: /* @__PURE__ */ jsx(X, {})
281
+ })]
282
+ }), /* @__PURE__ */ jsx("div", {
283
+ className: "min-h-0 flex-1",
284
+ children
285
+ })]
286
+ }), CORNERS.map(({ name, left, top, cursor, place }) => /* @__PURE__ */ jsx("div", {
287
+ "aria-label": `Drag the ${name} corner to resize`,
288
+ onPointerDown: (start) => {
289
+ const right = x + width;
290
+ const bottom = y + height;
291
+ drag(cursor, (event) => {
292
+ const across = left ? right - clamp(event.clientX, 8, right - MIN_WIDTH) : clamp(event.clientX, x + MIN_WIDTH, window.innerWidth - 8) - x;
293
+ const down = top ? bottom - clamp(event.clientY, 8, bottom - MIN_HEIGHT) : clamp(event.clientY, y + MIN_HEIGHT, window.innerHeight - 8) - y;
294
+ onChange({
295
+ x: left ? right - across : x,
296
+ y: top ? bottom - down : y,
297
+ width: across,
298
+ height: down
299
+ });
300
+ })(start);
301
+ },
302
+ className: cn("absolute z-20 size-5 touch-none", place),
303
+ style: { cursor }
304
+ }, name))]
305
+ });
306
+ }
307
+ const MIN_WIDTH = 360;
308
+ const MIN_HEIGHT = 240;
309
+ /** The four corners a floating window is resized by. */
310
+ const CORNERS = [
311
+ {
312
+ name: "top left",
313
+ left: true,
314
+ top: true,
315
+ cursor: "nwse-resize",
316
+ place: "-top-1 -left-1"
317
+ },
318
+ {
319
+ name: "top right",
320
+ left: false,
321
+ top: true,
322
+ cursor: "nesw-resize",
323
+ place: "-top-1 -right-1"
324
+ },
325
+ {
326
+ name: "bottom left",
327
+ left: true,
328
+ top: false,
329
+ cursor: "nesw-resize",
330
+ place: "-bottom-1 -left-1"
331
+ },
332
+ {
333
+ name: "bottom right",
334
+ left: false,
335
+ top: false,
336
+ cursor: "nwse-resize",
337
+ place: "-right-1 -bottom-1"
338
+ }
339
+ ];
340
+ //#endregion
341
+ export { DevtoolsDock };
342
+
343
+ //# sourceMappingURL=devtools-dock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-dock.js","names":[],"sources":["../../../src/recipes/devtools-dock/devtools-dock.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type { CSSProperties, PointerEvent, ReactNode } from 'react';\nimport {\n AppWindow,\n Maximize2,\n Network,\n PanelBottom,\n PanelRight,\n X,\n} from 'lucide-react';\nimport {\n ContextMenu,\n ContextMenuContent,\n ContextMenuLabel,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuTrigger,\n} from '#components/ui/context-menu';\nimport { useIsMobile } from '#hooks/use-mobile';\nimport { cn } from '#lib/utils';\n\n/*\n * The Devtools Dock: a page with devtools beside it, opened from one button\n * that never moves.\n *\n * 1. Keep whether it is open, its Layout and each Layout's size, in\n * `localStorage`, so a reload finds it as it was left.\n * 2. Lay out the page fills what the devtools leave: a full-screen layer\n * over it, a bar under it or a side panel on its right, both\n * pushing it aside and resizable by their edge, or a floating\n * window, moved by its title and resized by any corner. A page\n * that scrolls the window, as a site does, is left in the\n * document's flow: the bar and the panel are fixed over it, and\n * the body is padded by their size so nothing hides under them.\n * 3. Button two icons, fixed in the bottom-right corner at one size\n * whatever is open or marked: open and close, and the Layout. A\n * click on the Layout steps to the next one; a right click or a\n * long press picks one from a menu. On a phone only full screen\n * and the bottom bar make sense, so only they are offered, and a\n * Layout kept from a wide screen shows as the nearest of them.\n * It sits in the row of the devtools' bottom bar, which are\n * handed the room it takes as `--dock-reserve`.\n * 4. Keys Ctrl+/ or ⌘/ opens and closes them.\n */\n\nexport type DockLayout = 'full' | 'bottom' | 'side' | 'floating';\n\ninterface Kept {\n readonly open: boolean;\n readonly layout: DockLayout;\n readonly bottom: number;\n readonly side: number;\n readonly floating: {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n };\n}\n\nconst INITIAL: Kept = {\n open: false,\n layout: 'full',\n bottom: 360,\n side: 480,\n floating: { x: 48, y: 48, width: 720, height: 480 },\n};\n\nconst LAYOUTS = [\n { value: 'full', label: 'Full screen', Icon: Maximize2, phone: true },\n { value: 'bottom', label: 'Bottom', Icon: PanelBottom, phone: true },\n { value: 'side', label: 'Right side', Icon: PanelRight, phone: false },\n { value: 'floating', label: 'Floating', Icon: AppWindow, phone: false },\n] as const;\n/** On a phone, what a wide screen's Layout shows as. */\nconst ON_PHONE: Record<DockLayout, DockLayout> = {\n full: 'full',\n bottom: 'bottom',\n side: 'bottom',\n floating: 'full',\n};\n\nconst MIN = 200;\nconst clamp = (value: number, low: number, high: number) =>\n Math.min(Math.max(value, low), Math.max(low, high));\n\nexport function DevtoolsDock({\n devtools,\n children,\n label = 'Devtools',\n status,\n storageKey = 'devtools-dock',\n scrolls = 'dock',\n}: {\n /** The devtools, drawn for the Layout they are in. */\n readonly devtools: (layout: DockLayout) => ReactNode;\n /** The page. */\n readonly children: ReactNode;\n readonly label?: string;\n /** A mark on the button's corner, such as a dot for a page showing the past; it never changes the button's size. */\n readonly status?: ReactNode;\n /** Where the Dock keeps how it was left. */\n readonly storageKey?: string;\n /**\n * What scrolls the page: `dock`, a box the height of the screen that the\n * page fills, for an app that scrolls its own parts; or `window`, for a\n * site whose page scrolls the window, which keeps its sticky headers and\n * its scroll restoration.\n */\n readonly scrolls?: 'dock' | 'window';\n}) {\n // 1. Keep\n const [kept, setKept] = useState(INITIAL);\n // Read after mount: the server renders it closed.\n useEffect(() => {\n try {\n const stored = localStorage.getItem(storageKey);\n if (stored) setKept({ ...INITIAL, ...JSON.parse(stored) });\n } catch {}\n }, [storageKey]);\n const keep = (change: Partial<Kept> | ((was: Kept) => Partial<Kept>)) =>\n setKept((was) => {\n const next = {\n ...was,\n ...(typeof change === 'function' ? change(was) : change),\n };\n try {\n localStorage.setItem(storageKey, JSON.stringify(next));\n } catch {}\n return next;\n });\n const phone = useIsMobile();\n const offered = LAYOUTS.filter((each) => !phone || each.phone);\n const { open } = kept;\n const layout = phone ? ON_PHONE[kept.layout] : kept.layout;\n const current = offered.find((each) => each.value === layout)!;\n const next = offered[(offered.indexOf(current) + 1) % offered.length]!;\n\n // 4. Keys\n const toggle = useRef(() => {});\n toggle.current = () => keep((was) => ({ open: !was.open }));\n useEffect(() => {\n const onKey = (event: KeyboardEvent) => {\n if (!((event.ctrlKey || event.metaKey) && event.key === '/')) return;\n event.preventDefault();\n toggle.current();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, []);\n\n // 3. Button: how much room it takes from the corner.\n const button = useRef<HTMLDivElement>(null);\n const [reserve, setReserve] = useState(0);\n useEffect(() => {\n const at = button.current;\n if (!at) return;\n const measure = () =>\n setReserve(window.innerWidth - at.getBoundingClientRect().left);\n const observer = new ResizeObserver(measure);\n observer.observe(at);\n window.addEventListener('resize', measure);\n return () => {\n observer.disconnect();\n window.removeEventListener('resize', measure);\n };\n }, []);\n const clear = { '--dock-reserve': `${reserve}px` } as CSSProperties;\n\n // 2. Lay out\n const docked = open && (layout === 'bottom' || layout === 'side');\n const size = layout === 'bottom' ? kept.bottom : kept.side;\n const inFlow = scrolls === 'window';\n // A page in the window's flow makes room for the bar or the panel over it.\n useEffect(() => {\n if (!inFlow || !docked) return;\n const side = layout === 'bottom' ? 'paddingBottom' : 'paddingRight';\n const body = document.body.style;\n const before = body[side];\n body[side] = `${size}px`;\n return () => {\n body[side] = before;\n };\n }, [inFlow, docked, layout, size]);\n\n const aside = docked && (\n <aside\n // A new Layout lays the devtools out afresh: the Map fits again.\n key={layout}\n aria-label={label}\n className={cn(\n 'bg-background flex shrink-0 flex-col',\n layout === 'bottom' ? 'border-t' : 'border-l',\n !inFlow\n ? 'relative'\n : layout === 'bottom'\n ? 'fixed inset-x-0 bottom-0 z-40'\n : 'fixed inset-y-0 right-0 z-40',\n )}\n style={{\n ...clear,\n [layout === 'bottom' ? 'height' : 'width']: size,\n }}\n >\n <Edge\n along={layout === 'bottom' ? 'top' : 'left'}\n onResize={(to) =>\n keep(\n layout === 'bottom'\n ? {\n bottom: clamp(\n window.innerHeight - to.y,\n MIN,\n window.innerHeight * 0.85,\n ),\n }\n : {\n side: clamp(\n window.innerWidth - to.x,\n MIN + 120,\n window.innerWidth * 0.85,\n ),\n },\n )\n }\n />\n <div className=\"min-h-0 flex-1\">{devtools(layout)}</div>\n </aside>\n );\n\n return (\n <>\n {inFlow ? (\n <>\n {children}\n {aside}\n </>\n ) : (\n <div\n className={cn(\n 'flex h-dvh w-full overflow-hidden',\n docked && layout === 'bottom' ? 'flex-col' : 'flex-row',\n )}\n >\n <div className=\"relative min-h-0 min-w-0 flex-1\">{children}</div>\n {aside}\n </div>\n )}\n\n {open && layout === 'full' && (\n <div\n role=\"dialog\"\n aria-label={label}\n className=\"fixed inset-0 z-40\"\n style={clear}\n >\n {devtools(layout)}\n </div>\n )}\n\n {open && layout === 'floating' && (\n <Floating\n label={label}\n rect={kept.floating}\n onChange={(floating) => keep({ floating })}\n onClose={() => keep({ open: false })}\n >\n {devtools(layout)}\n </Floating>\n )}\n\n {/* 3. Button: one size and place whatever is open, in the Timeline's row. */}\n <div\n ref={button}\n className={cn(\n 'bg-background fixed right-3 bottom-[9px] z-50 flex h-9 items-center rounded-md border transition-shadow duration-150',\n !open &&\n 'shadow-[0_1px_2px_rgb(0_0_0/0.04),0_4px_16px_-4px_rgb(0_0_0/0.12)]',\n )}\n >\n <button\n type=\"button\"\n aria-pressed={open}\n aria-label={`${open ? 'Close' : 'Open'} ${label}`}\n title={`${open ? 'Close' : 'Open'} ${label} (Ctrl /)`}\n onClick={() => keep({ open: !open })}\n className=\"text-muted-foreground hover:text-foreground aria-pressed:text-foreground aria-pressed:bg-muted relative grid h-full w-9 place-items-center rounded-l-[5px] transition-colors duration-150 outline-offset-2 focus-visible:outline-2 [&_svg]:size-4\"\n >\n <Network />\n {status && (\n <span className=\"pointer-events-none absolute top-1.5 right-1.5\">\n {status}\n </span>\n )}\n </button>\n <ContextMenu>\n <ContextMenuTrigger\n render={\n <button\n type=\"button\"\n aria-label={`Devtools layout: ${current.label}`}\n title={`${current.label}. Click for ${next.label.toLowerCase()}, right click to pick`}\n onClick={() => keep({ layout: next.value, open: true })}\n className=\"text-muted-foreground hover:text-foreground grid h-full w-9 place-items-center rounded-r-[5px] border-l transition-colors duration-150 outline-offset-2 focus-visible:outline-2 [&_svg]:size-4\"\n />\n }\n >\n <current.Icon />\n </ContextMenuTrigger>\n <ContextMenuContent className=\"w-44\">\n <ContextMenuRadioGroup\n value={layout}\n onValueChange={(value) =>\n keep({ layout: value as DockLayout, open: true })\n }\n >\n <ContextMenuLabel>Layout</ContextMenuLabel>\n {offered.map(({ value, label: name, Icon }) => (\n <ContextMenuRadioItem key={value} value={value} closeOnClick>\n <Icon />\n {name}\n </ContextMenuRadioItem>\n ))}\n </ContextMenuRadioGroup>\n </ContextMenuContent>\n </ContextMenu>\n </div>\n </>\n );\n}\n\n/** Drag a pointer and follow it; the page keeps the cursor and selects nothing meanwhile. */\nconst drag =\n (cursor: string, onMove: (event: globalThis.PointerEvent) => void) =>\n (event: PointerEvent<HTMLElement>) => {\n if (event.button !== 0) return;\n event.preventDefault();\n const target = event.currentTarget;\n target.setPointerCapture(event.pointerId);\n document.body.style.cursor = cursor;\n document.body.style.userSelect = 'none';\n const move = (each: globalThis.PointerEvent) => onMove(each);\n const up = () => {\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', up);\n };\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', up);\n };\n\n/** The edge a docked panel is resized by. */\nfunction Edge({\n along,\n onResize,\n}: {\n readonly along: 'top' | 'left';\n readonly onResize: (to: { x: number; y: number }) => void;\n}) {\n const cursor = along === 'top' ? 'row-resize' : 'col-resize';\n return (\n <div\n role=\"separator\"\n aria-orientation={along === 'top' ? 'horizontal' : 'vertical'}\n aria-label=\"Drag to resize\"\n onPointerDown={drag(cursor, (event) =>\n onResize({ x: event.clientX, y: event.clientY }),\n )}\n className={cn(\n 'hover:after:bg-primary/40 absolute z-20 touch-none after:absolute after:transition-[background-color] after:duration-150',\n along === 'top'\n ? 'inset-x-0 -top-1 h-2 cursor-row-resize after:inset-x-0 after:top-1 after:h-0.5'\n : 'inset-y-0 -left-1 w-2 cursor-col-resize after:inset-y-0 after:left-1 after:w-0.5',\n )}\n />\n );\n}\n\n/** A window over the page, moved by its title bar and resized by any corner. */\nfunction Floating({\n label,\n rect,\n onChange,\n onClose,\n children,\n}: {\n readonly label: string;\n readonly rect: Kept['floating'];\n readonly onChange: (rect: Kept['floating']) => void;\n readonly onClose: () => void;\n readonly children: ReactNode;\n}) {\n const width = clamp(rect.width, MIN_WIDTH, window.innerWidth - 16);\n const height = clamp(rect.height, MIN_HEIGHT, window.innerHeight - 16);\n const x = clamp(rect.x, 8, window.innerWidth - width - 8);\n const y = clamp(rect.y, 8, window.innerHeight - height - 8);\n return (\n <div\n role=\"dialog\"\n aria-label={label}\n className=\"fixed z-40\"\n style={{ left: x, top: y, width, height }}\n >\n {/* Rounded and clipped inside; the corners sit outside the clip, so all of each stays grabbable. */}\n <div className=\"bg-background flex h-full flex-col overflow-hidden rounded-xl border shadow-[0_1px_2px_rgb(0_0_0/0.06),0_16px_48px_rgb(0_0_0/0.2)]\">\n <div\n onPointerDown={(start) => {\n const from = { x: start.clientX - x, y: start.clientY - y };\n drag('grabbing', (event) =>\n onChange({\n ...rect,\n width,\n height,\n x: event.clientX - from.x,\n y: event.clientY - from.y,\n }),\n )(start);\n }}\n className=\"text-muted-foreground relative flex h-8 shrink-0 cursor-grab items-center justify-center border-b text-xs font-medium select-none active:cursor-grabbing\"\n >\n {label}\n <button\n type=\"button\"\n aria-label={`Close ${label}`}\n title={`Close ${label} (Ctrl /)`}\n // A press on it closes the window; it never starts a move.\n onPointerDown={(event) => event.stopPropagation()}\n onClick={onClose}\n className=\"text-muted-foreground hover:text-foreground hover:bg-muted absolute right-1.5 z-30 grid size-6 cursor-default place-items-center rounded-md transition-[background-color,color,scale] duration-150 ease-out outline-none focus-visible:ring-2 active:scale-[0.92] [&_svg]:size-3.5\"\n >\n <X />\n </button>\n </div>\n <div className=\"min-h-0 flex-1\">{children}</div>\n </div>\n {CORNERS.map(({ name, left, top, cursor, place }) => (\n <div\n key={name}\n aria-label={`Drag the ${name} corner to resize`}\n onPointerDown={(start) => {\n // The corner across from this one stays where it is.\n const right = x + width;\n const bottom = y + height;\n drag(cursor, (event) => {\n const across = left\n ? right - clamp(event.clientX, 8, right - MIN_WIDTH)\n : clamp(event.clientX, x + MIN_WIDTH, window.innerWidth - 8) -\n x;\n const down = top\n ? bottom - clamp(event.clientY, 8, bottom - MIN_HEIGHT)\n : clamp(event.clientY, y + MIN_HEIGHT, window.innerHeight - 8) -\n y;\n onChange({\n x: left ? right - across : x,\n y: top ? bottom - down : y,\n width: across,\n height: down,\n });\n })(start);\n }}\n className={cn('absolute z-20 size-5 touch-none', place)}\n style={{ cursor }}\n />\n ))}\n </div>\n );\n}\n\nconst MIN_WIDTH = 360;\nconst MIN_HEIGHT = 240;\n\n/** The four corners a floating window is resized by. */\nconst CORNERS = [\n {\n name: 'top left',\n left: true,\n top: true,\n cursor: 'nwse-resize',\n place: '-top-1 -left-1',\n },\n {\n name: 'top right',\n left: false,\n top: true,\n cursor: 'nesw-resize',\n place: '-top-1 -right-1',\n },\n {\n name: 'bottom left',\n left: true,\n top: false,\n cursor: 'nesw-resize',\n place: '-bottom-1 -left-1',\n },\n {\n name: 'bottom right',\n left: false,\n top: false,\n cursor: 'nwse-resize',\n place: '-right-1 -bottom-1',\n },\n] as const;\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { DevtoolsDock, DockLayout } from "./devtools-dock.js";
2
+ export { DevtoolsDock, type DockLayout };
@@ -0,0 +1,2 @@
1
+ import { DevtoolsDock } from "./devtools-dock.js";
2
+ export { DevtoolsDock };
@@ -1 +1 @@
1
- {"version":3,"file":"kabuki-devtools.d.ts","names":[],"sources":["../../../src/recipes/kabuki-devtools/kabuki-devtools.tsx"],"mappings":";;;;;;;;;KA0CK,WAAW,YAAY,kBAAkB;wBAe9B,iBACd,SACA,QACA,QACA;;WAGS,SAAS;;WAET,SAAS;;WAET;;WAEA;oBACV,IAAA;wBAqEe,YACd,YACA,MACA,UACA,QACA,UACA,QACA;;WAGS,YAAY;;WAEZ;aAAiB,UAAU;aAAmB,OAAO;;;WAErD,WAAW;;WAEX,SAAS;WACT,UAAU,sBAAsB;;WAEhC;;WAEA;oBACV,IAAA"}
1
+ {"version":3,"file":"kabuki-devtools.d.ts","names":[],"sources":["../../../src/recipes/kabuki-devtools/kabuki-devtools.tsx"],"mappings":";;;;;;;;;KA0CK,WAAW,YAAY,kBAAkB;wBAgB9B,iBACd,SACA,QACA,QACA;;WAGS,SAAS;;WAET,SAAS;;WAET;;WAEA;oBACV,IAAA;wBAwEe,YACd,YACA,MACA,UACA,QACA,UACA,QACA;;WAGS,YAAY;;WAEZ;aAAiB,UAAU;aAAmB,OAAO;;;WAErD,WAAW;;WAEX,SAAS;WACT,UAAU,sBAAsB;;WAEhC;;WAEA;oBACV,IAAA"}
@@ -49,7 +49,10 @@ function KabukiDevtools({ runtime, layout, dotted, className }) {
49
49
  shown: at,
50
50
  live,
51
51
  onShow: (index) => runtime.show(index === 0 ? "init" : steps[index - 1].id),
52
- onLive: () => runtime.show(null)
52
+ onLive: () => runtime.show(null),
53
+ head: steps === branch,
54
+ window: history.window(),
55
+ onWindow: history.keep
53
56
  }
54
57
  });
55
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"kabuki-devtools.js","names":[],"sources":["../../../src/recipes/kabuki-devtools/kabuki-devtools.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport type { ComponentProps } from 'react';\nimport { AnimatePresence } from 'motion/react';\nimport { handle, isRestore } from '@kestack/kabuki';\nimport type {\n ActorDefinition,\n Entry,\n Instance,\n Snapshot,\n} from '@kestack/kabuki';\nimport type { AppRuntime } from '@kestack/kabuki/react';\nimport { cn } from '#lib/utils';\nimport { INK } from './ink/index.ts';\nimport type { Tone } from './ink/index.ts';\nimport { AppMap } from './map/index.ts';\nimport type { Pick } from './map/index.ts';\nimport { sceneOf } from './scene/index.ts';\nimport type {\n Kind,\n Placed,\n Said,\n Scene,\n WorkerKey,\n WorkerStatus,\n} from './scene/index.ts';\nimport { Sheet } from './sheet/index.ts';\nimport { Timeline } from './timeline/index.ts';\n\n/*\n * The Inspector: the Step shown, drawn as a Map, with the Sheet and the\n * Timeline under it. It fills whatever holds it, and lays itself out by its\n * own width, not the screen's.\n *\n * 1. Scene the Map of the Step shown, from the Actors and its Snapshot,\n * with the live app's Workers laid over it when it is live.\n * 2. Pick a card, a State or Worker on it, one of its Instances, or the\n * Step's Message; a pick gone from the Step shown closes the\n * Sheet.\n * 3. Sheet what is picked, taken apart.\n * 4. Timeline which Step is shown, and whether it is live.\n */\n\ntype Statuses = NonNullable<Parameters<typeof sceneOf>[0]['statuses']>;\n\n/*\n * The Kabuki devtools: the Inspector on a running app, from its\n * runtime (`App.useRuntime()`). It fills whatever holds it.\n *\n * 1. Steps init, then every entry on the Branch shown: the Head's, or\n * the one a Step shown from another Branch is on.\n * 2. Step the Step shown is the app's own: picking one on the Timeline\n * Time Travels the app, and going live comes back to the Head.\n * Its Snapshot is replayed from the History; the Commands its\n * Message asked for are worked out again from the Step before,\n * once each.\n * 3. Workers live and running, what each Instance's Workers are doing.\n */\nexport function KabukiDevtools({\n runtime,\n layout,\n dotted,\n className,\n}: {\n /** The running app: `App.useRuntime()`. */\n readonly runtime: AppRuntime;\n /** How the Map is laid out: a packed tree, or an outline. */\n readonly layout?: Kind;\n /** Dots under the Map. On by default; off, a background given by `className` shows through. */\n readonly dotted?: boolean;\n /** Classes for its root, merged over its own: a see-through background over the app. */\n readonly className?: string;\n}) {\n const { history, branch, shown, running } = runtime;\n const definition = history.actor.definition;\n\n // 1. Steps\n const steps =\n typeof shown === 'number' && !branch.some((entry) => entry.id === shown)\n ? history.branch(shown)\n : branch;\n const live = shown === null;\n const at =\n shown === null\n ? steps.length\n : shown === 'init'\n ? 0\n : steps.findIndex((entry) => entry.id === shown) + 1;\n const entry = at === 0 ? undefined : steps[at - 1];\n\n // 2. Step\n // By the entry itself, not its id: ids start again in another runtime, or\n // once the History is cleared.\n const asked = useRef(new WeakMap<Entry, ReadonlyArray<unknown>>());\n const commandsOf = (of: Entry) => {\n const known = asked.current.get(of);\n if (known) return known;\n const commands = isRestore(of.message)\n ? []\n : handle(definition, history.snapshotAt(of.parent), of).commands;\n asked.current.set(of, commands);\n return commands;\n };\n const step = useMemo(\n () => ({\n snapshot: history.snapshotAt(entry?.id ?? null),\n ...(entry\n ? { said: { envelope: entry, commands: commandsOf(entry) } }\n : {}),\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- an entry never changes once written.\n [history, entry],\n );\n\n return (\n <Inspector\n definition={definition}\n step={step}\n {...(layout ? { layout } : {})}\n {...(className ? { className } : {})}\n {...(dotted === undefined ? {} : { dotted })}\n // 3. Workers\n {...(live && running ? { statuses: runtime.workers } : {})}\n timeline={{\n steps: [\n { id: 'init', label: 'init' },\n ...steps.map((each) => ({\n id: `${each.id}`,\n label: each.message._tag,\n })),\n ],\n shown: at,\n live,\n onShow: (index) =>\n runtime.show(index === 0 ? 'init' : steps[index - 1]!.id),\n onLive: () => runtime.show(null),\n }}\n />\n );\n}\n\nexport function Inspector({\n definition,\n step,\n statuses,\n layout,\n timeline,\n dotted,\n className,\n}: {\n /** The root Actor's definition. */\n readonly definition: ActorDefinition;\n /** The Step shown: its Snapshot, and the Message that made it; none for init. */\n readonly step: { readonly snapshot: Snapshot; readonly said?: Said };\n /** What the live app says of each Instance's Workers, while the Step shown is live. */\n readonly statuses?: Statuses;\n /** How the Map is laid out: a packed tree, or an outline. */\n readonly layout?: Kind;\n readonly timeline: ComponentProps<typeof Timeline>;\n /** Dots under the Map. On by default. */\n readonly dotted?: boolean;\n /** Classes for its root, merged over its own. */\n readonly className?: string;\n}) {\n const [picked, setPicked] = useState<Pick | undefined>(undefined);\n const [chosen, setChosen] = useState<ReadonlyMap<string, string>>(\n () => new Map(),\n );\n const [folded, setFolded] = useState<ReadonlySet<string>>(() => new Set());\n\n // 1. Scene\n const scene = useMemo(\n () =>\n sceneOf({\n definition,\n snapshot: step.snapshot,\n ...(step.said ? { said: step.said } : {}),\n ...(statuses ? { statuses } : {}),\n ...(layout ? { layout } : {}),\n chosen,\n folded,\n }),\n [definition, step, statuses, layout, chosen, folded],\n );\n\n // 2. Pick and 3. Sheet\n const sheet = picked && sheetOf(scene, picked);\n\n return (\n <div\n className={cn(\n '@container bg-background text-foreground relative flex h-full w-full flex-col overflow-clip',\n className,\n )}\n style={INK}\n >\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\n <div className=\"relative min-h-0 flex-1\">\n <AppMap\n scene={scene}\n {...(dotted === undefined ? {} : { dotted })}\n picked={sheet ? picked : undefined}\n onPick={setPicked}\n onChoose={(place, instance) =>\n setChosen((was) => new Map(was).set(place, instance))\n }\n onFold={(place) =>\n setFolded((was) => {\n const next = new Set(was);\n if (!next.delete(place)) next.add(place);\n return next;\n })\n }\n />\n </div>\n <AnimatePresence>\n {sheet && (\n <Sheet\n key=\"sheet\"\n {...sheet}\n onClose={() => setPicked(undefined)}\n />\n )}\n </AnimatePresence>\n </div>\n {/* 4. Timeline */}\n <Timeline {...timeline} />\n </div>\n );\n}\n\ntype SheetOf = Omit<ComponentProps<typeof Sheet>, 'onClose'>;\n\nconst dataOf = ({ _tag, ...data }: { readonly _tag: string }) => data;\nconst hasFields = (value: object) => Object.keys(value).length > 0;\nconst isEmpty = (value: unknown) =>\n value !== null && typeof value === 'object' && !hasFields(value);\n\nconst TONE: Record<WorkerStatus, Tone> = {\n running: 'running',\n finished: 'dim',\n failed: 'failed',\n on: 'lit',\n};\n\nconst keyName = (name: string, key: WorkerKey) =>\n key.key === '' ? name : `${name}#${key.key}`;\n\n/** Who Sent the Message, in words. */\nconst fromOf = ({ source, send }: NonNullable<Scene['beats']>) => {\n switch (source.kind) {\n case 'view':\n return 'its View';\n case 'command':\n return `Command ${send?.by ?? source.name}`;\n case 'worker':\n return `Worker ${send?.by ?? source.name}`;\n case 'provide':\n return 'what it Provides';\n case 'outside':\n return 'outside the app';\n case 'restore':\n return 'a Restore';\n }\n};\n\n/** What the Sheet shows of a pick, or nothing if it is not in this Step. */\nconst sheetOf = (scene: Scene, pick: Pick): SheetOf | undefined => {\n if ('message' in pick) {\n const beats = scene.beats;\n if (!beats) return undefined;\n return {\n kind: 'Message',\n tone: 'message',\n title: beats.tag,\n facts: [\n {\n label: 'To',\n value: beats.to ? scene.at.get(beats.to)!.name : 'dropped',\n },\n { label: 'From', value: fromOf(beats) },\n { label: 'Time', value: `${(beats.at / 1000).toFixed(2)}s` },\n ],\n sections: [\n { title: 'Payload', value: beats.payload },\n ...(beats.commands.length\n ? [{ title: 'Commands asked for', value: beats.commands }]\n : []),\n ],\n };\n }\n\n const node = scene.at.get(pick.node);\n if (!node) return undefined;\n const { part } = pick;\n if (part && 'worker' in part)\n return workerSheet(node, part.worker, scene.live);\n if (part && 'state' in part) return stateSheet(node, part.state);\n\n const one =\n pick.instance !== undefined\n ? node.instances.find((each) => each.id === pick.instance)\n : !node.many\n ? node.instances[0]\n : undefined;\n if (one) return instanceSheet(node.name, one);\n if (node.instances.length === 0)\n return {\n kind: 'Actor',\n tone: 'dim',\n title: node.name,\n facts: [{ label: 'Running', value: 'no Instance here at this Step' }],\n };\n return {\n kind: 'Actor',\n title: node.name,\n facts: [\n { label: 'Instances', value: node.instances.length },\n ...(node.states\n ? [\n {\n label: 'In each State',\n value: node.states\n .flat()\n .map((option) => `${option.tag} ${option.count}`)\n .join(' · '),\n },\n ]\n : []),\n ],\n };\n};\n\n/** One Instance taken apart. */\nconst instanceSheet = (name: string, instance: Instance): SheetOf => {\n const state = dataOf(instance.state);\n return {\n kind: 'Instance',\n title: instance.key === undefined ? name : `${name}#${instance.key}`,\n facts: [\n { label: 'ID', value: instance.id },\n ...(instance.state._tag === 'Single'\n ? []\n : [{ label: 'State', value: instance.state._tag }]),\n ...(instance.key === undefined\n ? []\n : [{ label: 'Key', value: instance.key }]),\n ],\n sections: [\n ...(isEmpty(instance.model)\n ? []\n : [{ title: 'Model', value: instance.model }]),\n ...(hasFields(state)\n ? [{ title: `State ${instance.state._tag}`, value: state }]\n : []),\n ...(instance.props === undefined\n ? []\n : [{ title: 'Props', value: instance.props }]),\n ],\n };\n};\n\n/** One State of a card: which Instances are in it, and its data if one is. */\nconst stateSheet = (node: Placed, tag: string): SheetOf => {\n const inIt = node.instances.filter((each) => each.state._tag === tag);\n const data = !node.many && inIt[0] ? dataOf(inIt[0].state) : {};\n return {\n kind: 'State',\n tone: inIt.length ? 'active' : 'dim',\n title: tag,\n facts: [\n { label: 'Of', value: node.name },\n node.many\n ? { label: 'Instances in it', value: inIt.length }\n : {\n label: 'Active',\n value: inIt.length\n ? 'yes'\n : node.instances[0]\n ? `no, it is in ${node.instances[0].state._tag}`\n : 'not running at this Step',\n },\n ],\n sections: hasFields(data) ? [{ title: 'Data', value: data }] : [],\n };\n};\n\n/** One Worker of a card: where it is written, and each key it runs now. */\nconst workerSheet = (node: Placed, name: string, live: boolean): SheetOf => {\n const row = node.workers.find((each) => each.name === name);\n const keys = row?.keys ?? [];\n return {\n kind: 'Worker',\n tone: keys.some((each) => each.status === 'failed')\n ? 'failed'\n : keys.length\n ? 'running'\n : 'dim',\n title: name,\n facts: [\n { label: 'Of', value: node.name },\n {\n label: 'Written',\n value: row?.state ? `in ${row.state}` : 'at its top',\n },\n { label: 'Keys on', value: keys.length || 'none' },\n ],\n rows: {\n title: live ? 'Keys, live' : 'Keys on at this Step',\n items: keys.map((key) => ({\n label:\n key.instance.key === undefined\n ? keyName(name, key)\n : `${keyName(name, key)} · ${key.instance.key}`,\n detail: key.status,\n tone: TONE[key.status],\n })),\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"kabuki-devtools.js","names":[],"sources":["../../../src/recipes/kabuki-devtools/kabuki-devtools.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport type { ComponentProps } from 'react';\nimport { AnimatePresence } from 'motion/react';\nimport { handle, isRestore } from '@kestack/kabuki';\nimport type {\n ActorDefinition,\n Entry,\n Instance,\n Snapshot,\n} from '@kestack/kabuki';\nimport type { AppRuntime } from '@kestack/kabuki/react';\nimport { cn } from '#lib/utils';\nimport { INK } from './ink/index.ts';\nimport type { Tone } from './ink/index.ts';\nimport { AppMap } from './map/index.ts';\nimport type { Pick } from './map/index.ts';\nimport { sceneOf } from './scene/index.ts';\nimport type {\n Kind,\n Placed,\n Said,\n Scene,\n WorkerKey,\n WorkerStatus,\n} from './scene/index.ts';\nimport { Sheet } from './sheet/index.ts';\nimport { Timeline } from './timeline/index.ts';\n\n/*\n * The Inspector: the Step shown, drawn as a Map, with the Sheet and the\n * Timeline under it. It fills whatever holds it, and lays itself out by its\n * own width, not the screen's.\n *\n * 1. Scene the Map of the Step shown, from the Actors and its Snapshot,\n * with the live app's Workers laid over it when it is live.\n * 2. Pick a card, a State or Worker on it, one of its Instances, or the\n * Step's Message; a pick gone from the Step shown closes the\n * Sheet.\n * 3. Sheet what is picked, taken apart.\n * 4. Timeline which Step is shown, and whether it is live.\n */\n\ntype Statuses = NonNullable<Parameters<typeof sceneOf>[0]['statuses']>;\n\n/*\n * The Kabuki devtools: the Inspector on a running app, from its\n * runtime (`App.useRuntime()`). It fills whatever holds it.\n *\n * 1. Steps init, then every entry on the Branch shown: the Head's, or\n * the one a Step shown from another Branch is on.\n * 2. Step the Step shown is the app's own: picking one on the Timeline\n * Time Travels the app, and going live comes back to the Head.\n * Its Snapshot is replayed from the History; the Commands its\n * Message asked for are worked out again from the Step before,\n * once each.\n * 3. Workers live and running, what each Instance's Workers are doing.\n * 4. Window how many entries the History keeps, changed at any time.\n */\nexport function KabukiDevtools({\n runtime,\n layout,\n dotted,\n className,\n}: {\n /** The running app: `App.useRuntime()`. */\n readonly runtime: AppRuntime;\n /** How the Map is laid out: a packed tree, or an outline. */\n readonly layout?: Kind;\n /** Dots under the Map. On by default; off, a background given by `className` shows through. */\n readonly dotted?: boolean;\n /** Classes for its root, merged over its own: a see-through background over the app. */\n readonly className?: string;\n}) {\n const { history, branch, shown, running } = runtime;\n const definition = history.actor.definition;\n\n // 1. Steps\n const steps =\n typeof shown === 'number' && !branch.some((entry) => entry.id === shown)\n ? history.branch(shown)\n : branch;\n const live = shown === null;\n const at =\n shown === null\n ? steps.length\n : shown === 'init'\n ? 0\n : steps.findIndex((entry) => entry.id === shown) + 1;\n const entry = at === 0 ? undefined : steps[at - 1];\n\n // 2. Step\n // By the entry itself, not its id: ids start again in another runtime, or\n // once the History is cleared.\n const asked = useRef(new WeakMap<Entry, ReadonlyArray<unknown>>());\n const commandsOf = (of: Entry) => {\n const known = asked.current.get(of);\n if (known) return known;\n const commands = isRestore(of.message)\n ? []\n : handle(definition, history.snapshotAt(of.parent), of).commands;\n asked.current.set(of, commands);\n return commands;\n };\n const step = useMemo(\n () => ({\n snapshot: history.snapshotAt(entry?.id ?? null),\n ...(entry\n ? { said: { envelope: entry, commands: commandsOf(entry) } }\n : {}),\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- an entry never changes once written.\n [history, entry],\n );\n\n return (\n <Inspector\n definition={definition}\n step={step}\n {...(layout ? { layout } : {})}\n {...(className ? { className } : {})}\n {...(dotted === undefined ? {} : { dotted })}\n // 3. Workers\n {...(live && running ? { statuses: runtime.workers } : {})}\n timeline={{\n steps: [\n { id: 'init', label: 'init' },\n ...steps.map((each) => ({\n id: `${each.id}`,\n label: each.message._tag,\n })),\n ],\n shown: at,\n live,\n onShow: (index) =>\n runtime.show(index === 0 ? 'init' : steps[index - 1]!.id),\n onLive: () => runtime.show(null),\n head: steps === branch,\n window: history.window(),\n onWindow: history.keep,\n }}\n />\n );\n}\n\nexport function Inspector({\n definition,\n step,\n statuses,\n layout,\n timeline,\n dotted,\n className,\n}: {\n /** The root Actor's definition. */\n readonly definition: ActorDefinition;\n /** The Step shown: its Snapshot, and the Message that made it; none for init. */\n readonly step: { readonly snapshot: Snapshot; readonly said?: Said };\n /** What the live app says of each Instance's Workers, while the Step shown is live. */\n readonly statuses?: Statuses;\n /** How the Map is laid out: a packed tree, or an outline. */\n readonly layout?: Kind;\n readonly timeline: ComponentProps<typeof Timeline>;\n /** Dots under the Map. On by default. */\n readonly dotted?: boolean;\n /** Classes for its root, merged over its own. */\n readonly className?: string;\n}) {\n const [picked, setPicked] = useState<Pick | undefined>(undefined);\n const [chosen, setChosen] = useState<ReadonlyMap<string, string>>(\n () => new Map(),\n );\n const [folded, setFolded] = useState<ReadonlySet<string>>(() => new Set());\n\n // 1. Scene\n const scene = useMemo(\n () =>\n sceneOf({\n definition,\n snapshot: step.snapshot,\n ...(step.said ? { said: step.said } : {}),\n ...(statuses ? { statuses } : {}),\n ...(layout ? { layout } : {}),\n chosen,\n folded,\n }),\n [definition, step, statuses, layout, chosen, folded],\n );\n\n // 2. Pick and 3. Sheet\n const sheet = picked && sheetOf(scene, picked);\n\n return (\n <div\n className={cn(\n '@container bg-background text-foreground relative flex h-full w-full flex-col overflow-clip',\n className,\n )}\n style={INK}\n >\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\n <div className=\"relative min-h-0 flex-1\">\n <AppMap\n scene={scene}\n {...(dotted === undefined ? {} : { dotted })}\n picked={sheet ? picked : undefined}\n onPick={setPicked}\n onChoose={(place, instance) =>\n setChosen((was) => new Map(was).set(place, instance))\n }\n onFold={(place) =>\n setFolded((was) => {\n const next = new Set(was);\n if (!next.delete(place)) next.add(place);\n return next;\n })\n }\n />\n </div>\n <AnimatePresence>\n {sheet && (\n <Sheet\n key=\"sheet\"\n {...sheet}\n onClose={() => setPicked(undefined)}\n />\n )}\n </AnimatePresence>\n </div>\n {/* 4. Timeline */}\n <Timeline {...timeline} />\n </div>\n );\n}\n\ntype SheetOf = Omit<ComponentProps<typeof Sheet>, 'onClose'>;\n\nconst dataOf = ({ _tag, ...data }: { readonly _tag: string }) => data;\nconst hasFields = (value: object) => Object.keys(value).length > 0;\nconst isEmpty = (value: unknown) =>\n value !== null && typeof value === 'object' && !hasFields(value);\n\nconst TONE: Record<WorkerStatus, Tone> = {\n running: 'running',\n finished: 'dim',\n failed: 'failed',\n on: 'lit',\n};\n\nconst keyName = (name: string, key: WorkerKey) =>\n key.key === '' ? name : `${name}#${key.key}`;\n\n/** Who Sent the Message, in words. */\nconst fromOf = ({ source, send }: NonNullable<Scene['beats']>) => {\n switch (source.kind) {\n case 'view':\n return 'its View';\n case 'command':\n return `Command ${send?.by ?? source.name}`;\n case 'worker':\n return `Worker ${send?.by ?? source.name}`;\n case 'provide':\n return 'what it Provides';\n case 'outside':\n return 'outside the app';\n case 'restore':\n return 'a Restore';\n }\n};\n\n/** What the Sheet shows of a pick, or nothing if it is not in this Step. */\nconst sheetOf = (scene: Scene, pick: Pick): SheetOf | undefined => {\n if ('message' in pick) {\n const beats = scene.beats;\n if (!beats) return undefined;\n return {\n kind: 'Message',\n tone: 'message',\n title: beats.tag,\n facts: [\n {\n label: 'To',\n value: beats.to ? scene.at.get(beats.to)!.name : 'dropped',\n },\n { label: 'From', value: fromOf(beats) },\n { label: 'Time', value: `${(beats.at / 1000).toFixed(2)}s` },\n ],\n sections: [\n { title: 'Payload', value: beats.payload },\n ...(beats.commands.length\n ? [{ title: 'Commands asked for', value: beats.commands }]\n : []),\n ],\n };\n }\n\n const node = scene.at.get(pick.node);\n if (!node) return undefined;\n const { part } = pick;\n if (part && 'worker' in part)\n return workerSheet(node, part.worker, scene.live);\n if (part && 'state' in part) return stateSheet(node, part.state);\n\n const one =\n pick.instance !== undefined\n ? node.instances.find((each) => each.id === pick.instance)\n : !node.many\n ? node.instances[0]\n : undefined;\n if (one) return instanceSheet(node.name, one);\n if (node.instances.length === 0)\n return {\n kind: 'Actor',\n tone: 'dim',\n title: node.name,\n facts: [{ label: 'Running', value: 'no Instance here at this Step' }],\n };\n return {\n kind: 'Actor',\n title: node.name,\n facts: [\n { label: 'Instances', value: node.instances.length },\n ...(node.states\n ? [\n {\n label: 'In each State',\n value: node.states\n .flat()\n .map((option) => `${option.tag} ${option.count}`)\n .join(' · '),\n },\n ]\n : []),\n ],\n };\n};\n\n/** One Instance taken apart. */\nconst instanceSheet = (name: string, instance: Instance): SheetOf => {\n const state = dataOf(instance.state);\n return {\n kind: 'Instance',\n title: instance.key === undefined ? name : `${name}#${instance.key}`,\n facts: [\n { label: 'ID', value: instance.id },\n ...(instance.state._tag === 'Single'\n ? []\n : [{ label: 'State', value: instance.state._tag }]),\n ...(instance.key === undefined\n ? []\n : [{ label: 'Key', value: instance.key }]),\n ],\n sections: [\n ...(isEmpty(instance.model)\n ? []\n : [{ title: 'Model', value: instance.model }]),\n ...(hasFields(state)\n ? [{ title: `State ${instance.state._tag}`, value: state }]\n : []),\n ...(instance.props === undefined\n ? []\n : [{ title: 'Props', value: instance.props }]),\n ],\n };\n};\n\n/** One State of a card: which Instances are in it, and its data if one is. */\nconst stateSheet = (node: Placed, tag: string): SheetOf => {\n const inIt = node.instances.filter((each) => each.state._tag === tag);\n const data = !node.many && inIt[0] ? dataOf(inIt[0].state) : {};\n return {\n kind: 'State',\n tone: inIt.length ? 'active' : 'dim',\n title: tag,\n facts: [\n { label: 'Of', value: node.name },\n node.many\n ? { label: 'Instances in it', value: inIt.length }\n : {\n label: 'Active',\n value: inIt.length\n ? 'yes'\n : node.instances[0]\n ? `no, it is in ${node.instances[0].state._tag}`\n : 'not running at this Step',\n },\n ],\n sections: hasFields(data) ? [{ title: 'Data', value: data }] : [],\n };\n};\n\n/** One Worker of a card: where it is written, and each key it runs now. */\nconst workerSheet = (node: Placed, name: string, live: boolean): SheetOf => {\n const row = node.workers.find((each) => each.name === name);\n const keys = row?.keys ?? [];\n return {\n kind: 'Worker',\n tone: keys.some((each) => each.status === 'failed')\n ? 'failed'\n : keys.length\n ? 'running'\n : 'dim',\n title: name,\n facts: [\n { label: 'Of', value: node.name },\n {\n label: 'Written',\n value: row?.state ? `in ${row.state}` : 'at its top',\n },\n { label: 'Keys on', value: keys.length || 'none' },\n ],\n rows: {\n title: live ? 'Keys, live' : 'Keys on at this Step',\n items: keys.map((key) => ({\n label:\n key.instance.key === undefined\n ? keyName(name, key)\n : `${keyName(name, key)} · ${key.instance.key}`,\n detail: key.status,\n tone: TONE[key.status],\n })),\n },\n };\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","names":[],"sources":["../../../../../src/recipes/kabuki-devtools/map/canvas/canvas.tsx"],"mappings":";;iBA6BiB;WACN;WACA;WACA;WACA;;;qBAaE;WAAQ;;;qBAKR;WAAS;;wBAON,SACd,QACA,OACA,OACA,QACA;;WAGS,QAAQ;WACR;;WAEA;;WAEA;;WAEA,UAAU;oBACpB,IAAA"}
1
+ {"version":3,"file":"canvas.d.ts","names":[],"sources":["../../../../../src/recipes/kabuki-devtools/map/canvas/canvas.tsx"],"mappings":";;iBA8BiB;WACN;WACA;WACA;WACA;;;qBAaE;WAAQ;;;qBAKR;WAAS;;wBAON,SACd,QACA,OACA,OACA,QACA;;WAGS,QAAQ;WACR;;WAEA;;WAEA;;WAEA,UAAU;oBACpB,IAAA"}
@@ -56,6 +56,19 @@ function Canvas({ bounds, label, onTap, dotted = true, children }) {
56
56
  y,
57
57
  scale
58
58
  ]);
59
+ useEffect(() => {
60
+ const element = surface.current;
61
+ let width = element.clientWidth;
62
+ let height = element.clientHeight;
63
+ const observer = new ResizeObserver(() => {
64
+ x.set(x.get() + (element.clientWidth - width) / 2);
65
+ y.set(y.get() + (element.clientHeight - height) / 2);
66
+ width = element.clientWidth;
67
+ height = element.clientHeight;
68
+ });
69
+ observer.observe(element);
70
+ return () => observer.disconnect();
71
+ }, [x, y]);
59
72
  useEffect(() => {
60
73
  const element = surface.current;
61
74
  const onWheel = (event) => {