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.
- package/README.md +21 -11
- package/dist/devtools/overlay-styles.d.ts +1 -1
- package/dist/devtools/overlay-styles.d.ts.map +1 -1
- package/dist/devtools/overlay-styles.js +5 -1
- package/dist/devtools/overlay.d.ts.map +1 -1
- package/dist/devtools/overlay.js +52 -19
- package/dist/file/error.d.ts +10 -0
- package/dist/file/error.d.ts.map +1 -0
- package/dist/file/error.js +4 -0
- package/dist/file/file.d.ts +24 -0
- package/dist/file/file.d.ts.map +1 -0
- package/dist/file/file.js +16 -0
- package/dist/file/index.d.ts +5 -0
- package/dist/file/index.d.ts.map +1 -0
- package/dist/file/index.js +4 -0
- package/dist/file/public.d.ts +2 -0
- package/dist/file/public.d.ts.map +1 -0
- package/dist/file/public.js +1 -0
- package/dist/file/reader.d.ts +56 -0
- package/dist/file/reader.d.ts.map +1 -0
- package/dist/file/reader.js +92 -0
- package/dist/file/select.d.ts +36 -0
- package/dist/file/select.d.ts.map +1 -0
- package/dist/file/select.js +60 -0
- package/dist/html/index.d.ts +34 -3
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +40 -1
- package/dist/html/lazy.d.ts +7 -5
- package/dist/html/lazy.d.ts.map +1 -1
- package/dist/html/lazy.js +12 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +2 -4
- package/dist/test/apps/disabledButton.d.ts +11 -3
- package/dist/test/apps/disabledButton.d.ts.map +1 -1
- package/dist/test/apps/fileUpload.d.ts +17 -0
- package/dist/test/apps/fileUpload.d.ts.map +1 -0
- package/dist/test/apps/fileUpload.js +29 -0
- package/dist/test/apps/resumeUpload.d.ts +43 -0
- package/dist/test/apps/resumeUpload.d.ts.map +1 -0
- package/dist/test/apps/resumeUpload.js +85 -0
- package/dist/test/query.d.ts +2 -1
- package/dist/test/query.d.ts.map +1 -1
- package/dist/test/query.js +15 -2
- package/dist/test/scene.d.ts +18 -0
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +46 -0
- package/dist/ui/combobox/multi.d.ts +31 -10
- package/dist/ui/combobox/multi.d.ts.map +1 -1
- package/dist/ui/combobox/public.d.ts +1 -2
- package/dist/ui/combobox/public.d.ts.map +1 -1
- package/dist/ui/combobox/public.js +1 -2
- package/dist/ui/combobox/shared.d.ts +33 -33
- package/dist/ui/combobox/shared.d.ts.map +1 -1
- package/dist/ui/combobox/shared.js +89 -112
- package/dist/ui/combobox/single.d.ts +31 -10
- package/dist/ui/combobox/single.d.ts.map +1 -1
- package/dist/ui/combobox/single.js +1 -5
- package/dist/ui/dialog/index.d.ts +10 -15
- package/dist/ui/dialog/index.d.ts.map +1 -1
- package/dist/ui/dialog/index.js +68 -73
- package/dist/ui/dialog/public.d.ts +1 -1
- package/dist/ui/dialog/public.d.ts.map +1 -1
- package/dist/ui/dialog/public.js +1 -1
- package/dist/ui/listbox/multi.d.ts +30 -9
- package/dist/ui/listbox/multi.d.ts.map +1 -1
- package/dist/ui/listbox/public.d.ts +1 -2
- package/dist/ui/listbox/public.d.ts.map +1 -1
- package/dist/ui/listbox/public.js +1 -2
- package/dist/ui/listbox/shared.d.ts +35 -36
- package/dist/ui/listbox/shared.d.ts.map +1 -1
- package/dist/ui/listbox/shared.js +106 -104
- package/dist/ui/listbox/single.d.ts +30 -9
- package/dist/ui/listbox/single.d.ts.map +1 -1
- package/dist/ui/listbox/single.js +9 -12
- package/dist/ui/menu/index.d.ts +22 -26
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +103 -91
- package/dist/ui/menu/public.d.ts +1 -2
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/dist/ui/menu/public.js +1 -2
- package/dist/ui/popover/index.d.ts +21 -22
- package/dist/ui/popover/index.d.ts.map +1 -1
- package/dist/ui/popover/index.js +92 -75
- package/dist/ui/popover/public.d.ts +1 -2
- package/dist/ui/popover/public.d.ts.map +1 -1
- package/dist/ui/popover/public.js +1 -2
- package/dist/ui/transition/index.d.ts +8 -58
- package/dist/ui/transition/index.d.ts.map +1 -1
- package/dist/ui/transition/index.js +21 -111
- package/dist/ui/transition/public.d.ts +1 -1
- package/dist/ui/transition/public.d.ts.map +1 -1
- package/dist/ui/transition/public.js +1 -1
- package/dist/ui/transition/schema.d.ts +43 -0
- package/dist/ui/transition/schema.d.ts.map +1 -0
- package/dist/ui/transition/schema.js +35 -0
- package/dist/ui/transition/update.d.ts +22 -0
- package/dist/ui/transition/update.d.ts.map +1 -0
- package/dist/ui/transition/update.js +68 -0
- package/package.json +5 -1
- package/dist/ui/transition.d.ts +0 -5
- package/dist/ui/transition.d.ts.map +0 -1
- 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
|
|
140
|
-
- **Routing
|
|
141
|
-
- **Subscriptions
|
|
142
|
-
- **Managed Resources
|
|
143
|
-
- **
|
|
144
|
-
- **
|
|
145
|
-
- **
|
|
146
|
-
- **
|
|
147
|
-
- **
|
|
148
|
-
- **
|
|
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,
|
|
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:
|
|
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;
|
|
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"}
|
package/dist/devtools/overlay.js
CHANGED
|
@@ -15,7 +15,8 @@ import { INIT_INDEX, } from './store';
|
|
|
15
15
|
// MODEL
|
|
16
16
|
const DisplayEntry = S.Struct({
|
|
17
17
|
tag: S.String,
|
|
18
|
-
|
|
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_.
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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 }) =>
|
|
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.
|
|
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,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 @@
|
|
|
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 @@
|
|
|
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 });
|