foldkit-primitives 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/device/index.d.mts +312 -0
- package/dist/device/index.d.mts.map +1 -0
- package/dist/device/index.mjs +490 -0
- package/dist/device/index.mjs.map +1 -0
- package/dist/dom/index.d.mts +73 -0
- package/dist/dom/index.d.mts.map +1 -0
- package/dist/dom/index.mjs +218 -0
- package/dist/dom/index.mjs.map +1 -0
- package/dist/events/index.d.mts +154 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +287 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +10 -0
- package/dist/media/index.d.mts +79 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +127 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/motion/index.d.mts +112 -0
- package/dist/motion/index.d.mts.map +1 -0
- package/dist/motion/index.mjs +208 -0
- package/dist/motion/index.mjs.map +1 -0
- package/dist/net/index.d.mts +199 -0
- package/dist/net/index.d.mts.map +1 -0
- package/dist/net/index.mjs +329 -0
- package/dist/net/index.mjs.map +1 -0
- package/dist/observers/index.d.mts +59 -0
- package/dist/observers/index.d.mts.map +1 -0
- package/dist/observers/index.mjs +156 -0
- package/dist/observers/index.mjs.map +1 -0
- package/dist/state/index.d.mts +342 -0
- package/dist/state/index.d.mts.map +1 -0
- package/dist/state/index.mjs +504 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/time/index.d.mts +129 -0
- package/dist/time/index.d.mts.map +1 -0
- package/dist/time/index.mjs +213 -0
- package/dist/time/index.mjs.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
4
|
+
//#region src/dom/autofocus.ts
|
|
5
|
+
/**
|
|
6
|
+
* Autofocus as a Mount: focuses the element on insert, then emits `Focused`.
|
|
7
|
+
* The message means focus was requested — a non-focusable element or a
|
|
8
|
+
* hidden tab may decline, and the browser does not report that. Plain
|
|
9
|
+
* `focus()`, no scroll options: scroll behavior stays the application's
|
|
10
|
+
* choice.
|
|
11
|
+
*/
|
|
12
|
+
const Focused = Schema.TaggedStruct("Focused", {});
|
|
13
|
+
const Autofocus = Mount.define("Autofocus", {
|
|
14
|
+
messages: [Focused],
|
|
15
|
+
execute: ({ element }) => Effect.sync(() => {
|
|
16
|
+
const focusable = element;
|
|
17
|
+
if (typeof focusable.focus === "function") focusable.focus.call(element);
|
|
18
|
+
return Focused.make({});
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/dom/clipboard.ts
|
|
23
|
+
/**
|
|
24
|
+
* Clipboard copy as a Command: runs in `update` beside any bundle, yields
|
|
25
|
+
* `Copied` on success and `CopyFailed` otherwise. No Model involved: the
|
|
26
|
+
* clipboard is not application state.
|
|
27
|
+
*/
|
|
28
|
+
const ClipboardMessage = defineMessageUnion({
|
|
29
|
+
Copied: {},
|
|
30
|
+
CopyFailed: { message: Schema.String }
|
|
31
|
+
});
|
|
32
|
+
const failMessage$2 = (error) => error instanceof Error ? error.message : String(error);
|
|
33
|
+
/**
|
|
34
|
+
* Copies text to the clipboard. Without a clipboard API (SSR, insecure
|
|
35
|
+
* context, denied permission) the command yields `CopyFailed` instead of
|
|
36
|
+
* throwing, so `update` handles both outcomes as Messages.
|
|
37
|
+
*/
|
|
38
|
+
const copyText = (text) => ({
|
|
39
|
+
name: "Clipboard.copy",
|
|
40
|
+
args: { text },
|
|
41
|
+
effect: typeof navigator !== "undefined" && typeof navigator.clipboard?.writeText === "function" ? Effect.matchEffect(Effect.tryPromise({
|
|
42
|
+
try: () => navigator.clipboard.writeText(text),
|
|
43
|
+
catch: failMessage$2
|
|
44
|
+
}), {
|
|
45
|
+
onFailure: (message) => Effect.succeed(ClipboardMessage.CopyFailed({ message })),
|
|
46
|
+
onSuccess: () => Effect.succeed(ClipboardMessage.Copied())
|
|
47
|
+
}) : Effect.succeed(ClipboardMessage.CopyFailed({ message: "clipboard is unavailable" }))
|
|
48
|
+
});
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/dom/input-mask.ts
|
|
51
|
+
/**
|
|
52
|
+
* An input mask as a Mount plus a pure mask function. The pattern language
|
|
53
|
+
* is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —
|
|
54
|
+
* with every other character literal. Literals appear only once a later placeholder
|
|
55
|
+
* consumes input, so partial values never dangle a separator. The Mount
|
|
56
|
+
* rewrites the field, restores the caret approximately, and emits `Input`
|
|
57
|
+
* with both the masked and raw values; the parent owns the state, like any
|
|
58
|
+
* controlled input.
|
|
59
|
+
*/
|
|
60
|
+
const Input = Schema.TaggedStruct("Input", {
|
|
61
|
+
value: Schema.String,
|
|
62
|
+
raw: Schema.String
|
|
63
|
+
});
|
|
64
|
+
const isDigit = (char) => char >= "0" && char <= "9";
|
|
65
|
+
const isLetter = (char) => char >= "a" && char <= "z" || char >= "A" && char <= "Z";
|
|
66
|
+
const matches = (placeholder, char) => placeholder === "#" ? isDigit(char) : placeholder === "A" ? isLetter(char) : isDigit(char) || isLetter(char);
|
|
67
|
+
const isPlaceholder = (char) => char === "#" || char === "A" || char === "*";
|
|
68
|
+
/** Whether any placeholder at or after `from` consumes any input at or after `at`. */
|
|
69
|
+
const fillable = (pattern, from, value, at) => {
|
|
70
|
+
for (let i = from; i < pattern.length; i++) {
|
|
71
|
+
const placeholder = pattern[i];
|
|
72
|
+
if (!isPlaceholder(placeholder)) continue;
|
|
73
|
+
for (let j = at; j < value.length; j++) if (matches(placeholder, value[j])) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
const applyMask = (value, pattern) => {
|
|
78
|
+
let masked = "";
|
|
79
|
+
let raw = "";
|
|
80
|
+
let at = 0;
|
|
81
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
82
|
+
const slot = pattern[i];
|
|
83
|
+
if (isPlaceholder(slot)) {
|
|
84
|
+
while (at < value.length && !matches(slot, value[at])) at++;
|
|
85
|
+
if (at >= value.length) break;
|
|
86
|
+
masked += value[at];
|
|
87
|
+
raw += value[at];
|
|
88
|
+
at++;
|
|
89
|
+
} else if (fillable(pattern, i + 1, value, at)) masked += slot;
|
|
90
|
+
else break;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
masked,
|
|
94
|
+
raw
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const asField = (element) => {
|
|
98
|
+
const field = element;
|
|
99
|
+
return typeof field.value === "string" && typeof field.addEventListener === "function" ? field : null;
|
|
100
|
+
};
|
|
101
|
+
const InputMask = Mount.defineStream("InputMask", {
|
|
102
|
+
messages: [Input],
|
|
103
|
+
args: { pattern: Schema.String },
|
|
104
|
+
execute: ({ element, pattern }) => Stream.callback((queue) => Effect.gen(function* () {
|
|
105
|
+
const field = asField(element);
|
|
106
|
+
if (field === null) return;
|
|
107
|
+
const onInput = () => {
|
|
108
|
+
const { masked, raw } = applyMask(field.value, pattern);
|
|
109
|
+
if (masked !== field.value) {
|
|
110
|
+
const caret = field.selectionStart ?? masked.length;
|
|
111
|
+
field.value = masked;
|
|
112
|
+
const at = Math.min(caret, masked.length);
|
|
113
|
+
try {
|
|
114
|
+
field.setSelectionRange?.(at, at);
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
Queue.offerUnsafe(queue, Input.make({
|
|
118
|
+
value: masked,
|
|
119
|
+
raw
|
|
120
|
+
}));
|
|
121
|
+
};
|
|
122
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
123
|
+
field.addEventListener("input", onInput);
|
|
124
|
+
return field;
|
|
125
|
+
}), (current) => Effect.sync(() => current.removeEventListener("input", onInput)));
|
|
126
|
+
}))
|
|
127
|
+
});
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/dom/script-loader.ts
|
|
130
|
+
/**
|
|
131
|
+
* Script loading as a Command: appends a `src` script to the head unless one
|
|
132
|
+
* is already there. Idempotent by URL — concurrent placements collapse onto
|
|
133
|
+
* the first tag instead of fetching twice. Yields `Loaded` on success and
|
|
134
|
+
* `LoadFailed` otherwise (network error, no document), so `update` handles
|
|
135
|
+
* both outcomes as Messages.
|
|
136
|
+
*/
|
|
137
|
+
const ScriptMessage = defineMessageUnion({
|
|
138
|
+
Loaded: {},
|
|
139
|
+
LoadFailed: { message: Schema.String }
|
|
140
|
+
});
|
|
141
|
+
const failMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
142
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
143
|
+
/** Loads a script by URL, once per URL. */
|
|
144
|
+
const loadScript = (src) => ({
|
|
145
|
+
name: "Script.load",
|
|
146
|
+
args: { src },
|
|
147
|
+
effect: Effect.matchEffect(Effect.tryPromise({
|
|
148
|
+
try: () => {
|
|
149
|
+
if (typeof document === "undefined") return Promise.reject(/* @__PURE__ */ new Error("script loading is unavailable"));
|
|
150
|
+
const running = inflight.get(src);
|
|
151
|
+
if (running !== void 0) return running;
|
|
152
|
+
if (Array.from(document.getElementsByTagName("script")).some((element) => element.getAttribute("src") === src)) return Promise.resolve();
|
|
153
|
+
const tracked = new Promise((resolve, reject) => {
|
|
154
|
+
const element = document.createElement("script");
|
|
155
|
+
element.src = src;
|
|
156
|
+
element.async = true;
|
|
157
|
+
element.onload = () => resolve();
|
|
158
|
+
element.onerror = () => {
|
|
159
|
+
element.remove();
|
|
160
|
+
reject(/* @__PURE__ */ new Error(`failed to load script: ${src}`));
|
|
161
|
+
};
|
|
162
|
+
document.head.appendChild(element);
|
|
163
|
+
}).then(() => {
|
|
164
|
+
inflight.delete(src);
|
|
165
|
+
}, (error) => {
|
|
166
|
+
inflight.delete(src);
|
|
167
|
+
throw error;
|
|
168
|
+
});
|
|
169
|
+
inflight.set(src, tracked);
|
|
170
|
+
return tracked;
|
|
171
|
+
},
|
|
172
|
+
catch: (error) => error
|
|
173
|
+
}), {
|
|
174
|
+
onFailure: (error) => Effect.succeed(ScriptMessage.LoadFailed({ message: failMessage$1(error) })),
|
|
175
|
+
onSuccess: () => Effect.succeed(ScriptMessage.Loaded())
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/dom/share.ts
|
|
180
|
+
/**
|
|
181
|
+
* Web Share as a Command: runs in `update` beside any bundle, yields
|
|
182
|
+
* `Shared` on success. Dismissal is its own outcome, not a failure — the
|
|
183
|
+
* application tells a cancelled sheet apart from a broken one. Without a
|
|
184
|
+
* share API (desktop, insecure context) it yields `ShareFailed` instead of
|
|
185
|
+
* throwing, so `update` handles every outcome as a Message.
|
|
186
|
+
*/
|
|
187
|
+
const ShareData = Schema.Struct({
|
|
188
|
+
title: Schema.optional(Schema.String),
|
|
189
|
+
text: Schema.optional(Schema.String),
|
|
190
|
+
url: Schema.optional(Schema.String)
|
|
191
|
+
});
|
|
192
|
+
const ShareMessage = defineMessageUnion({
|
|
193
|
+
Shared: {},
|
|
194
|
+
Dismissed: {},
|
|
195
|
+
ShareFailed: { message: Schema.String }
|
|
196
|
+
});
|
|
197
|
+
const failMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
198
|
+
const isDismissal = (error) => typeof error?.name === "string" && error.name === "AbortError";
|
|
199
|
+
/** Shares data through the platform sheet. */
|
|
200
|
+
const share = (data) => ({
|
|
201
|
+
name: "Share.share",
|
|
202
|
+
args: { data },
|
|
203
|
+
effect: typeof navigator !== "undefined" && typeof navigator.share === "function" ? Effect.matchEffect(Effect.tryPromise({
|
|
204
|
+
try: () => navigator.share({
|
|
205
|
+
...data.title !== void 0 ? { title: data.title } : {},
|
|
206
|
+
...data.text !== void 0 ? { text: data.text } : {},
|
|
207
|
+
...data.url !== void 0 ? { url: data.url } : {}
|
|
208
|
+
}),
|
|
209
|
+
catch: (error) => error
|
|
210
|
+
}), {
|
|
211
|
+
onFailure: (error) => Effect.succeed(isDismissal(error) ? ShareMessage.Dismissed() : ShareMessage.ShareFailed({ message: failMessage(error) })),
|
|
212
|
+
onSuccess: () => Effect.succeed(ShareMessage.Shared())
|
|
213
|
+
}) : Effect.succeed(ShareMessage.ShareFailed({ message: "share is unavailable" }))
|
|
214
|
+
});
|
|
215
|
+
//#endregion
|
|
216
|
+
export { Autofocus, ClipboardMessage, Focused, Input, InputMask, ScriptMessage, ShareData, ShareMessage, applyMask, copyText, loadScript, share };
|
|
217
|
+
|
|
218
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["failMessage","failMessage"],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts"],"sourcesContent":["/**\n * Autofocus as a Mount: focuses the element on insert, then emits `Focused`.\n * The message means focus was requested — a non-focusable element or a\n * hidden tab may decline, and the browser does not report that. Plain\n * `focus()`, no scroll options: scroll behavior stays the application's\n * choice.\n */\nimport { Effect, Schema } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Focused = Schema.TaggedStruct('Focused', {})\nexport type Focused = typeof Focused.Type\n\nexport const Autofocus = Mount.define('Autofocus', {\n messages: [Focused],\n execute: ({ element }) =>\n Effect.sync(() => {\n const focusable = element as unknown as { readonly focus?: unknown }\n if (typeof focusable.focus === 'function') {\n ;(focusable.focus as () => void).call(element)\n }\n return Focused.make({})\n }),\n})\n","/**\n * Clipboard copy as a Command: runs in `update` beside any bundle, yields\n * `Copied` on success and `CopyFailed` otherwise. No Model involved: the\n * clipboard is not application state.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ClipboardMessage = defineMessageUnion({\n Copied: {},\n CopyFailed: { message: Schema.String },\n})\nexport type ClipboardMessage = typeof ClipboardMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Copies text to the clipboard. Without a clipboard API (SSR, insecure\n * context, denied permission) the command yields `CopyFailed` instead of\n * throwing, so `update` handles both outcomes as Messages.\n */\nexport const copyText = (text: string): Command<ClipboardMessage, never, never> => ({\n name: 'Clipboard.copy',\n args: { text },\n effect:\n typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({\n try: () => navigator.clipboard.writeText(text),\n catch: failMessage,\n }),\n {\n onFailure: message => Effect.succeed(ClipboardMessage.CopyFailed({ message })),\n onSuccess: () => Effect.succeed(ClipboardMessage.Copied()),\n },\n )\n : Effect.succeed(ClipboardMessage.CopyFailed({ message: 'clipboard is unavailable' })),\n})\n","/**\n * An input mask as a Mount plus a pure mask function. The pattern language\n * is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —\n * with every other character literal. Literals appear only once a later placeholder\n * consumes input, so partial values never dangle a separator. The Mount\n * rewrites the field, restores the caret approximately, and emits `Input`\n * with both the masked and raw values; the parent owns the state, like any\n * controlled input.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Input = Schema.TaggedStruct('Input', {\n value: Schema.String,\n raw: Schema.String,\n})\nexport type Input = typeof Input.Type\n\nconst isDigit = (char: string): boolean => char >= '0' && char <= '9'\nconst isLetter = (char: string): boolean =>\n (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')\n\nconst matches = (placeholder: string, char: string): boolean =>\n placeholder === '#'\n ? isDigit(char)\n : placeholder === 'A'\n ? isLetter(char)\n : isDigit(char) || isLetter(char)\n\nconst isPlaceholder = (char: string): boolean => char === '#' || char === 'A' || char === '*'\n\n/** Whether any placeholder at or after `from` consumes any input at or after `at`. */\nconst fillable = (pattern: string, from: number, value: string, at: number): boolean => {\n for (let i = from; i < pattern.length; i++) {\n const placeholder = pattern[i]!\n if (!isPlaceholder(placeholder)) continue\n for (let j = at; j < value.length; j++) {\n if (matches(placeholder, value[j]!)) return true\n }\n }\n return false\n}\n\nexport const applyMask = (\n value: string,\n pattern: string,\n): { readonly masked: string; readonly raw: string } => {\n let masked = ''\n let raw = ''\n let at = 0\n for (let i = 0; i < pattern.length; i++) {\n const slot = pattern[i]!\n if (isPlaceholder(slot)) {\n while (at < value.length && !matches(slot, value[at]!)) at++\n if (at >= value.length) break\n masked += value[at]!\n raw += value[at]!\n at++\n } else if (fillable(pattern, i + 1, value, at)) {\n masked += slot\n } else {\n break\n }\n }\n return { masked, raw }\n}\n\ninterface MaskableField {\n value: string\n selectionStart: number | null\n addEventListener(type: string, listener: () => void): void\n removeEventListener(type: string, listener: () => void): void\n setSelectionRange?(start: number, end: number): void\n}\n\nconst asField = (element: Element): MaskableField | null => {\n const field = element as unknown as Partial<MaskableField>\n return typeof field.value === 'string' && typeof field.addEventListener === 'function'\n ? (field as MaskableField)\n : null\n}\n\nexport const InputMask = Mount.defineStream('InputMask', {\n messages: [Input],\n args: { pattern: Schema.String },\n execute: ({ element, pattern }) =>\n Stream.callback<typeof Input.Type>(queue =>\n Effect.gen(function* () {\n const field = asField(element)\n if (field === null) return\n const onInput = () => {\n const { masked, raw } = applyMask(field.value, pattern)\n if (masked !== field.value) {\n const caret = field.selectionStart ?? masked.length\n field.value = masked\n const at = Math.min(caret, masked.length)\n try {\n field.setSelectionRange?.(at, at)\n } catch {\n // Select-all style inputs reject ranges; the value still stands.\n }\n }\n Queue.offerUnsafe(queue, Input.make({ value: masked, raw }))\n }\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n field.addEventListener('input', onInput)\n return field\n }),\n current => Effect.sync(() => current.removeEventListener('input', onInput)),\n )\n }),\n ),\n})\n","/**\n * Script loading as a Command: appends a `src` script to the head unless one\n * is already there. Idempotent by URL — concurrent placements collapse onto\n * the first tag instead of fetching twice. Yields `Loaded` on success and\n * `LoadFailed` otherwise (network error, no document), so `update` handles\n * both outcomes as Messages.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ScriptMessage = defineMessageUnion({\n Loaded: {},\n LoadFailed: { message: Schema.String },\n})\nexport type ScriptMessage = typeof ScriptMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n// Tags still fetching, by URL: concurrent loads share one tag and its fate.\nconst inflight = new Map<string, Promise<void>>()\n\n/** Loads a script by URL, once per URL. */\nexport const loadScript = (src: string): Command<ScriptMessage, never, never> => ({\n name: 'Script.load',\n args: { src },\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n if (typeof document === 'undefined') {\n return Promise.reject(new Error('script loading is unavailable'))\n }\n // Concurrent loads share one tag and its fate, instead of racing\n // past the presence check and fetching twice: the in-flight entry\n // is checked before presence, in the same synchronous block that\n // appends, so a second load cannot slip between them.\n const running = inflight.get(src)\n if (running !== undefined) return running\n // getAttribute, not .src: the property resolves to an absolute URL\n // and would never equal the given string.\n const present = Array.from(document.getElementsByTagName('script')).some(\n element => element.getAttribute('src') === src,\n )\n if (present) return Promise.resolve()\n const completion = new Promise<void>((resolve, reject) => {\n const element = document.createElement('script')\n element.src = src\n element.async = true\n element.onload = () => resolve()\n element.onerror = () => {\n // A dead tag must not satisfy later presence checks.\n element.remove()\n reject(new Error(`failed to load script: ${src}`))\n }\n document.head.appendChild(element)\n })\n const tracked = completion.then(\n () => {\n inflight.delete(src)\n },\n (error: unknown) => {\n inflight.delete(src)\n throw error\n },\n )\n inflight.set(src, tracked)\n return tracked\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(ScriptMessage.LoadFailed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(ScriptMessage.Loaded()),\n },\n ),\n})\n","/**\n * Web Share as a Command: runs in `update` beside any bundle, yields\n * `Shared` on success. Dismissal is its own outcome, not a failure — the\n * application tells a cancelled sheet apart from a broken one. Without a\n * share API (desktop, insecure context) it yields `ShareFailed` instead of\n * throwing, so `update` handles every outcome as a Message.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ShareData = Schema.Struct({\n title: Schema.optional(Schema.String),\n text: Schema.optional(Schema.String),\n url: Schema.optional(Schema.String),\n})\nexport type ShareData = typeof ShareData.Type\n\nexport const ShareMessage = defineMessageUnion({\n Shared: {},\n Dismissed: {},\n ShareFailed: { message: Schema.String },\n})\nexport type ShareMessage = typeof ShareMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDismissal = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n (error as { readonly name: string }).name === 'AbortError'\n\n/** Shares data through the platform sheet. */\nexport const share = (data: ShareData): Command<ShareMessage, never, never> => ({\n name: 'Share.share',\n args: { data },\n effect:\n typeof navigator !== 'undefined' && typeof navigator.share === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({\n // Omit absent fields: the platform type rejects explicit\n // undefined under exactOptionalPropertyTypes.\n try: () =>\n navigator.share({\n ...(data.title !== undefined ? { title: data.title } : {}),\n ...(data.text !== undefined ? { text: data.text } : {}),\n ...(data.url !== undefined ? { url: data.url } : {}),\n }),\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error =>\n Effect.succeed(\n isDismissal(error)\n ? ShareMessage.Dismissed()\n : ShareMessage.ShareFailed({ message: failMessage(error) }),\n ),\n onSuccess: () => Effect.succeed(ShareMessage.Shared()),\n },\n )\n : Effect.succeed(ShareMessage.ShareFailed({ message: 'share is unavailable' })),\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Schema, Stream } from "effect";
|
|
2
|
+
import { Bundle } from "foldkit-bundle";
|
|
3
|
+
//#region src/events/active-element.d.ts
|
|
4
|
+
export declare const ActiveElementMessage: import("foldkit/message").MessageUnion<{
|
|
5
|
+
readonly Changed: {
|
|
6
|
+
readonly tag: Schema.String;
|
|
7
|
+
readonly id: Schema.NullOr<Schema.String>;
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
export type ActiveElementMessage = typeof ActiveElementMessage.Type;
|
|
11
|
+
/**
|
|
12
|
+
* Focus changes on the document. Without a document the stream is empty
|
|
13
|
+
* instead of throwing. Lift with `Subscription.persistent`, mapping into
|
|
14
|
+
* the parent's Message.
|
|
15
|
+
*/
|
|
16
|
+
export declare const activeElementEvents: () => Stream.Stream<ActiveElementMessage>;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/events/hotkeys.d.ts
|
|
19
|
+
/**
|
|
20
|
+
* Hotkey matching as a pure function over a `Pressed` payload: `matchHotkey`
|
|
21
|
+
* answers whether a press is the shortcut, so `update` stays a table of
|
|
22
|
+
* chords instead of a nest of modifier checks. Patterns read like
|
|
23
|
+
* `"ctrl+shift+k"`: modifiers first (any order), the key last,
|
|
24
|
+
* case-insensitive, with `cmd`/`command` and `option` as the Mac names for
|
|
25
|
+
* meta and alt. Matching is exact — unlisted modifiers must be up — and
|
|
26
|
+
* auto-repeat never matches, so holding a chord fires once. `space` (or a
|
|
27
|
+
* bare space) means the space key; `esc` means escape. A pattern with
|
|
28
|
+
* no key, or two keys, matches nothing: predicates return false, they do
|
|
29
|
+
* not throw on the hot path.
|
|
30
|
+
*/
|
|
31
|
+
export interface KeyPress {
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly repeat: boolean;
|
|
34
|
+
readonly ctrl: boolean;
|
|
35
|
+
readonly shift: boolean;
|
|
36
|
+
readonly alt: boolean;
|
|
37
|
+
readonly meta: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const matchHotkey: (pattern: string, press: KeyPress) => boolean;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/events/idle.d.ts
|
|
42
|
+
export declare const IdleModel: Schema.Struct<{
|
|
43
|
+
readonly idle: Schema.Boolean;
|
|
44
|
+
}>;
|
|
45
|
+
export type IdleModel = typeof IdleModel.Type;
|
|
46
|
+
export declare const IdleMessage: import("foldkit/message").MessageUnion<{
|
|
47
|
+
readonly BecameIdle: {};
|
|
48
|
+
readonly BecameActive: {};
|
|
49
|
+
}>;
|
|
50
|
+
export type IdleMessage = typeof IdleMessage.Type;
|
|
51
|
+
export declare const Idle: Bundle.Bundle<"Idle", {
|
|
52
|
+
readonly timeoutMs: number;
|
|
53
|
+
}, {
|
|
54
|
+
idle: boolean;
|
|
55
|
+
}, {
|
|
56
|
+
readonly _tag: "BecameIdle";
|
|
57
|
+
} | {
|
|
58
|
+
readonly _tag: "BecameActive";
|
|
59
|
+
}, never, never, never, void, {}, {}>;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/events/keyboard.d.ts
|
|
62
|
+
export declare const KeyboardMessage: import("foldkit/message").MessageUnion<{
|
|
63
|
+
readonly Pressed: {
|
|
64
|
+
readonly key: Schema.String;
|
|
65
|
+
readonly repeat: Schema.Boolean;
|
|
66
|
+
readonly ctrl: Schema.Boolean;
|
|
67
|
+
readonly shift: Schema.Boolean;
|
|
68
|
+
readonly alt: Schema.Boolean;
|
|
69
|
+
readonly meta: Schema.Boolean;
|
|
70
|
+
};
|
|
71
|
+
readonly Released: {
|
|
72
|
+
readonly key: Schema.String;
|
|
73
|
+
};
|
|
74
|
+
}>;
|
|
75
|
+
export type KeyboardMessage = typeof KeyboardMessage.Type;
|
|
76
|
+
/**
|
|
77
|
+
* Key presses and releases on the window. Without a window the stream is
|
|
78
|
+
* empty instead of throwing. Lift with `Subscription.persistent`, mapping
|
|
79
|
+
* into the parent's Message.
|
|
80
|
+
*/
|
|
81
|
+
export declare const keyboardEvents: () => Stream.Stream<KeyboardMessage>;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/events/pointer.d.ts
|
|
84
|
+
export declare const PointerMessage: import("foldkit/message").MessageUnion<{
|
|
85
|
+
readonly Moved: {
|
|
86
|
+
readonly x: Schema.Number;
|
|
87
|
+
readonly y: Schema.Number;
|
|
88
|
+
};
|
|
89
|
+
}>;
|
|
90
|
+
export type PointerMessage = typeof PointerMessage.Type;
|
|
91
|
+
/**
|
|
92
|
+
* Pointer moves on the window. Without a window the stream is empty instead
|
|
93
|
+
* of throwing. Lift with `Subscription.persistent`, mapping into the
|
|
94
|
+
* parent's Message.
|
|
95
|
+
*/
|
|
96
|
+
export declare const pointerEvents: () => Stream.Stream<PointerMessage>;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/events/scroll.d.ts
|
|
99
|
+
export declare const ScrollMessage: import("foldkit/message").MessageUnion<{
|
|
100
|
+
readonly Scrolled: {
|
|
101
|
+
readonly x: Schema.Number;
|
|
102
|
+
readonly y: Schema.Number;
|
|
103
|
+
};
|
|
104
|
+
}>;
|
|
105
|
+
export type ScrollMessage = typeof ScrollMessage.Type;
|
|
106
|
+
/**
|
|
107
|
+
* Scrolls on the window, in the capture phase so container scrolls (which
|
|
108
|
+
* do not bubble) report too. Without a window the stream is empty instead
|
|
109
|
+
* of throwing. Lift with `Subscription.persistent`, mapping into the
|
|
110
|
+
* parent's Message.
|
|
111
|
+
*/
|
|
112
|
+
export declare const scrollEvents: () => Stream.Stream<ScrollMessage>;
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/events/visibility.d.ts
|
|
115
|
+
export declare const VisibilityModel: Schema.Struct<{
|
|
116
|
+
readonly visible: Schema.Boolean;
|
|
117
|
+
}>;
|
|
118
|
+
export type VisibilityModel = typeof VisibilityModel.Type;
|
|
119
|
+
export declare const VisibilityMessage: import("foldkit/message").MessageUnion<{
|
|
120
|
+
readonly Changed: {
|
|
121
|
+
readonly visible: Schema.Boolean;
|
|
122
|
+
};
|
|
123
|
+
}>;
|
|
124
|
+
export type VisibilityMessage = typeof VisibilityMessage.Type;
|
|
125
|
+
export declare const Visibility: Bundle.Bundle<"Visibility", void, {
|
|
126
|
+
visible: boolean;
|
|
127
|
+
}, {
|
|
128
|
+
readonly _tag: "Changed";
|
|
129
|
+
readonly visible: boolean;
|
|
130
|
+
}, never, never, never, void, {}, {}>;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/events/window-size.d.ts
|
|
133
|
+
export declare const WindowSizeModel: Schema.Struct<{
|
|
134
|
+
readonly width: Schema.Number;
|
|
135
|
+
readonly height: Schema.Number;
|
|
136
|
+
}>;
|
|
137
|
+
export type WindowSizeModel = typeof WindowSizeModel.Type;
|
|
138
|
+
export declare const WindowSizeMessage: import("foldkit/message").MessageUnion<{
|
|
139
|
+
readonly Changed: {
|
|
140
|
+
readonly width: Schema.Number;
|
|
141
|
+
readonly height: Schema.Number;
|
|
142
|
+
};
|
|
143
|
+
}>;
|
|
144
|
+
export type WindowSizeMessage = typeof WindowSizeMessage.Type;
|
|
145
|
+
export declare const WindowSize: Bundle.Bundle<"WindowSize", void, {
|
|
146
|
+
width: number;
|
|
147
|
+
height: number;
|
|
148
|
+
}, {
|
|
149
|
+
readonly _tag: "Changed";
|
|
150
|
+
readonly width: number;
|
|
151
|
+
readonly height: number;
|
|
152
|
+
}, never, never, never, void, {}, {}>;
|
|
153
|
+
//#endregion
|
|
154
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/events/active-element.ts","../../src/events/hotkeys.ts","../../src/events/idle.ts","../../src/events/keyboard.ts","../../src/events/pointer.ts","../../src/events/scroll.ts","../../src/events/visibility.ts","../../src/events/window-size.ts"],"mappings":";;;qBASa,gDAAoB;;;;;;YAGrB,8BAA8B,qBAAqB;;;;;;qBAOlD,2BAA0B,OAAO,OAAO;;;;;;;;;;;;;;;iBCNpC;WACN;WACA;WACA;WACA;WACA;WACA;;qBAkCE,cAAW,iBAAmB,OAAS;;;qBCvCvC,WAAS,OAAA;;;YACV,mBAAmB,UAAU;qBAE5B,uCAAW;;;;YACZ,qBAAqB,YAAY;qBAchC,MAAI,OAAA;;;;;;;;;;;qBCvBJ,2CAAe;;;;;;;;;;;;;YAWhB,yBAAyB,gBAAgB;;;;;;qBAOxC,sBAAqB,OAAO,OAAO;;;qBClBnC,0CAAc;;;;;;YAGf,wBAAwB,eAAe;;;;;;qBAOtC,qBAAoB,OAAO,OAAO;;;qBCVlC,yCAAa;;;;;;YAGd,uBAAuB,cAAc;;;;;;;qBAQpC,oBAAmB,OAAO,OAAO;;;qBCTjC,iBAAe,OAAA;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;sBAAjB,OAAA;;;YACD,2BAA2B,kBAAkB;qBAiB5C,YAAU,OAAA;;;;;;;;qBCpBV,iBAAe,OAAA;;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;;;;;YAGlB,2BAA2B,kBAAkB;qBAmC5C,YAAU,OAAA"}
|