foldkit 0.56.0 → 0.58.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 (105) hide show
  1. package/README.md +21 -11
  2. package/dist/devtools/overlay-styles.d.ts +1 -1
  3. package/dist/devtools/overlay-styles.d.ts.map +1 -1
  4. package/dist/devtools/overlay-styles.js +5 -1
  5. package/dist/devtools/overlay.d.ts.map +1 -1
  6. package/dist/devtools/overlay.js +52 -19
  7. package/dist/file/error.d.ts +10 -0
  8. package/dist/file/error.d.ts.map +1 -0
  9. package/dist/file/error.js +4 -0
  10. package/dist/file/file.d.ts +24 -0
  11. package/dist/file/file.d.ts.map +1 -0
  12. package/dist/file/file.js +16 -0
  13. package/dist/file/index.d.ts +5 -0
  14. package/dist/file/index.d.ts.map +1 -0
  15. package/dist/file/index.js +4 -0
  16. package/dist/file/public.d.ts +2 -0
  17. package/dist/file/public.d.ts.map +1 -0
  18. package/dist/file/public.js +1 -0
  19. package/dist/file/reader.d.ts +56 -0
  20. package/dist/file/reader.d.ts.map +1 -0
  21. package/dist/file/reader.js +92 -0
  22. package/dist/file/select.d.ts +36 -0
  23. package/dist/file/select.d.ts.map +1 -0
  24. package/dist/file/select.js +60 -0
  25. package/dist/html/index.d.ts +34 -3
  26. package/dist/html/index.d.ts.map +1 -1
  27. package/dist/html/index.js +40 -1
  28. package/dist/html/lazy.d.ts +7 -5
  29. package/dist/html/lazy.d.ts.map +1 -1
  30. package/dist/html/lazy.js +12 -8
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/runtime/runtime.d.ts.map +1 -1
  35. package/dist/runtime/runtime.js +2 -4
  36. package/dist/test/apps/disabledButton.d.ts +11 -3
  37. package/dist/test/apps/disabledButton.d.ts.map +1 -1
  38. package/dist/test/apps/fileUpload.d.ts +17 -0
  39. package/dist/test/apps/fileUpload.d.ts.map +1 -0
  40. package/dist/test/apps/fileUpload.js +29 -0
  41. package/dist/test/apps/resumeUpload.d.ts +43 -0
  42. package/dist/test/apps/resumeUpload.d.ts.map +1 -0
  43. package/dist/test/apps/resumeUpload.js +85 -0
  44. package/dist/test/query.d.ts +2 -1
  45. package/dist/test/query.d.ts.map +1 -1
  46. package/dist/test/query.js +15 -2
  47. package/dist/test/scene.d.ts +18 -0
  48. package/dist/test/scene.d.ts.map +1 -1
  49. package/dist/test/scene.js +46 -0
  50. package/dist/ui/combobox/multi.d.ts +31 -10
  51. package/dist/ui/combobox/multi.d.ts.map +1 -1
  52. package/dist/ui/combobox/public.d.ts +1 -2
  53. package/dist/ui/combobox/public.d.ts.map +1 -1
  54. package/dist/ui/combobox/public.js +1 -2
  55. package/dist/ui/combobox/shared.d.ts +33 -33
  56. package/dist/ui/combobox/shared.d.ts.map +1 -1
  57. package/dist/ui/combobox/shared.js +89 -112
  58. package/dist/ui/combobox/single.d.ts +31 -10
  59. package/dist/ui/combobox/single.d.ts.map +1 -1
  60. package/dist/ui/combobox/single.js +1 -5
  61. package/dist/ui/dialog/index.d.ts +10 -15
  62. package/dist/ui/dialog/index.d.ts.map +1 -1
  63. package/dist/ui/dialog/index.js +68 -73
  64. package/dist/ui/dialog/public.d.ts +1 -1
  65. package/dist/ui/dialog/public.d.ts.map +1 -1
  66. package/dist/ui/dialog/public.js +1 -1
  67. package/dist/ui/listbox/multi.d.ts +30 -9
  68. package/dist/ui/listbox/multi.d.ts.map +1 -1
  69. package/dist/ui/listbox/public.d.ts +1 -2
  70. package/dist/ui/listbox/public.d.ts.map +1 -1
  71. package/dist/ui/listbox/public.js +1 -2
  72. package/dist/ui/listbox/shared.d.ts +35 -36
  73. package/dist/ui/listbox/shared.d.ts.map +1 -1
  74. package/dist/ui/listbox/shared.js +106 -104
  75. package/dist/ui/listbox/single.d.ts +30 -9
  76. package/dist/ui/listbox/single.d.ts.map +1 -1
  77. package/dist/ui/listbox/single.js +9 -12
  78. package/dist/ui/menu/index.d.ts +22 -26
  79. package/dist/ui/menu/index.d.ts.map +1 -1
  80. package/dist/ui/menu/index.js +103 -91
  81. package/dist/ui/menu/public.d.ts +1 -2
  82. package/dist/ui/menu/public.d.ts.map +1 -1
  83. package/dist/ui/menu/public.js +1 -2
  84. package/dist/ui/popover/index.d.ts +21 -22
  85. package/dist/ui/popover/index.d.ts.map +1 -1
  86. package/dist/ui/popover/index.js +92 -75
  87. package/dist/ui/popover/public.d.ts +1 -2
  88. package/dist/ui/popover/public.d.ts.map +1 -1
  89. package/dist/ui/popover/public.js +1 -2
  90. package/dist/ui/transition/index.d.ts +8 -58
  91. package/dist/ui/transition/index.d.ts.map +1 -1
  92. package/dist/ui/transition/index.js +21 -111
  93. package/dist/ui/transition/public.d.ts +1 -1
  94. package/dist/ui/transition/public.d.ts.map +1 -1
  95. package/dist/ui/transition/public.js +1 -1
  96. package/dist/ui/transition/schema.d.ts +43 -0
  97. package/dist/ui/transition/schema.d.ts.map +1 -0
  98. package/dist/ui/transition/schema.js +35 -0
  99. package/dist/ui/transition/update.d.ts +22 -0
  100. package/dist/ui/transition/update.d.ts.map +1 -0
  101. package/dist/ui/transition/update.js +68 -0
  102. package/package.json +5 -1
  103. package/dist/ui/transition.d.ts +0 -5
  104. package/dist/ui/transition.d.ts.map +0 -1
  105. package/dist/ui/transition.js +0 -3
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <h3 align="center">The frontend framework for correctness.</h3>
14
14
 
15
15
  <p align="center">
16
- <a href="https://foldkit.dev"><strong>Documentation</strong></a> · <a href="https://foldkit.dev/example-apps"><strong>Examples</strong></a> · <a href="https://foldkit.dev/getting-started"><strong>Getting Started</strong></a>
16
+ <a href="https://foldkit.dev"><strong>Documentation</strong></a> · <a href="https://foldkit.dev/manifesto"><strong>Manifesto</strong></a> · <a href="https://foldkit.dev/example-apps"><strong>Examples</strong></a> · <a href="https://foldkit.dev/getting-started"><strong>Getting Started</strong></a>
17
17
  </p>
18
18
 
19
19
  ---
@@ -33,6 +33,10 @@ It's not incremental. There's no React interop, no escape hatch from Effect, no
33
33
 
