foldkit 0.57.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/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 +31 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +40 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/package.json +5 -1
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
|
|
|
@@ -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 });
|
package/dist/html/index.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { Data, Effect, Option } from 'effect';
|
|
2
|
+
import type { File } from '../file';
|
|
2
3
|
import { Dispatch } from '../runtime';
|
|
3
4
|
import { VNode } from '../vdom';
|
|
4
5
|
export { createKeyedLazy, createLazy } from './lazy';
|
|
6
|
+
/**
|
|
7
|
+
* Tag symbol attached to file-aware event handler functions so Scene test
|
|
8
|
+
* helpers can distinguish `OnFileChange` from `OnChange` (both register on
|
|
9
|
+
* the DOM `change` event) and `OnDropFiles` from `OnDrop` (both register on
|
|
10
|
+
* the DOM `drop` event). Internal implementation detail — consumer code
|
|
11
|
+
* should never need to reference this directly.
|
|
12
|
+
*/
|
|
13
|
+
export declare const FileHandlerSymbol: unique symbol;
|
|
14
|
+
/** Type-level brand for file-aware event handler tags. */
|
|
15
|
+
export type FileHandlerSymbol = typeof FileHandlerSymbol;
|
|
5
16
|
/** Modifier key state extracted from a `KeyboardEvent`. */
|
|
6
17
|
export type KeyboardModifiers = Readonly<{
|
|
7
18
|
shiftKey: boolean;
|
|
@@ -130,6 +141,9 @@ export type Attribute<Message> = Data.TaggedEnum<{
|
|
|
130
141
|
OnChange: {
|
|
131
142
|
readonly f: (value: string) => Message;
|
|
132
143
|
};
|
|
144
|
+
OnFileChange: {
|
|
145
|
+
readonly f: (files: ReadonlyArray<File>) => Message;
|
|
146
|
+
};
|
|
133
147
|
OnSubmit: {
|
|
134
148
|
readonly message: Message;
|
|
135
149
|
};
|
|
@@ -181,6 +195,9 @@ export type Attribute<Message> = Data.TaggedEnum<{
|
|
|
181
195
|
OnDrop: {
|
|
182
196
|
readonly message: Message;
|
|
183
197
|
};
|
|
198
|
+
OnDropFiles: {
|
|
199
|
+
readonly f: (files: ReadonlyArray<File>) => Message;
|
|
200
|
+
};
|
|
184
201
|
OnTouchStart: {
|
|
185
202
|
readonly message: Message;
|
|
186
203
|
};
|
|
@@ -878,6 +895,9 @@ export declare const html: <Message = never>() => {
|
|
|
878
895
|
} | {
|
|
879
896
|
readonly _tag: "OnChange";
|
|
880
897
|
readonly f: (value: string) => Message;
|
|
898
|
+
} | {
|
|
899
|
+
readonly _tag: "OnFileChange";
|
|
900
|
+
readonly f: (files: ReadonlyArray<File>) => Message;
|
|
881
901
|
} | {
|
|
882
902
|
readonly _tag: "OnSubmit";
|
|
883
903
|
readonly message: Message;
|
|
@@ -929,6 +949,9 @@ export declare const html: <Message = never>() => {
|
|
|
929
949
|
} | {
|
|
930
950
|
readonly _tag: "OnDrop";
|
|
931
951
|
readonly message: Message;
|
|
952
|
+
} | {
|
|
953
|
+
readonly _tag: "OnDropFiles";
|
|
954
|
+
readonly f: (files: ReadonlyArray<File>) => Message;
|
|
932
955
|
} | {
|
|
933
956
|
readonly _tag: "OnTouchStart";
|
|
934
957
|
readonly message: Message;
|
|
@@ -1668,6 +1691,10 @@ export declare const html: <Message = never>() => {
|
|
|
1668
1691
|
readonly _tag: "OnChange";
|
|
1669
1692
|
readonly f: (value: string) => Message;
|
|
1670
1693
|
};
|
|
1694
|
+
OnFileChange: (f: (files: ReadonlyArray<File>) => Message) => {
|
|
1695
|
+
readonly _tag: "OnFileChange";
|
|
1696
|
+
readonly f: (files: ReadonlyArray<File>) => Message;
|
|
1697
|
+
};
|
|
1671
1698
|
OnSubmit: (message: Message) => {
|
|
1672
1699
|
readonly _tag: "OnSubmit";
|
|
1673
1700
|
readonly message: Message;
|
|
@@ -1736,6 +1763,10 @@ export declare const html: <Message = never>() => {
|
|
|
1736
1763
|
readonly _tag: "OnDrop";
|
|
1737
1764
|
readonly message: Message;
|
|
1738
1765
|
};
|
|
1766
|
+
OnDropFiles: (f: (files: ReadonlyArray<File>) => Message) => {
|
|
1767
|
+
readonly _tag: "OnDropFiles";
|
|
1768
|
+
readonly f: (files: ReadonlyArray<File>) => Message;
|
|
1769
|
+
};
|
|
1739
1770
|
OnTouchStart: (message: Message) => {
|
|
1740
1771
|
readonly _tag: "OnTouchStart";
|
|
1741
1772
|
readonly message: Message;
|