34
34
  Every Foldkit application is an [Effect](https://effect.website/) program. Your Model is a [Schema](https://effect.website/docs/schema/introduction/). Side effects are values you return, not callbacks you fire — the runtime handles when and how. If you already know Effect, Foldkit feels natural. If you're new to Effect, Foldkit is a great way to immerse yourself in it.
35
35
 
36
+ ## Coming from React?
37
+
38
+ [Coming from React](https://foldkit.dev/coming-from-react) is a guided walk through the differences. [Foldkit vs React: Side by Side](https://foldkit.dev/foldkit-vs-react-side-by-side) implements the same pixel-art editor in both frameworks so you can read them line by line.
39
+
36
40
  ## Get Started
37
41
 
38
42
  `create-foldkit-app` is the recommended way to start a new project. It scaffolds a complete setup with Tailwind, TypeScript, ESLint, Prettier, and the Vite plugin for state-preserving HMR — and lets you choose from a set of examples as your starting point.
@@ -136,16 +140,21 @@ Source: [examples/counter/src/main.ts](https://github.com/foldkit/foldkit/blob/m
136
140
 
137
141
  Foldkit is a complete system, not a collection of libraries you stitch together.
138
142
 
139
- - **Commands** Side effects are named Effects that return Messages and are executed by the runtime. Each Command has a `name` for identification in tracing and testing, and an `effect` that the runtime runs. Use any Effect combinator you want retry, timeout, race, parallel.
140
- - **Routing** Type-safe bidirectional routing. URLs parse into typed routes and routes build back into URLs. No string matching, no mismatches between parsing and building.
141
- - **Subscriptions** Declare which streams your app needs as a function of the Model. The runtime diffs and switches them when the Model changes.
142
- - **Managed Resources** Model-driven lifecycle for long-lived browser resources like WebSockets, AudioContext, and RTCPeerConnection. Acquire on state change, release on cleanup.
143
- - **UI Components** Dialog, menu, tabs, listbox, disclosure fully accessible primitives that are easy to style and customize.
144
- - **Field Validation** Per-field validation state modeled as a discriminated union. Define rules as data, apply them in update, and the Model tracks the result.
145
- - **Virtual DOM** Declarative Views powered by [Snabbdom](https://github.com/snabbdom/snabbdom). Fast, keyed diffing. Views are plain functions of your Model.
146
- - **DevTools** Built-in overlay for inspecting Messages, Model state, and Commands. Time-travel mode lets you jump to any point in your app's history.
147
- - **Testing** Simulate the update loop in tests. Send Messages, resolve Commands inline, and assert on the Model. No mocking libraries, no fake timers, no setup or teardown.
148
- - **HMR** Vite plugin with state-preserving hot module replacement. Change your view, keep your state.
143
+ - **Commands**: Side effects are named Effects that return Messages and are executed by the runtime. Define them with `Command.define`, passing the result Message schemas so the Effect's return type stays in lockstep with your Messages. Use any Effect combinator you want: retry, timeout, race, parallel. You write the Effect, the runtime runs it.
144
+ - **Routing**: Type-safe bidirectional routing built from parser combinators. URLs parse into typed Routes and Routes build back into URLs. No string matching, no mismatches between parsing and building.
145
+ - **Subscriptions**: Declare which streams your app needs as a function of the Model. The runtime diffs and switches them as the Model changes.
146
+ - **Managed Resources**: Model-driven lifecycle for long-lived browser resources like WebSockets, AudioContext, and RTCPeerConnection. Acquire on state change, release on cleanup.
147
+ - **Submodels**: A pattern for composing nested modules. A child owns its own Model, Messages, update function, and view; the parent embeds it and wraps child Messages in a `Got*Message` envelope. The pattern scales unchanged from a login form to a multi-page app.
148
+ - **OutMessage**: A typed channel for a child Submodel to emit domain events up to its parent, so the parent reacts to meaningful facts instead of internal child Messages.
149
+ - **UI Components**: Accessible, keyboard-friendly primitives covering Button, Checkbox, Combobox, Dialog, Disclosure, DragAndDrop, Fieldset, Input, Listbox, Menu, Popover, RadioGroup, Select, Switch, Tabs, Textarea, and Transition. Every component is a Submodel with a typed `ViewConfig`, domain-event callbacks like `onSelected`, `onClosed`, and `onToggled`, and `className` plus `attributes` props on every slot for styling and extension. Animated components share a `Transition` Submodel that coordinates CSS enter and leave animations.
150
+ - **Field Validation**: Per-field validation state modeled as a discriminated union. Define rules as data, apply them in update, and the Model tracks the result.
151
+ - **Virtual DOM**: Declarative views powered by [Snabbdom](https://github.com/snabbdom/snabbdom), with lazy memoization and fast, keyed diffing. Views are plain functions of your Model.
152
+ - **DevTools**: Built-in overlay for inspecting Messages, Model state, and Commands. Time-travel mode lets you jump to any point in history, Inspect mode browses snapshots without pausing, and Submodel drill-in filtering scopes the Message list to any nested module.
153
+ - **Crash View and Reporting**: Configure `crash.view` to render a custom fallback UI when the update loop throws. A `crash.report` callback fires first with the error, Model, and triggering Message, so you can ship it straight to Sentry or your logger.
154
+ - **Story Testing**: Exercise the update function directly. Send Messages, resolve Commands inline with `resolve` and `resolveAll`, and assert with focused helpers: `Story.model`, `Story.expectHasCommands`, `Story.expectExactCommands`, `Story.expectNoCommands`, and `Story.expectOutMessage`. No mocking libraries, no fake timers.
155
+ - **Scene Testing**: Drive your app the way a user does. Scene renders your real view, then clicks buttons, types into inputs, presses keys, and asserts on what's on screen. Accessible locators (`role`, `label`, `placeholder`, `altText`, `title`, `testId`, `displayValue`) with full options (`name`, `level`, `checked`, `selected`, `pressed`, `expanded`, `disabled`), multi-match `Scene.all` with `Scene.filter` and `Scene.nth`, scoped steps via `Scene.inside`, pointer events, event bubbling, and Vitest matchers like `toHaveText`, `toBeVisible`, `toHaveAccessibleName`, and `toHaveCount`. API parity with React Testing Library and Playwright, without a browser.
156
+ - **Slow-View Monitoring**: Wire `slowView` on `makeProgram` to catch renders that exceed a threshold you set. The callback fires with the current Model, the triggering Message, and the render duration, so you can log it, sample it, or ship it to your observability tool.
157
+ - **HMR**: Vite plugin with state-preserving hot module replacement. Change your view, keep your state.
149
158
 
150
159
  ## Correctness You (And Your LLM) Can See
151
160
 
@@ -168,6 +177,7 @@ This is what makes Foldkit unusually AI-friendly. The same property that makes t
168
177
  - **[Shopping Cart](https://foldkit.dev/example-apps/shopping-cart)** — Nested models and complex state
169
178
  - **[WebSocket Chat](https://foldkit.dev/example-apps/websocket-chat)** — Managed Resources with WebSocket integration
170
179
  - **[Kanban](https://foldkit.dev/example-apps/kanban)** — Drag-and-drop kanban board with cross-column reordering and keyboard navigation
180
+ - **[Pixel Art](https://foldkit.dev/example-apps/pixel-art)** — Grid-based pixel editor with painting, erasing, and palette selection
171
181
  - **[UI Showcase](https://foldkit.dev/example-apps/ui-showcase)** — Interactive showcase of every Foldkit UI component
172
182
  - **[Typing Game](https://github.com/foldkit/foldkit/tree/main/packages/typing-game)** — Multiplayer typing game with Effect RPC backend ([play it live](https://typingterminal.com))
173
183
 
@@ -1,3 +1,3 @@
1
- declare const overlayStyles = ":host {\n position: relative;\n z-index: 2147483647;\n\n --dt-bg: #1e1e2e;\n --dt-surface-selected: #282839;\n --dt-border: #45475a;\n --dt-text: #cdd6f4;\n --dt-text-muted: #9399b2;\n --dt-accent: #cba6f7;\n --dt-live: #a6e3a1;\n --dt-paused: #fab387;\n --dt-json-string: #a6e3a1;\n --dt-json-number: #89b4fa;\n --dt-json-boolean: #fab387;\n --dt-json-null: #9399b2;\n --dt-json-key: #89dceb;\n --dt-json-tag: #cba6f7;\n --dt-json-preview: #9399b2;\n --dt-json-arrow: #9399b2;\n --dt-tree-hover: #313244;\n --dt-diff-changed: #74c7ec;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\nbutton {\n font: inherit;\n color: inherit;\n}\nul {\n list-style: none;\n}\n\n.fixed {\n position: fixed;\n}\n.flex {\n display: flex;\n}\n.flex-col {\n flex-direction: column;\n}\n.flex-1 {\n flex: 1 1 0%;\n}\n.items-center {\n align-items: center;\n}\n.justify-center {\n justify-content: center;\n}\n.justify-between {\n justify-content: space-between;\n}\n.shrink-0 {\n flex-shrink: 0;\n}\n.inline-block {\n display: inline-block;\n}\n.gap-0\\.5 {\n gap: 2px;\n}\n.gap-1\\.5 {\n gap: 6px;\n}\n.gap-2 {\n gap: 8px;\n}\n.gap-px {\n gap: 1px;\n}\n.px-1 {\n padding-left: 4px;\n padding-right: 4px;\n}\n.px-2 {\n padding-left: 8px;\n padding-right: 8px;\n}\n.px-2\\.5 {\n padding-left: 10px;\n padding-right: 10px;\n}\n.p-3 {\n padding: 12px;\n}\n.px-3 {\n padding-left: 12px;\n padding-right: 12px;\n}\n.py-0\\.5 {\n padding-top: 2px;\n padding-bottom: 2px;\n}\n.pt-1 {\n padding-top: 4px;\n}\n.pl-1 {\n padding-left: 4px;\n}\n.py-1 {\n padding-top: 4px;\n padding-bottom: 4px;\n}\n.py-1\\.5 {\n padding-top: 6px;\n padding-bottom: 6px;\n}\n.py-2 {\n padding-top: 8px;\n padding-bottom: 8px;\n}\n.py-px {\n padding-top: 1px;\n padding-bottom: 1px;\n}\n.w-1\\.5 {\n width: 6px;\n}\n.h-1\\.5 {\n height: 6px;\n}\n.w-3 {\n width: 12px;\n}\n.w-5 {\n width: 20px;\n}\n.h-5 {\n height: 20px;\n}\n.w-14 {\n width: 56px;\n}\n.h-14 {\n height: 56px;\n}\n.min-w-0 {\n min-width: 0;\n}\n.min-w-5 {\n min-width: 20px;\n}\n.min-h-0 {\n min-height: 0;\n}\n/* Badge positions \u2014 flush against side edge */\n.dt-pos-br {\n bottom: 16px;\n right: 0;\n border-radius: 6px 0 0 6px;\n}\n.dt-pos-bl {\n bottom: 16px;\n left: 0;\n border-radius: 0 6px 6px 0;\n}\n.dt-pos-tr {\n top: 16px;\n right: 0;\n border-radius: 6px 0 0 6px;\n}\n.dt-pos-tl {\n top: 16px;\n left: 0;\n border-radius: 0 6px 6px 0;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-y-auto {\n overflow-y: auto;\n}\n.overscroll-none {\n overscroll-behavior: none;\n}\n\n.truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.rounded {\n border-radius: 4px;\n}\n.rounded-lg {\n border-radius: 8px;\n}\n.rounded-full {\n border-radius: 9999px;\n}\n.border {\n border-width: 1px;\n border-style: solid;\n border-color: var(--dt-border);\n}\n.border-b {\n border-bottom: 1px solid var(--dt-border);\n}\n.border-t {\n border-top: 1px solid var(--dt-border);\n}\n.border-r {\n border-right: 1px solid var(--dt-border);\n}\n.border-l {\n border-left: 1px solid var(--dt-border);\n}\n.border-none {\n border: none;\n}\n.selected {\n background-color: var(--dt-surface-selected);\n}\n.dt-row:hover:not(.selected) {\n background-color: var(--dt-tree-hover);\n}\n.dt-header-button:hover {\n color: var(--dt-text);\n}\n.dt-resume-button:hover {\n opacity: 0.7;\n}\n.dt-filter-wrapper {\n position: relative;\n flex-shrink: 0;\n border-bottom: 1px solid var(--dt-border);\n}\n.dt-filter-button {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n background: transparent;\n border: none;\n color: var(--dt-text-muted);\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n font-size: 13px;\n cursor: pointer;\n text-align: left;\n}\n.dt-filter-button:hover {\n color: var(--dt-text);\n background-color: var(--dt-tree-hover);\n}\n.dt-filter-button:focus-visible {\n outline: 1px solid var(--dt-accent);\n outline-offset: -1px;\n}\n.dt-filter-button[data-open] {\n color: var(--dt-text);\n background-color: var(--dt-surface-selected);\n}\n.dt-filter-button[data-open] .json-arrow {\n transform: rotate(180deg);\n}\n.dt-filter-items {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n background-color: var(--dt-bg);\n border-top: none;\n border-bottom: 1px solid var(--dt-border);\n z-index: 10;\n max-height: 200px;\n overflow-y: auto;\n outline: none;\n}\n.dt-filter-item {\n padding: 6px 12px;\n color: var(--dt-text-muted);\n cursor: pointer;\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n font-size: 13px;\n border-bottom: 1px solid var(--dt-border);\n}\n.dt-filter-item:last-child {\n border-bottom: none;\n}\n.dt-filter-item[data-active] {\n background-color: var(--dt-tree-hover);\n color: var(--dt-text);\n}\n.dt-filter-item[data-selected] {\n color: var(--dt-accent);\n}\n.dt-filter-check {\n width: 12px;\n height: 12px;\n visibility: hidden;\n}\n.dt-filter-item[data-selected] .dt-filter-check {\n visibility: visible;\n color: var(--dt-accent);\n}\n.dt-filter-backdrop {\n position: fixed;\n inset: 0;\n}\n.dt-tab-button {\n position: relative;\n background: transparent;\n border: none;\n border-right: 1px solid var(--dt-border);\n outline: none;\n flex: 1;\n}\n.dt-tab-button:last-child {\n border-right: none;\n}\n.dt-tab-active {\n background-color: var(--dt-surface-selected);\n}\n.dt-tab-button:not(.dt-tab-active):hover {\n color: var(--dt-text);\n background-color: rgba(49, 50, 68, 0.3);\n}\n.font-sans {\n font-family:\n system-ui,\n -apple-system,\n BlinkMacSystemFont,\n 'Segoe UI',\n Roboto,\n sans-serif;\n}\n.font-mono {\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n}\n.font-medium {\n font-weight: 500;\n}\n.font-semibold {\n font-weight: 600;\n}\n.text-xs {\n font-size: 12px;\n}\n.text-2xs {\n font-size: 10px;\n}\n.text-sm {\n font-size: 11px;\n}\n.text-base {\n font-size: 13px;\n}\n.text-md {\n font-size: 15px;\n}\n.text-lg {\n font-size: 20px;\n}\n.text-xl {\n font-size: 26px;\n}\n.italic {\n font-style: italic;\n}\n.text-right {\n text-align: right;\n}\n.tracking-wide {\n letter-spacing: 0.025em;\n}\n.tracking-wider {\n letter-spacing: 0.05em;\n}\n.leading-none {\n line-height: 1;\n}\n.leading-snug {\n line-height: 1.35;\n}\n.bg-dt-bg {\n background-color: var(--dt-bg);\n}\n.bg-dt-live {\n background-color: var(--dt-live);\n}\n.bg-transparent {\n background-color: transparent;\n}\n.text-dt {\n color: var(--dt-text);\n}\n.text-dt-bg {\n color: var(--dt-bg);\n}\n.text-dt-muted {\n color: var(--dt-text-muted);\n}\n.text-dt-accent {\n color: var(--dt-accent);\n}\n.text-dt-live {\n color: var(--dt-live);\n}\n.text-dt-paused {\n color: var(--dt-paused);\n}\n.cursor-pointer {\n cursor: pointer;\n}\n.outline-none {\n outline: none;\n}\n.transition-colors {\n transition-property: color, background-color, border-color;\n transition-duration: 100ms;\n transition-timing-function: ease;\n}\n\n/* Panel */\n.dt-panel {\n width: 360px;\n height: 480px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n z-index: 99998;\n}\n/* Panel positions */\n.dt-panel-br {\n bottom: 16px;\n right: 28px;\n}\n.dt-panel-bl {\n bottom: 16px;\n left: 28px;\n}\n.dt-panel-tr {\n top: 16px;\n right: 28px;\n}\n.dt-panel-tl {\n top: 16px;\n left: 28px;\n}\n.dt-panel-wide {\n width: 720px;\n}\n.dt-message-pane {\n width: 320px;\n flex-shrink: 0;\n}\n.dt-badge {\n z-index: 99999;\n box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);\n transition: background-color 150ms ease;\n border: 1px solid var(--dt-border);\n}\n.dt-badge-accent:hover {\n background-color: #252538;\n}\n.dt-badge-paused {\n background-color: var(--dt-paused);\n color: var(--dt-bg);\n border: none;\n}\n.dt-badge-paused:hover {\n background-color: #e0a070;\n}\n.dt-badge.dt-pos-br,\n.dt-badge.dt-pos-tr {\n border-right: none;\n}\n.dt-badge.dt-pos-bl,\n.dt-badge.dt-pos-tl {\n border-left: none;\n}\n\n/* JSON tree */\n.tree-row {\n position: relative;\n white-space: nowrap;\n line-height: 18px;\n padding-right: 8px;\n}\n.tree-row-expandable:hover {\n background-color: var(--dt-tree-hover);\n}\n.inspector-tree {\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n}\n.json-key {\n color: var(--dt-json-key);\n}\n.json-string {\n color: var(--dt-json-string);\n}\n.json-number {\n color: var(--dt-json-number);\n}\n.json-boolean {\n color: var(--dt-json-boolean);\n}\n.json-null {\n color: var(--dt-json-null);\n}\n.json-tag {\n color: var(--dt-json-tag);\n margin-right: 4px;\n}\n.json-preview {\n color: var(--dt-json-preview);\n}\n.json-arrow {\n color: var(--dt-json-arrow);\n width: 10px;\n height: 10px;\n user-select: none;\n}\n\n/* Diff */\n.diff-changed {\n background-color: rgba(116, 199, 236, 0.06);\n}\n.diff-dot {\n position: absolute;\n left: 3px;\n width: 5px;\n height: 5px;\n border-radius: 9999px;\n background-color: var(--dt-diff-changed);\n}\n.diff-dot-inline {\n width: 5px;\n height: 5px;\n border-radius: 9999px;\n background-color: var(--dt-diff-changed);\n flex-shrink: 0;\n}\n.dot-column {\n width: 5px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.pause-column {\n width: 8px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.dt-pause-icon {\n width: 8px;\n height: 8px;\n color: var(--dt-paused);\n}\n\n/* Interaction blocker \u2014 covers the app while time-travelling */\n.dt-interaction-blocker {\n position: fixed;\n inset: 0;\n z-index: 99997;\n cursor: not-allowed;\n}\n\n/* Mobile */\n@media (max-width: 767px) {\n .dt-panel {\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n border-radius: 0;\n border: none;\n }\n .dt-panel-wide {\n width: 100%;\n }\n .dt-content {\n flex-direction: column;\n }\n .dt-message-pane {\n width: 100%;\n max-height: 40%;\n border-bottom: 1px solid var(--dt-border);\n }\n .message-list > :last-child {\n border-bottom: none;\n }\n .dt-inspector-pane {\n border-left: none;\n }\n}\n";
1
+ declare const overlayStyles = ":host {\n position: relative;\n z-index: 2147483647;\n\n --dt-bg: #1e1e2e;\n --dt-surface-selected: #282839;\n --dt-border: #45475a;\n --dt-text: #cdd6f4;\n --dt-text-muted: #9399b2;\n --dt-accent: #cba6f7;\n --dt-live: #a6e3a1;\n --dt-paused: #fab387;\n --dt-json-string: #a6e3a1;\n --dt-json-number: #89b4fa;\n --dt-json-boolean: #fab387;\n --dt-json-null: #9399b2;\n --dt-json-key: #89dceb;\n --dt-json-tag: #cba6f7;\n --dt-json-preview: #9399b2;\n --dt-json-arrow: #9399b2;\n --dt-tree-hover: #313244;\n --dt-diff-changed: #74c7ec;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\nbutton {\n font: inherit;\n color: inherit;\n}\nul {\n list-style: none;\n}\n\n.fixed {\n position: fixed;\n}\n.flex {\n display: flex;\n}\n.flex-col {\n flex-direction: column;\n}\n.flex-1 {\n flex: 1 1 0%;\n}\n.items-center {\n align-items: center;\n}\n.justify-center {\n justify-content: center;\n}\n.justify-between {\n justify-content: space-between;\n}\n.shrink-0 {\n flex-shrink: 0;\n}\n.inline-block {\n display: inline-block;\n}\n.gap-0\\.5 {\n gap: 2px;\n}\n.gap-1\\.5 {\n gap: 6px;\n}\n.gap-2 {\n gap: 8px;\n}\n.gap-px {\n gap: 1px;\n}\n.px-1 {\n padding-left: 4px;\n padding-right: 4px;\n}\n.px-2 {\n padding-left: 8px;\n padding-right: 8px;\n}\n.px-2\\.5 {\n padding-left: 10px;\n padding-right: 10px;\n}\n.p-3 {\n padding: 12px;\n}\n.px-3 {\n padding-left: 12px;\n padding-right: 12px;\n}\n.py-0\\.5 {\n padding-top: 2px;\n padding-bottom: 2px;\n}\n.pt-1 {\n padding-top: 4px;\n}\n.pl-1 {\n padding-left: 4px;\n}\n.py-1 {\n padding-top: 4px;\n padding-bottom: 4px;\n}\n.py-1\\.5 {\n padding-top: 6px;\n padding-bottom: 6px;\n}\n.py-2 {\n padding-top: 8px;\n padding-bottom: 8px;\n}\n.py-px {\n padding-top: 1px;\n padding-bottom: 1px;\n}\n.w-1\\.5 {\n width: 6px;\n}\n.h-1\\.5 {\n height: 6px;\n}\n.w-3 {\n width: 12px;\n}\n.w-5 {\n width: 20px;\n}\n.h-5 {\n height: 20px;\n}\n.w-14 {\n width: 56px;\n}\n.h-14 {\n height: 56px;\n}\n.min-w-0 {\n min-width: 0;\n}\n.min-w-5 {\n min-width: 20px;\n}\n.min-h-0 {\n min-height: 0;\n}\n/* Badge positions \u2014 flush against side edge */\n.dt-pos-br {\n bottom: 16px;\n right: 0;\n border-radius: 6px 0 0 6px;\n}\n.dt-pos-bl {\n bottom: 16px;\n left: 0;\n border-radius: 0 6px 6px 0;\n}\n.dt-pos-tr {\n top: 16px;\n right: 0;\n border-radius: 6px 0 0 6px;\n}\n.dt-pos-tl {\n top: 16px;\n left: 0;\n border-radius: 0 6px 6px 0;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-auto {\n overflow: auto;\n}\n.overflow-y-auto {\n overflow-y: auto;\n}\n.overscroll-none {\n overscroll-behavior: none;\n}\n\n.truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.rounded {\n border-radius: 4px;\n}\n.rounded-lg {\n border-radius: 8px;\n}\n.rounded-full {\n border-radius: 9999px;\n}\n.border {\n border-width: 1px;\n border-style: solid;\n border-color: var(--dt-border);\n}\n.border-b {\n border-bottom: 1px solid var(--dt-border);\n}\n.border-t {\n border-top: 1px solid var(--dt-border);\n}\n.border-r {\n border-right: 1px solid var(--dt-border);\n}\n.border-l {\n border-left: 1px solid var(--dt-border);\n}\n.border-none {\n border: none;\n}\n.selected {\n background-color: var(--dt-surface-selected);\n}\n.dt-row:hover:not(.selected) {\n background-color: var(--dt-tree-hover);\n}\n.dt-header-button:hover {\n color: var(--dt-text);\n}\n.dt-resume-button:hover {\n opacity: 0.7;\n}\n.dt-filter-wrapper {\n position: relative;\n flex-shrink: 0;\n border-bottom: 1px solid var(--dt-border);\n}\n.dt-filter-button {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n background: transparent;\n border: none;\n color: var(--dt-text-muted);\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n font-size: 13px;\n cursor: pointer;\n text-align: left;\n}\n.dt-filter-button:hover {\n color: var(--dt-text);\n background-color: var(--dt-tree-hover);\n}\n.dt-filter-button:focus-visible {\n outline: 1px solid var(--dt-accent);\n outline-offset: -1px;\n}\n.dt-filter-button[data-open] {\n color: var(--dt-text);\n background-color: var(--dt-surface-selected);\n}\n.dt-filter-button[data-open]:hover {\n background-color: var(--dt-tree-hover);\n}\n.dt-filter-button[data-open] .json-arrow {\n transform: rotate(180deg);\n}\n.dt-filter-items {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n background-color: var(--dt-bg);\n border-top: 1px solid var(--dt-border);\n border-bottom: 1px solid var(--dt-border);\n z-index: 10;\n max-height: 200px;\n overflow-y: auto;\n outline: none;\n}\n.dt-filter-item {\n padding: 6px 12px;\n color: var(--dt-text-muted);\n cursor: pointer;\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n font-size: 13px;\n border-bottom: 1px solid var(--dt-border);\n}\n.dt-filter-item:last-child {\n border-bottom: none;\n}\n.dt-filter-item[data-active] {\n background-color: var(--dt-tree-hover);\n color: var(--dt-text);\n}\n.dt-filter-item[data-selected] {\n color: var(--dt-accent);\n}\n.dt-filter-check {\n width: 12px;\n height: 12px;\n visibility: hidden;\n}\n.dt-filter-item[data-selected] .dt-filter-check {\n visibility: visible;\n color: var(--dt-accent);\n}\n.dt-filter-backdrop {\n position: fixed;\n inset: 0;\n pointer-events: none;\n}\n.dt-tab-button {\n position: relative;\n background: transparent;\n border: none;\n border-right: 1px solid var(--dt-border);\n outline: none;\n flex: 1;\n}\n.dt-tab-button:last-child {\n border-right: none;\n}\n.dt-tab-active {\n background-color: var(--dt-surface-selected);\n}\n.dt-tab-button:not(.dt-tab-active):hover {\n color: var(--dt-text);\n background-color: rgba(49, 50, 68, 0.3);\n}\n.font-sans {\n font-family:\n system-ui,\n -apple-system,\n BlinkMacSystemFont,\n 'Segoe UI',\n Roboto,\n sans-serif;\n}\n.font-mono {\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n}\n.font-medium {\n font-weight: 500;\n}\n.font-semibold {\n font-weight: 600;\n}\n.text-xs {\n font-size: 12px;\n}\n.text-2xs {\n font-size: 10px;\n}\n.text-sm {\n font-size: 11px;\n}\n.text-base {\n font-size: 13px;\n}\n.text-md {\n font-size: 15px;\n}\n.text-lg {\n font-size: 20px;\n}\n.text-xl {\n font-size: 26px;\n}\n.italic {\n font-style: italic;\n}\n.text-right {\n text-align: right;\n}\n.tracking-wide {\n letter-spacing: 0.025em;\n}\n.tracking-wider {\n letter-spacing: 0.05em;\n}\n.leading-none {\n line-height: 1;\n}\n.leading-snug {\n line-height: 1.35;\n}\n.bg-dt-bg {\n background-color: var(--dt-bg);\n}\n.bg-dt-live {\n background-color: var(--dt-live);\n}\n.bg-transparent {\n background-color: transparent;\n}\n.text-dt {\n color: var(--dt-text);\n}\n.text-dt-bg {\n color: var(--dt-bg);\n}\n.text-dt-muted {\n color: var(--dt-text-muted);\n}\n.text-dt-accent {\n color: var(--dt-accent);\n}\n.text-dt-live {\n color: var(--dt-live);\n}\n.text-dt-paused {\n color: var(--dt-paused);\n}\n.cursor-pointer {\n cursor: pointer;\n}\n.outline-none {\n outline: none;\n}\n.transition-colors {\n transition-property: color, background-color, border-color;\n transition-duration: 100ms;\n transition-timing-function: ease;\n}\n\n/* Panel */\n.dt-panel {\n width: 360px;\n height: 480px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n z-index: 99998;\n}\n/* Panel positions */\n.dt-panel-br {\n bottom: 16px;\n right: 28px;\n}\n.dt-panel-bl {\n bottom: 16px;\n left: 28px;\n}\n.dt-panel-tr {\n top: 16px;\n right: 28px;\n}\n.dt-panel-tl {\n top: 16px;\n left: 28px;\n}\n.dt-panel-wide {\n width: 720px;\n}\n.dt-message-pane {\n width: 320px;\n flex-shrink: 0;\n}\n.dt-badge {\n z-index: 99999;\n box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);\n transition: background-color 150ms ease;\n border: 1px solid var(--dt-border);\n}\n.dt-badge-accent:hover {\n background-color: #252538;\n}\n.dt-badge-paused {\n background-color: var(--dt-paused);\n color: var(--dt-bg);\n border: none;\n}\n.dt-badge-paused:hover {\n background-color: #e0a070;\n}\n.dt-badge.dt-pos-br,\n.dt-badge.dt-pos-tr {\n border-right: none;\n}\n.dt-badge.dt-pos-bl,\n.dt-badge.dt-pos-tl {\n border-left: none;\n}\n\n/* JSON tree */\n.tree-row {\n position: relative;\n white-space: nowrap;\n line-height: 18px;\n padding-right: 8px;\n}\n.tree-row-expandable:hover {\n background-color: var(--dt-tree-hover);\n}\n.inspector-tree {\n font-family:\n ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;\n}\n.json-key {\n color: var(--dt-json-key);\n}\n.json-string {\n color: var(--dt-json-string);\n}\n.json-number {\n color: var(--dt-json-number);\n}\n.json-boolean {\n color: var(--dt-json-boolean);\n}\n.json-null {\n color: var(--dt-json-null);\n}\n.json-tag {\n color: var(--dt-json-tag);\n margin-right: 4px;\n}\n.json-preview {\n color: var(--dt-json-preview);\n}\n.json-arrow {\n color: var(--dt-json-arrow);\n width: 10px;\n height: 10px;\n user-select: none;\n}\n\n/* Diff */\n.diff-changed {\n background-color: rgba(116, 199, 236, 0.06);\n}\n.diff-dot {\n position: absolute;\n left: 3px;\n width: 5px;\n height: 5px;\n border-radius: 9999px;\n background-color: var(--dt-diff-changed);\n}\n.diff-dot-inline {\n width: 5px;\n height: 5px;\n border-radius: 9999px;\n background-color: var(--dt-diff-changed);\n flex-shrink: 0;\n}\n.dot-column {\n width: 5px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.pause-column {\n width: 8px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.dt-pause-icon {\n width: 8px;\n height: 8px;\n color: var(--dt-paused);\n}\n\n/* Interaction blocker \u2014 covers the app while time-travelling */\n.dt-interaction-blocker {\n position: fixed;\n inset: 0;\n z-index: 99997;\n cursor: not-allowed;\n}\n\n/* Mobile */\n@media (max-width: 767px) {\n .dt-panel {\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n border-radius: 0;\n border: none;\n }\n .dt-panel-wide {\n width: 100%;\n }\n .dt-content {\n flex-direction: column;\n }\n .dt-message-pane {\n width: 100%;\n max-height: 40%;\n border-bottom: 1px solid var(--dt-border);\n }\n .message-list > :last-child {\n border-bottom: none;\n }\n .dt-inspector-pane {\n border-left: none;\n }\n}\n";
2
2
  export { overlayStyles };
3
3
  //# sourceMappingURL=overlay-styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-styles.d.ts","sourceRoot":"","sources":["../../src/devtools/overlay-styles.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,aAAa,kvUA6lBlB,CAAA;AAED,OAAO,EAAE,aAAa,EAAE,CAAA"}
1
+ {"version":3,"file":"overlay-styles.d.ts","sourceRoot":"","sources":["../../src/devtools/overlay-styles.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,aAAa,q3UAimBlB,CAAA;AAED,OAAO,EAAE,aAAa,EAAE,CAAA"}
@@ -263,6 +263,9 @@ ul {
263
263
  color: var(--dt-text);
264
264
  background-color: var(--dt-surface-selected);
265
265
  }
266
+ .dt-filter-button[data-open]:hover {
267
+ background-color: var(--dt-tree-hover);
268
+ }
266
269
  .dt-filter-button[data-open] .json-arrow {
267
270
  transform: rotate(180deg);
268
271
  }
@@ -272,7 +275,7 @@ ul {
272
275
  left: 0;
273
276
  right: 0;
274
277
  background-color: var(--dt-bg);
275
- border-top: none;
278
+ border-top: 1px solid var(--dt-border);
276
279
  border-bottom: 1px solid var(--dt-border);
277
280
  z-index: 10;
278
281
  max-height: 200px;
@@ -310,6 +313,7 @@ ul {
310
313
  .dt-filter-backdrop {
311
314
  position: fixed;
312
315
  inset: 0;
316
+ pointer-events: none;
313
317
  }
314
318
  .dt-tab-button {
315
319
  position: relative;
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../src/devtools/overlay.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,MAAM,EAGN,OAAO,EAGP,MAAM,EASP,MAAM,QAAQ,CAAA;AAEf,OAAO,KAAK,OAAO,MAAM,YAAY,CAAA;AAKrC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAOxE,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,SAAS,CAAA;AAmNhB,eAAO,MAAM,MAAM;;EAA0C,CAAA;AAC7D,eAAO,MAAM,YAAY;;;;;;EAGxB,CAAA;AACD,eAAO,MAAM,aAAa;;;;;;EAGzB,CAAA;AACD,eAAO,MAAM,MAAM;;EAA4C,CAAA;AAC/D,eAAO,MAAM,KAAK;;EAA0C,CAAA;AAC5D,eAAO,MAAM,UAAU;;EAA6C,CAAA;AACpE,eAAO,MAAM,YAAY;;EAAiD,CAAA;AAC1E,eAAO,MAAM,WAAW;;EAA+C,CAAA;AA2xCvE,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,UAAU,gBAAgB,EAC1B,MAAM,YAAY,EAClB,aAAa,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,sCAkDhC,CAAA"}
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../src/devtools/overlay.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,MAAM,EAGN,OAAO,EAGP,MAAM,EASP,MAAM,QAAQ,CAAA;AAEf,OAAO,KAAK,OAAO,MAAM,YAAY,CAAA;AAKrC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAOxE,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,SAAS,CAAA;AAuOhB,eAAO,MAAM,MAAM;;EAA0C,CAAA;AAC7D,eAAO,MAAM,YAAY;;;;;;EAGxB,CAAA;AACD,eAAO,MAAM,aAAa;;;;;;EAGzB,CAAA;AACD,eAAO,MAAM,MAAM;;EAA4C,CAAA;AAC/D,eAAO,MAAM,KAAK;;EAA0C,CAAA;AAC5D,eAAO,MAAM,UAAU;;EAA6C,CAAA;AACpE,eAAO,MAAM,YAAY;;EAAiD,CAAA;AAC1E,eAAO,MAAM,WAAW;;EAA+C,CAAA;AAmzCvE,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,UAAU,gBAAgB,EAC1B,MAAM,YAAY,EAClB,aAAa,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,sCAkDhC,CAAA"}
@@ -15,7 +15,8 @@ import { INIT_INDEX, } from './store';
15
15
  // MODEL
16
16
  const DisplayEntry = S.Struct({
17
17
  tag: S.String,
18
- maybeInnerTag: S.OptionFromSelf(S.String),
18
+ submodelPath: S.Array(S.String),
19
+ maybeLeafTag: S.OptionFromSelf(S.String),
19
20
  commandNames: S.Array(S.String),
20
21
  timestamp: S.Number,
21
22
  isModelChanged: S.Boolean,
@@ -104,20 +105,36 @@ const MAX_PREVIEW_KEYS = 3;
104
105
  const ALL_MESSAGES_VALUE = '';
105
106
  const formatTimeDelta = (deltaMs) => M.value(deltaMs).pipe(M.when(0, () => '0ms'), M.when(Number_.lessThan(MILLIS_PER_SECOND), ms => `+${Math.round(ms)}ms`), M.orElse(ms => `+${(ms / MILLIS_PER_SECOND).toFixed(1)}s`));
106
107
  const MESSAGE_LIST_SELECTOR = '.message-list';
107
- const computeSubmodelTags = (entries) => pipe(entries, Array_.filterMap(entry => entry.maybeInnerTag.pipe(Option.as(entry.tag))), Array_.dedupe, Array_.sort(Order.string));
108
+ const computeSubmodelTags = (entries) => pipe(entries, Array_.flatMap(({ submodelPath }) => submodelPath), Array_.dedupe, Array_.sort(Order.string));
108
109
  const GOT_MESSAGE_PATTERN = /^Got.+Message$/;
109
- const extractInnerTag = (entry) => pipe(entry.tag, String_.search(GOT_MESSAGE_PATTERN), Option.flatMap(() => {
110
+ const extractSubmodelInfo = (entry) => {
111
+ if (!GOT_MESSAGE_PATTERN.test(entry.tag)) {
112
+ return { submodelPath: [], maybeLeafTag: Option.none() };
113
+ }
114
+ const path = [entry.tag];
110
115
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
111
- const inner = entry.message?.['message'];
112
- return pipe(inner, Option.liftPredicate(isTagged), Option.map(({ _tag }) => _tag));
113
- }));
114
- const toDisplayEntries = ({ entries }) => Array_.map(entries, entry => ({
115
- tag: entry.tag,
116
- maybeInnerTag: extractInnerTag(entry),
117
- commandNames: entry.commandNames,
118
- timestamp: entry.timestamp,
119
- isModelChanged: entry.isModelChanged,
120
- }));
116
+ let current = entry.message?.['message'];
117
+ while (isTagged(current) && GOT_MESSAGE_PATTERN.test(current._tag)) {
118
+ path.push(current._tag);
119
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
120
+ current = current?.['message'];
121
+ }
122
+ return {
123
+ submodelPath: path,
124
+ maybeLeafTag: pipe(current, Option.liftPredicate(isTagged), Option.map(({ _tag }) => _tag)),
125
+ };
126
+ };
127
+ const toDisplayEntries = ({ entries }) => Array_.map(entries, entry => {
128
+ const { submodelPath, maybeLeafTag } = extractSubmodelInfo(entry);
129
+ return {
130
+ tag: entry.tag,
131
+ submodelPath,
132
+ maybeLeafTag,
133
+ commandNames: entry.commandNames,
134
+ timestamp: entry.timestamp,
135
+ isModelChanged: entry.isModelChanged,
136
+ };
137
+ });
121
138
  const toDisplayState = (state) => ({
122
139
  entries: toDisplayEntries(state),
123
140
  initCommandNames: state.initCommandNames,
@@ -521,9 +538,24 @@ const makeView = (position, mode, maybeBanner) => {
521
538
  if (Option.isNone(model.maybeSubmodelFilter)) {
522
539
  return message;
523
540
  }
524
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
525
- const inner = message?.['message'];
526
- return isTagged(inner) ? inner : message;
541
+ const { value: filterTag } = model.maybeSubmodelFilter;
542
+ let current = message;
543
+ let matched = false;
544
+ while (isTagged(current) && GOT_MESSAGE_PATTERN.test(current._tag)) {
545
+ if (current._tag === filterTag) {
546
+ matched = true;
547
+ }
548
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
549
+ const inner = current?.['message'];
550
+ if (inner === undefined) {
551
+ break;
552
+ }
553
+ current = inner;
554
+ if (matched) {
555
+ break;
556
+ }
557
+ }
558
+ return current;
527
559
  };
528
560
  const messageTabContent = (model) => Option.match(model.maybeInspectedMessage, {
529
561
  onNone: () => noMessageView,
@@ -734,18 +766,19 @@ const makeView = (position, mode, maybeBanner) => {
734
766
  : model.startIndex + model.entries.length - 1;
735
767
  const selectedIndex = M.value(mode).pipe(M.when('TimeTravel', () => model.isPaused ? model.pausedAtIndex : lastIndex), M.when('Inspect', () => model.selectedIndex), M.exhaustive);
736
768
  const isInitSelected = selectedIndex === INIT_INDEX;
737
- const isFiltered = Option.isSome(model.maybeSubmodelFilter);
769
+ const { maybeSubmodelFilter: maybeFilterTag } = model;
770
+ const isFiltered = Option.isSome(maybeFilterTag);
738
771
  const indexedEntries = pipe(model.entries, Array_.map((entry, arrayIndex) => ({
739
772
  entry,
740
773
  absoluteIndex: model.startIndex + arrayIndex,
741
774
  })), isFiltered
742
- ? Array_.filter(({ entry }) => Option.exists(model.maybeSubmodelFilter, Equal.equals(entry.tag)))
775
+ ? Array_.filter(({ entry }) => Array_.contains(entry.submodelPath, maybeFilterTag.value))
743
776
  : Function.identity);
744
777
  const messageRows = pipe(indexedEntries, Array_.map(({ entry, absoluteIndex }) => {
745
778
  const isSelected = selectedIndex === absoluteIndex;
746
779
  const isPausedHere = model.isPaused && model.pausedAtIndex === absoluteIndex;
747
780
  const displayTag = isFiltered
748
- ? Option.getOrElse(entry.maybeInnerTag, () => entry.tag)
781
+ ? pipe(entry.submodelPath, Array_.findFirstIndex(Equal.equals(maybeFilterTag.value)), Option.flatMap(filterIndex => Array_.get(entry.submodelPath, Number_.increment(filterIndex))), Option.orElse(() => entry.maybeLeafTag), Option.getOrElse(() => entry.tag))
749
782
  : entry.tag;
750
783
  return lazyMessageRow(String(absoluteIndex), messageRowView, [
751
784
  displayTag,
@@ -0,0 +1,10 @@
1
+ declare const FileReadError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
2
+ readonly _tag: "FileReadError";
3
+ } & Readonly<A>;
4
+ /** Error raised when a `FileReader` operation fails. */
5
+ export declare class FileReadError extends FileReadError_base<{
6
+ readonly reason: string;
7
+ }> {
8
+ }
9
+ export {};
10
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/file/error.ts"],"names":[],"mappings":";;;AAEA,wDAAwD;AACxD,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB,CAAC;CAAG"}
@@ -0,0 +1,4 @@
1
+ import { Data } from 'effect';
2
+ /** Error raised when a `FileReader` operation fails. */
3
+ export class FileReadError extends Data.TaggedError('FileReadError') {
4
+ }
@@ -0,0 +1,24 @@
1
+ import { Schema as S } from 'effect';
2
+ /**
3
+ * A file selected by the user. Direct alias for the browser's native `File`
4
+ * type — opaque by convention (Foldkit never constructs files itself, only
5
+ * receives them from `File.select`, `File.selectMultiple`, or from
6
+ * `OnFileChange`/`OnDropFiles` event attributes).
7
+ */
8
+ export type File = globalThis.File;
9
+ /**
10
+ * Schema that accepts any value that is an instance of the DOM `File` class.
11
+ * Use in Model fields that hold user-selected files:
12
+ *
13
+ * ```ts
14
+ * attachedResume: S.OptionFromSelf(File.File)
15
+ * ```
16
+ */
17
+ export declare const File: S.Schema<File>;
18
+ /** The file's name including extension, as reported by the browser. */
19
+ export declare const name: (file: File) => string;
20
+ /** The file's size in bytes. */
21
+ export declare const size: (file: File) => number;
22
+ /** The file's MIME type (e.g. `"application/pdf"`), or empty string if the browser cannot determine one. */
23
+ export declare const mimeType: (file: File) => string;
24
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/file/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEpC;;;;;GAKG;AACH,MAAM,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAA;AAElC;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAiC,CAAA;AAEjE,uEAAuE;AACvE,eAAO,MAAM,IAAI,GAAI,MAAM,IAAI,KAAG,MAAmB,CAAA;AAErD,gCAAgC;AAChC,eAAO,MAAM,IAAI,GAAI,MAAM,IAAI,KAAG,MAAmB,CAAA;AAErD,4GAA4G;AAC5G,eAAO,MAAM,QAAQ,GAAI,MAAM,IAAI,KAAG,MAAmB,CAAA"}
@@ -0,0 +1,16 @@
1
+ import { Schema as S } from 'effect';
2
+ /**
3
+ * Schema that accepts any value that is an instance of the DOM `File` class.
4
+ * Use in Model fields that hold user-selected files:
5
+ *
6
+ * ```ts
7
+ * attachedResume: S.OptionFromSelf(File.File)
8
+ * ```
9
+ */
10
+ export const File = S.instanceOf(globalThis.File);
11
+ /** The file's name including extension, as reported by the browser. */
12
+ export const name = (file) => file.name;
13
+ /** The file's size in bytes. */
14
+ export const size = (file) => file.size;
15
+ /** The file's MIME type (e.g. `"application/pdf"`), or empty string if the browser cannot determine one. */
16
+ export const mimeType = (file) => file.type;
@@ -0,0 +1,5 @@
1
+ export { File, mimeType, name, size } from './file';
2
+ export { FileReadError } from './error';
3
+ export { select, selectMultiple } from './select';
4
+ export { readAsArrayBuffer, readAsDataUrl, readAsText } from './reader';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { File, mimeType, name, size } from './file';
2
+ export { FileReadError } from './error';
3
+ export { select, selectMultiple } from './select';
4
+ export { readAsArrayBuffer, readAsDataUrl, readAsText } from './reader';
@@ -0,0 +1,2 @@
1
+ export { File, FileReadError, mimeType, name, readAsArrayBuffer, readAsDataUrl, readAsText, select, selectMultiple, size, } from './index';
2
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/file/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,aAAa,EACb,QAAQ,EACR,IAAI,EACJ,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,MAAM,EACN,cAAc,EACd,IAAI,GACL,MAAM,SAAS,CAAA"}
@@ -0,0 +1 @@
1
+ export { File, FileReadError, mimeType, name, readAsArrayBuffer, readAsDataUrl, readAsText, select, selectMultiple, size, } from './index';
@@ -0,0 +1,56 @@
1
+ import { Effect } from 'effect';
2
+ import { FileReadError } from './error';
3
+ import type { File } from './file';
4
+ /**
5
+ * Reads the file's contents as a UTF-8 string. Mirrors Elm's `File.toString`.
6
+ *
7
+ * Fails with a `FileReadError` if the browser's `FileReader` encounters an
8
+ * error (e.g. the file was deleted while reading). Handle failures with
9
+ * `Effect.catchAll` to convert them into a failure Message.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * ReadResumeText(
14
+ * File.readAsText(file).pipe(
15
+ * Effect.map(text => GotResumeText({ text })),
16
+ * Effect.catchAll(error => Effect.succeed(FailedReadResume({ error: error.reason }))),
17
+ * ),
18
+ * )
19
+ * ```
20
+ */
21
+ export declare const readAsText: (file: File) => Effect.Effect<string, FileReadError>;
22
+ /**
23
+ * Reads the file's contents as a base64-encoded data URL. Useful for rendering
24
+ * image previews without uploading the file first. Mirrors Elm's `File.toUrl`.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * ReadImagePreview(
29
+ * File.readAsDataUrl(imageFile).pipe(
30
+ * Effect.map(dataUrl => GotImagePreview({ dataUrl })),
31
+ * Effect.catchAll(error => Effect.succeed(FailedReadImage({ error: error.reason }))),
32
+ * ),
33
+ * )
34
+ * ```
35
+ */
36
+ export declare const readAsDataUrl: (file: File) => Effect.Effect<string, FileReadError>;
37
+ /**
38
+ * Reads the file's contents as raw binary data. Mirrors Elm's `File.toBytes`.
39
+ *
40
+ * Use this when you need the full binary payload (e.g. to upload, hash, or
41
+ * parse a custom file format). For images you usually want `readAsDataUrl`
42
+ * instead.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * UploadFile(
47
+ * File.readAsArrayBuffer(file).pipe(
48
+ * Effect.flatMap(buffer => uploadToServer(buffer)),
49
+ * Effect.map(() => SucceededUpload()),
50
+ * Effect.catchAll(error => Effect.succeed(FailedUpload({ reason: String(error) }))),
51
+ * ),
52
+ * )
53
+ * ```
54
+ */
55
+ export declare const readAsArrayBuffer: (file: File) => Effect.Effect<ArrayBuffer, FileReadError>;
56
+ //# sourceMappingURL=reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/file/reader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAoDlC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,IAAI,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAGxE,CAAA;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,IAAI,KACT,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAGnC,CAAA;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,IAAI,KACT,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,aAAa,CAGxC,CAAA"}
@@ -0,0 +1,92 @@
1
+ import { Effect } from 'effect';
2
+ import { FileReadError } from './error';
3
+ const readFile = (file, mode, extract) => Effect.async((resume, signal) => {
4
+ const reader = new FileReader();
5
+ const handleLoad = () => {
6
+ const extracted = extract(reader);
7
+ if (extracted === null) {
8
+ resume(Effect.fail(new FileReadError({
9
+ reason: `FileReader returned an unexpected result type for mode "${mode}"`,
10
+ })));
11
+ }
12
+ else {
13
+ resume(Effect.succeed(extracted));
14
+ }
15
+ };
16
+ const handleError = () => {
17
+ const reason = reader.error?.message ?? 'Unknown FileReader error';
18
+ resume(Effect.fail(new FileReadError({ reason })));
19
+ };
20
+ reader.addEventListener('load', handleLoad);
21
+ reader.addEventListener('error', handleError);
22
+ signal.addEventListener('abort', () => {
23
+ reader.abort();
24
+ });
25
+ try {
26
+ if (mode === 'text') {
27
+ reader.readAsText(file);
28
+ }
29
+ else if (mode === 'dataUrl') {
30
+ reader.readAsDataURL(file);
31
+ }
32
+ else {
33
+ reader.readAsArrayBuffer(file);
34
+ }
35
+ }
36
+ catch (thrown) {
37
+ const reason = thrown instanceof Error ? thrown.message : String(thrown);
38
+ resume(Effect.fail(new FileReadError({ reason })));
39
+ }
40
+ });
41
+ /**
42
+ * Reads the file's contents as a UTF-8 string. Mirrors Elm's `File.toString`.
43
+ *
44
+ * Fails with a `FileReadError` if the browser's `FileReader` encounters an
45
+ * error (e.g. the file was deleted while reading). Handle failures with
46
+ * `Effect.catchAll` to convert them into a failure Message.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * ReadResumeText(
51
+ * File.readAsText(file).pipe(
52
+ * Effect.map(text => GotResumeText({ text })),
53
+ * Effect.catchAll(error => Effect.succeed(FailedReadResume({ error: error.reason }))),
54
+ * ),
55
+ * )
56
+ * ```
57
+ */
58
+ export const readAsText = (file) => readFile(file, 'text', reader => typeof reader.result === 'string' ? reader.result : null);
59
+ /**
60
+ * Reads the file's contents as a base64-encoded data URL. Useful for rendering
61
+ * image previews without uploading the file first. Mirrors Elm's `File.toUrl`.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * ReadImagePreview(
66
+ * File.readAsDataUrl(imageFile).pipe(
67
+ * Effect.map(dataUrl => GotImagePreview({ dataUrl })),
68
+ * Effect.catchAll(error => Effect.succeed(FailedReadImage({ error: error.reason }))),
69
+ * ),
70
+ * )
71
+ * ```
72
+ */
73
+ export const readAsDataUrl = (file) => readFile(file, 'dataUrl', reader => typeof reader.result === 'string' ? reader.result : null);
74
+ /**
75
+ * Reads the file's contents as raw binary data. Mirrors Elm's `File.toBytes`.
76
+ *
77
+ * Use this when you need the full binary payload (e.g. to upload, hash, or
78
+ * parse a custom file format). For images you usually want `readAsDataUrl`
79
+ * instead.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * UploadFile(
84
+ * File.readAsArrayBuffer(file).pipe(
85
+ * Effect.flatMap(buffer => uploadToServer(buffer)),
86
+ * Effect.map(() => SucceededUpload()),
87
+ * Effect.catchAll(error => Effect.succeed(FailedUpload({ reason: String(error) }))),
88
+ * ),
89
+ * )
90
+ * ```
91
+ */
92
+ export const readAsArrayBuffer = (file) => readFile(file, 'arrayBuffer', reader => reader.result instanceof ArrayBuffer ? reader.result : null);
@@ -0,0 +1,36 @@
1
+ import { Effect } from 'effect';
2
+ import type { File } from './file';
3
+ /**
4
+ * Opens the native file picker allowing a single file to be selected. Resolves
5
+ * with an array containing the selected file, or an empty array if the user
6
+ * cancelled. Mirrors Elm's `File.Select.file`.
7
+ *
8
+ * The `accept` argument is a list of MIME types or file extensions that
9
+ * restrict what the picker shows. Pass an empty array to allow any file.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * SelectResume(
14
+ * File.select(['application/pdf']).pipe(
15
+ * Effect.map(files => SelectedResume({ files })),
16
+ * ),
17
+ * )
18
+ * ```
19
+ */
20
+ export declare const select: (accept: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<File>>;
21
+ /**
22
+ * Opens the native file picker allowing multiple files to be selected at
23
+ * once. Resolves with the array of selected files, or an empty array if the
24
+ * user cancelled. Mirrors Elm's `File.Select.files`.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * SelectAttachments(
29
+ * File.selectMultiple(['image/*', 'application/pdf']).pipe(
30
+ * Effect.map(files => SelectedAttachments({ files })),
31
+ * ),
32
+ * )
33
+ * ```
34
+ */
35
+ export declare const selectMultiple: (accept: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<File>>;
36
+ //# sourceMappingURL=select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/file/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEtC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AA2ClC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,MAAM,GACjB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAA4C,CAAA;AAEhF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,CAAC,MAAM,CAAC,KAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAA2C,CAAA"}
@@ -0,0 +1,60 @@
1
+ import { Array, Effect } from 'effect';
2
+ const openPicker = ({ accept, multiple, }) => Effect.async((resume, signal) => {
3
+ const input = document.createElement('input');
4
+ input.type = 'file';
5
+ input.accept = accept.join(',');
6
+ input.multiple = multiple;
7
+ input.style.display = 'none';
8
+ const cleanup = () => {
9
+ input.remove();
10
+ };
11
+ const handleChange = () => {
12
+ const files = input.files
13
+ ? Array.fromIterable(input.files)
14
+ : Array.empty();
15
+ cleanup();
16
+ resume(Effect.succeed(files));
17
+ };
18
+ const handleCancel = () => {
19
+ cleanup();
20
+ resume(Effect.succeed(Array.empty()));
21
+ };
22
+ input.addEventListener('change', handleChange);
23
+ input.addEventListener('cancel', handleCancel);
24
+ signal.addEventListener('abort', cleanup);
25
+ document.body.appendChild(input);
26
+ input.click();
27
+ });
28
+ /**
29
+ * Opens the native file picker allowing a single file to be selected. Resolves
30
+ * with an array containing the selected file, or an empty array if the user
31
+ * cancelled. Mirrors Elm's `File.Select.file`.
32
+ *
33
+ * The `accept` argument is a list of MIME types or file extensions that
34
+ * restrict what the picker shows. Pass an empty array to allow any file.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * SelectResume(
39
+ * File.select(['application/pdf']).pipe(
40
+ * Effect.map(files => SelectedResume({ files })),
41
+ * ),
42
+ * )
43
+ * ```
44
+ */
45
+ export const select = (accept) => openPicker({ accept, multiple: false });
46
+ /**
47
+ * Opens the native file picker allowing multiple files to be selected at
48
+ * once. Resolves with the array of selected files, or an empty array if the
49
+ * user cancelled. Mirrors Elm's `File.Select.files`.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * SelectAttachments(
54
+ * File.selectMultiple(['image/*', 'application/pdf']).pipe(
55
+ * Effect.map(files => SelectedAttachments({ files })),
56
+ * ),
57
+ * )
58
+ * ```
59
+ */
60
+ export const selectMultiple = (accept) => openPicker({ accept, multiple: true });