foldkit-primitives 0.8.0 → 0.10.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 +32 -2
- package/dist/device/index.d.mts +66 -33
- package/dist/device/index.d.mts.map +1 -1
- package/dist/device/index.mjs +34 -34
- package/dist/device/index.mjs.map +1 -1
- package/dist/dom/index.d.mts +78 -18
- package/dist/dom/index.d.mts.map +1 -1
- package/dist/dom/index.mjs +56 -22
- package/dist/dom/index.mjs.map +1 -1
- package/dist/{editable-text-BbsPOATY.d.mts → editable-text-Dvm-Fuqs.d.mts} +65 -3
- package/dist/editable-text-Dvm-Fuqs.d.mts.map +1 -0
- package/dist/events/index.d.mts +52 -31
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +40 -28
- package/dist/events/index.mjs.map +1 -1
- package/dist/{follow-tab-stop-BDxyoUPQ.mjs → follow-tab-stop-Ci1tqAIW.mjs} +63 -61
- package/dist/follow-tab-stop-Ci1tqAIW.mjs.map +1 -0
- package/dist/{index-Cppf-lBx.d.mts → index-BC8kcar9.d.mts} +35 -16
- package/dist/index-BC8kcar9.d.mts.map +1 -0
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +5 -5
- package/dist/{input-modality-m6v7__Xs.mjs → input-modality-BXD383jg.mjs} +9 -9
- package/dist/input-modality-BXD383jg.mjs.map +1 -0
- package/dist/{input-modality-DGOhAknO.d.mts → input-modality-BY-bSrEc.d.mts} +9 -2
- package/dist/input-modality-BY-bSrEc.d.mts.map +1 -0
- package/dist/interaction/index.d.mts +118 -118
- package/dist/interaction/index.d.mts.map +1 -1
- package/dist/interaction/index.mjs +3 -3
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/media/index.d.mts +24 -3
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +23 -13
- package/dist/media/index.mjs.map +1 -1
- package/dist/motion/index.d.mts +1 -1
- package/dist/motion/index.mjs +1 -2
- package/dist/{motion-DGGBZ0wt.mjs → motion-DmFDppRn.mjs} +20 -20
- package/dist/{motion-DGGBZ0wt.mjs.map → motion-DmFDppRn.mjs.map} +1 -1
- package/dist/net/index.d.mts +57 -19
- package/dist/net/index.d.mts.map +1 -1
- package/dist/net/index.mjs +31 -31
- package/dist/net/index.mjs.map +1 -1
- package/dist/observers/index.d.mts +25 -0
- package/dist/observers/index.d.mts.map +1 -1
- package/dist/observers/index.mjs +22 -22
- package/dist/observers/index.mjs.map +1 -1
- package/dist/state/index.d.mts +98 -44
- package/dist/state/index.d.mts.map +1 -1
- package/dist/state/index.mjs +39 -39
- package/dist/state/index.mjs.map +1 -1
- package/dist/time/index.d.mts +35 -5
- package/dist/time/index.d.mts.map +1 -1
- package/dist/time/index.mjs +20 -20
- package/dist/time/index.mjs.map +1 -1
- package/package.json +14 -14
- package/dist/editable-text-BbsPOATY.d.mts.map +0 -1
- package/dist/follow-tab-stop-BDxyoUPQ.mjs.map +0 -1
- package/dist/index-Cppf-lBx.d.mts.map +0 -1
- package/dist/input-modality-DGOhAknO.d.mts.map +0 -1
- package/dist/input-modality-m6v7__Xs.mjs.map +0 -1
package/dist/dom/index.mjs.map
CHANGED
|
@@ -1 +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/keep-in-view.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts","../../src/dom/measure.ts","../../src/dom/scroll.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 * The clipboard as Commands: run in `update` beside any bundle. A copy yields\n * `Copied` or `CopyFailed`, a read `Read` or `ReadFailed`. No Model involved:\n * the 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\nexport const ClipboardReadMessage = defineMessageUnion({\n Read: { text: Schema.String },\n ReadFailed: { message: Schema.String },\n})\nexport type ClipboardReadMessage = typeof ClipboardReadMessage.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/**\n * Reads the clipboard's text. The browser may ask the user first; a refusal,\n * an insecure context or no clipboard API yields `ReadFailed`.\n */\nexport const readText = (): Command<ClipboardReadMessage, never, never> => ({\n name: 'Clipboard.read',\n effect:\n typeof navigator !== 'undefined' && typeof navigator.clipboard?.readText === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({ try: () => navigator.clipboard.readText(), catch: failMessage }),\n {\n onFailure: message => Effect.succeed(ClipboardReadMessage.ReadFailed({ message })),\n onSuccess: text => Effect.succeed(ClipboardReadMessage.Read({ text })),\n },\n )\n : Effect.succeed(ClipboardReadMessage.ReadFailed({ 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 * Keeps what is marked in view as a Mount: whenever an element in the mounted\n * one's subtree newly matches `selector`, such as the row just selected in a\n * list, it is scrolled into view the least amount that shows it (`nearest`).\n * The element keeps its identity and its focus: nothing is redrawn to move it.\n *\n * It watches with a `MutationObserver` for as long as it is mounted, which is as\n * long as the list or canvas that can change what is marked.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const KeepInView = Mount.defineStream('KeepInView', {\n messages: [Schema.Never],\n args: {\n /** What is marked, as a selector within the element: `[aria-selected=\"true\"]`. */\n selector: Schema.String,\n },\n execute: ({ element, selector }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n let marked = element.querySelector(selector)\n const observer = new MutationObserver(() => {\n const now = element.querySelector(selector)\n // Only a change of what is marked scrolls: the author's own scrolling stays.\n // A DOM with no layout (jsdom, in a test) has no scrollIntoView and nothing to scroll.\n if (now !== null && now !== marked && typeof now.scrollIntoView === 'function')\n now.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n marked = now\n })\n observer.observe(element, { subtree: true, childList: true, attributes: true })\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\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","/**\n * Measures marked elements as a Mount, for a view to draw something over them,\n * such as an editor's selection outline, with no Message and no Model: where\n * an element is, is presentation, known to the DOM and to no transition.\n *\n * For each named target, the first element in the mounted one's subtree that\n * matches its selector is measured relative to the mounted element's scroll\n * box, which is what an absolutely placed child of it is laid out in. The\n * mounted element gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, and\n * `--fk-<name>-display`: `block` while a target is found, `none` while none is.\n *\n * It measures again when the subtree changes (a selection moves), in the\n * frame the change is drawn in; and, at most once a frame, when the mounted\n * element scrolls, the window resizes, the mounted element or a measured one\n * changes size, an image or a font inside loads, and on every frame of a\n * transition or an animation inside. What moves a target by any other way,\n * such as a stylesheet added that shifts it without resizing it, is seen at\n * the next of these.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/** The custom properties `Measure` writes for a target named `name`. */\nexport const measured = (name: string) =>\n ({\n x: `--fk-${name}-x`,\n y: `--fk-${name}-y`,\n w: `--fk-${name}-w`,\n h: `--fk-${name}-h`,\n display: `--fk-${name}-display`,\n }) as const\n\nexport const Measure = Mount.defineStream('Measure', {\n messages: [Schema.Never],\n args: {\n /** Each target by name, as a selector within the element: `{ selected: '[aria-selected=\"true\"]' }`. */\n targets: Schema.Record(Schema.String, Schema.String),\n },\n execute: ({ element, targets }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const container = element as HTMLElement\n const names = Object.keys(targets)\n let watched: ReadonlyArray<Element> = []\n let frame: number | undefined\n // Whether a transition or an animation may be moving something, until one frame finds none.\n let animating = false\n\n const measure = () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n const box = container.getBoundingClientRect()\n const found: Array<Element> = []\n for (const name of names) {\n const property = measured(name)\n const target = container.querySelector(targets[name]!)\n if (target === null) {\n container.style.setProperty(property.display, 'none')\n continue\n }\n found.push(target)\n const rect = target.getBoundingClientRect()\n // Relative to the scroll box: where the content is, not where the viewport shows it.\n const x = rect.left - box.left - container.clientLeft + container.scrollLeft\n const y = rect.top - box.top - container.clientTop + container.scrollTop\n container.style.setProperty(property.x, `${x}px`)\n container.style.setProperty(property.y, `${y}px`)\n container.style.setProperty(property.w, `${rect.width}px`)\n container.style.setProperty(property.h, `${rect.height}px`)\n container.style.setProperty(property.display, 'block')\n }\n // A measured element that changes size, as an image loading does, is measured again.\n if (found.length !== watched.length || found.some((each, at) => each !== watched[at])) {\n for (const each of watched) sizes.unobserve(each)\n for (const each of found) sizes.observe(each)\n watched = found\n }\n if (animating) {\n // Asked each frame rather than counted: an element removed mid-transition ends none.\n animating = container\n .getAnimations({ subtree: true })\n .some(animation => animation.playState === 'running')\n if (animating) soon()\n }\n }\n const soon = () => {\n if (frame === undefined) frame = requestAnimationFrame(measure)\n }\n const moving = () => {\n animating = true\n soon()\n }\n\n // A change is measured as its records arrive, before the frame it is drawn in is\n // painted, so a box never shows one frame on the element it has left.\n const changes = new MutationObserver(measure)\n // Made, and the fonts listened to, before anything observes: where one is\n // missing (jsdom has neither a ResizeObserver nor `document.fonts`), the Mount\n // fails with nothing attached to call `measure`.\n const sizes = new ResizeObserver(soon)\n document.fonts.addEventListener('loadingdone', soon)\n // Its own properties change the container's style too, but writing the same\n // values again records nothing, so a measure that changed nothing rests.\n changes.observe(container, { subtree: true, childList: true, attributes: true })\n sizes.observe(container)\n container.addEventListener('scroll', soon, { passive: true })\n window.addEventListener('resize', soon)\n // `load` does not bubble, so it is caught on the way down.\n container.addEventListener('load', soon, { capture: true })\n container.addEventListener('transitionrun', moving)\n container.addEventListener('animationstart', moving)\n measure()\n return () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n changes.disconnect()\n sizes.disconnect()\n container.removeEventListener('scroll', soon)\n window.removeEventListener('resize', soon)\n container.removeEventListener('load', soon, { capture: true })\n document.fonts.removeEventListener('loadingdone', soon)\n container.removeEventListener('transitionrun', moving)\n container.removeEventListener('animationstart', moving)\n }\n }),\n stop => Effect.sync(stop),\n ),\n ),\n})\n","/**\n * Where the window is scrolled, across an application's own navigations, as\n * a stream. Left to itself, a browser keeps the scroll of the entry it\n * leaves when a link pushes a new one, and on Back restores the old offset\n * before the application has drawn the entry it returns to, so the offset is\n * clamped to a page still loading. Here a pushed entry starts at the top,\n * and Back, Forward or a reload return where that entry was, once the page\n * is tall enough to hold it.\n *\n * Three rules, each found the slow way:\n *\n * 1. The offset is taken when the reader acts. The next screen is drawn\n * before a Navigate Command pushes the address, so by the time the URL\n * changes the window is already clamped to the new, shorter page. The\n * scroll is recorded at the reader's click or key (capture phase), and at\n * the start of Back and Forward, which begin in the browser before\n * anything is drawn.\n * 2. A restore holds its place while the next screen settles. A restore made\n * when the entry changes is undone by the loading state drawn right after\n * it, so the target is held on each frame for a moment, until the reader\n * scrolls, touches or types, or another navigation starts.\n * 3. Entries are keyed by the Navigation API's entry key, which a reload and\n * a full navigation keep; offsets rest in `sessionStorage`, so Back into\n * another document restores too. Where the browser has no Navigation API\n * the stream attaches nothing and leaves the browser's own behaviour\n * alone.\n *\n * Lift with `Subscription.persistent`; the stream sends no Messages. Only\n * the window scrolls: a nested container is the route graph's to know, when\n * `Site.transition` arrives.\n */\nimport { Effect, Option, Stream } from 'effect'\n\n/** The part of the Navigation API used here: TypeScript's DOM library does not declare it yet. */\ninterface Navigation extends EventTarget {\n readonly currentEntry: { readonly key: string } | null\n}\ndeclare global {\n interface Window {\n readonly navigation?: Navigation\n }\n}\n\nexport interface KeepScrollOptions {\n /** The `sessionStorage` key holding entry offsets. Default `'foldkit:scroll'`. */\n readonly storageKey?: string | undefined\n /** Entries remembered at most: the oldest are forgotten first. Default 50. */\n readonly kept?: number | undefined\n /**\n * How long a restore holds its place while the next screen settles, in\n * milliseconds. Default 2000.\n */\n readonly patience?: number | undefined\n}\n\nconst navigationTypeOf = (event: Event): Option.Option<unknown> =>\n 'navigationType' in event ? Option.some(event.navigationType) : Option.none()\n\nconst readSaved = (storageKey: string): Map<string, number> => {\n try {\n const stored: unknown = JSON.parse(sessionStorage.getItem(storageKey) ?? '[]')\n return new Map(\n Array.isArray(stored)\n ? stored.filter(\n (pair): pair is [string, number] =>\n Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'number',\n )\n : [],\n )\n } catch {\n // Storage blocked, or something else wrote the key: nothing is remembered.\n return new Map()\n }\n}\n\n/** Starts keeping the scroll; the release undoes everything it installed. */\nconst attach = (storageKey: string, kept: number, patience: number): (() => void) => {\n const navigation = window.navigation\n if (navigation === undefined) return () => {}\n const restoration = window.history.scrollRestoration\n window.history.scrollRestoration = 'manual'\n const saved = readSaved(storageKey)\n const detaches: Array<() => void> = []\n const on = (target: EventTarget, type: string, listener: EventListener, capture?: boolean) => {\n target.addEventListener(type, listener, capture)\n detaches.push(() => target.removeEventListener(type, listener, capture))\n }\n const keyOf = () =>\n Option.fromNullOr(navigation.currentEntry).pipe(Option.map(entry => entry.key))\n /** Where the current entry was left, if it has been. */\n const savedHere = () => Option.flatMap(keyOf(), key => Option.fromUndefinedOr(saved.get(key)))\n const remember = () => {\n const key = keyOf()\n if (Option.isNone(key)) return\n // Moved to the end, so it is the last to be forgotten.\n saved.delete(key.value)\n saved.set(key.value, window.scrollY)\n for (const old of saved.keys()) {\n if (saved.size <= kept) break\n saved.delete(old)\n }\n }\n\n let settling = new AbortController()\n /**\n * Holds the window at `top` while the next screen settles, whenever the page\n * can hold it, until the reader scrolls, touches or types, or another\n * navigation starts.\n */\n const settle = (top: number) => {\n settling.abort()\n settling = new AbortController()\n const { signal } = settling\n for (const input of ['wheel', 'touchstart', 'keydown', 'pointerdown'])\n window.addEventListener(input, () => settling.abort(), { passive: true, signal })\n const until = performance.now() + patience\n const step = () => {\n if (signal.aborted) return\n const room = document.documentElement.scrollHeight - window.innerHeight\n if (room >= top && Math.round(window.scrollY) !== top)\n window.scrollTo({ top, behavior: 'instant' })\n if (performance.now() < until) requestAnimationFrame(step)\n else settling.abort()\n }\n step()\n }\n\n // Remembered where the reader acts, before the application does: by the time\n // it pushes an address it has drawn the next screen, and the window is\n // already clamped to it. Back and Forward start in the browser, before\n // anything is drawn, so they are remembered as they begin.\n const acted = () => remember()\n for (const input of ['click', 'keydown']) on(window, input, acted, true)\n const traversing = (event: Event) => {\n if (Option.contains(navigationTypeOf(event), 'traverse')) remember()\n }\n on(navigation, 'navigate', traversing)\n const changed = (event: Event) => {\n const type = navigationTypeOf(event)\n if (Option.contains(type, 'push')) settle(0)\n else if (Option.contains(type, 'traverse')) settle(Option.getOrElse(savedHere(), () => 0))\n // A replace is the same entry, narrowed or selected in place: it stays where it is.\n }\n on(navigation, 'currententrychange', changed)\n const hidden = () => {\n remember()\n try {\n sessionStorage.setItem(storageKey, JSON.stringify([...saved]))\n } catch {\n // Storage blocked: the next document starts at the top.\n }\n }\n on(window, 'pagehide', hidden)\n // A reload, or Back into this document from another: where this entry was.\n const here = savedHere()\n if (Option.isSome(here)) settle(here.value)\n return () => {\n settling.abort()\n for (const detach of detaches) detach()\n window.history.scrollRestoration = restoration\n }\n}\n\n/**\n * Keeps the window's scroll across same-document navigations: a pushed entry\n * starts at the top, and Back, Forward or a reload return where that entry\n * was. Without a window the stream is empty instead of throwing. Lift with\n * `Subscription.persistent`; the stream sends no Messages.\n */\nexport const keepScroll = (options: KeepScrollOptions = {}): Stream.Stream<never> => {\n if (typeof window === 'undefined') return Stream.empty\n const storageKey = options.storageKey ?? 'foldkit:scroll'\n const kept = options.kept ?? 50\n const patience = options.patience ?? 2000\n return Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => attach(storageKey, kept, patience)),\n detach => Effect.sync(detach),\n ),\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["failMessage","failMessage","urlToString"],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/keep-in-view.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts","../../src/dom/measure.ts","../../src/dom/scroll.ts","../../src/dom/pointed-links.ts"],"sourcesContent":["import { Effect, Schema } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Focused = Schema.TaggedStruct('Focused', {})\nexport type Focused = typeof Focused.Type\n\n/**\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 */\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 * The clipboard as Commands: run in `update` beside any bundle. A copy yields\n * `Copied` or `CopyFailed`, a read `Read` or `ReadFailed`. No Model involved:\n * the 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\nexport const ClipboardReadMessage = defineMessageUnion({\n Read: { text: Schema.String },\n ReadFailed: { message: Schema.String },\n})\nexport type ClipboardReadMessage = typeof ClipboardReadMessage.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/**\n * Reads the clipboard's text. The browser may ask the user first; a refusal,\n * an insecure context or no clipboard API yields `ReadFailed`.\n */\nexport const readText = (): Command<ClipboardReadMessage, never, never> => ({\n name: 'Clipboard.read',\n effect:\n typeof navigator !== 'undefined' && typeof navigator.clipboard?.readText === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({ try: () => navigator.clipboard.readText(), catch: failMessage }),\n {\n onFailure: message => Effect.succeed(ClipboardReadMessage.ReadFailed({ message })),\n onSuccess: text => Effect.succeed(ClipboardReadMessage.Read({ text })),\n },\n )\n : Effect.succeed(ClipboardReadMessage.ReadFailed({ message: 'clipboard is unavailable' })),\n})\n","import { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/** What `InputMask` reports after each edit: the masked text shown, and the characters typed. */\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\n/**\n * The mask as a pure function: `#` takes a digit, `A` a letter, `*` either, and\n * any other character of the pattern is written in once input reaches past it.\n * Input that fits no placeholder is skipped. `masked` is what the field shows;\n * `raw` is only the characters typed.\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\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 */\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","import { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/**\n * Keeps what is marked in view as a Mount: whenever an element in the mounted\n * one's subtree newly matches `selector`, such as the row just selected in a\n * list, it is scrolled into view the least amount that shows it (`nearest`).\n * The element keeps its identity and its focus: nothing is redrawn to move it.\n *\n * It watches with a `MutationObserver` for as long as it is mounted, which is as\n * long as the list or canvas that can change what is marked.\n */\nexport const KeepInView = Mount.defineStream('KeepInView', {\n messages: [Schema.Never],\n args: {\n /** What is marked, as a selector within the element: `[aria-selected=\"true\"]`. */\n selector: Schema.String,\n },\n execute: ({ element, selector }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n let marked = element.querySelector(selector)\n const observer = new MutationObserver(() => {\n const now = element.querySelector(selector)\n // Only a change of what is marked scrolls: the author's own scrolling stays.\n // A DOM with no layout (jsdom, in a test) has no scrollIntoView and nothing to scroll.\n if (now !== null && now !== marked && typeof now.scrollIntoView === 'function')\n now.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n marked = now\n })\n observer.observe(element, { subtree: true, childList: true, attributes: true })\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\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","import { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/** The custom properties `Measure` writes for a target named `name`. */\nexport const measured = (name: string) =>\n ({\n x: `--fk-${name}-x`,\n y: `--fk-${name}-y`,\n w: `--fk-${name}-w`,\n h: `--fk-${name}-h`,\n display: `--fk-${name}-display`,\n }) as const\n\n/**\n * Measures marked elements as a Mount, for a view to draw something over them,\n * such as an editor's selection outline, with no Message and no Model: where\n * an element is, is presentation, known to the DOM and to no transition.\n *\n * For each named target, the first element in the mounted one's subtree that\n * matches its selector is measured relative to the mounted element's scroll\n * box, which is what an absolutely placed child of it is laid out in. The\n * mounted element gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, and\n * `--fk-<name>-display`: `block` while a target is found, `none` while none is.\n *\n * It measures again when the subtree changes (a selection moves), in the\n * frame the change is drawn in; and, at most once a frame, when the mounted\n * element scrolls, the window resizes, the mounted element or a measured one\n * changes size, an image or a font inside loads, and on every frame of a\n * transition or an animation inside. What moves a target by any other way,\n * such as a stylesheet added that shifts it without resizing it, is seen at\n * the next of these.\n */\nexport const Measure = Mount.defineStream('Measure', {\n messages: [Schema.Never],\n args: {\n /** Each target by name, as a selector within the element: `{ selected: '[aria-selected=\"true\"]' }`. */\n targets: Schema.Record(Schema.String, Schema.String),\n },\n execute: ({ element, targets }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const container = element as HTMLElement\n const names = Object.keys(targets)\n let watched: ReadonlyArray<Element> = []\n let frame: number | undefined\n // Whether a transition or an animation may be moving something, until one frame finds none.\n let animating = false\n\n const measure = () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n const box = container.getBoundingClientRect()\n const found: Array<Element> = []\n for (const name of names) {\n const property = measured(name)\n const target = container.querySelector(targets[name]!)\n if (target === null) {\n container.style.setProperty(property.display, 'none')\n continue\n }\n found.push(target)\n const rect = target.getBoundingClientRect()\n // Relative to the scroll box: where the content is, not where the viewport shows it.\n const x = rect.left - box.left - container.clientLeft + container.scrollLeft\n const y = rect.top - box.top - container.clientTop + container.scrollTop\n container.style.setProperty(property.x, `${x}px`)\n container.style.setProperty(property.y, `${y}px`)\n container.style.setProperty(property.w, `${rect.width}px`)\n container.style.setProperty(property.h, `${rect.height}px`)\n container.style.setProperty(property.display, 'block')\n }\n // A measured element that changes size, as an image loading does, is measured again.\n if (found.length !== watched.length || found.some((each, at) => each !== watched[at])) {\n for (const each of watched) sizes.unobserve(each)\n for (const each of found) sizes.observe(each)\n watched = found\n }\n if (animating) {\n // Asked each frame rather than counted: an element removed mid-transition ends none.\n animating = container\n .getAnimations({ subtree: true })\n .some(animation => animation.playState === 'running')\n if (animating) soon()\n }\n }\n const soon = () => {\n if (frame === undefined) frame = requestAnimationFrame(measure)\n }\n const moving = () => {\n animating = true\n soon()\n }\n\n // A change is measured as its records arrive, before the frame it is drawn in is\n // painted, so a box never shows one frame on the element it has left.\n const changes = new MutationObserver(measure)\n // Made, and the fonts listened to, before anything observes: where one is\n // missing (jsdom has neither a ResizeObserver nor `document.fonts`), the Mount\n // fails with nothing attached to call `measure`.\n const sizes = new ResizeObserver(soon)\n document.fonts.addEventListener('loadingdone', soon)\n // Its own properties change the container's style too, but writing the same\n // values again records nothing, so a measure that changed nothing rests.\n changes.observe(container, { subtree: true, childList: true, attributes: true })\n sizes.observe(container)\n container.addEventListener('scroll', soon, { passive: true })\n window.addEventListener('resize', soon)\n // `load` does not bubble, so it is caught on the way down.\n container.addEventListener('load', soon, { capture: true })\n container.addEventListener('transitionrun', moving)\n container.addEventListener('animationstart', moving)\n measure()\n return () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n changes.disconnect()\n sizes.disconnect()\n container.removeEventListener('scroll', soon)\n window.removeEventListener('resize', soon)\n container.removeEventListener('load', soon, { capture: true })\n document.fonts.removeEventListener('loadingdone', soon)\n container.removeEventListener('transitionrun', moving)\n container.removeEventListener('animationstart', moving)\n }\n }),\n stop => Effect.sync(stop),\n ),\n ),\n})\n","/**\n * Where the window is scrolled, across an application's own navigations, as\n * a stream. Left to itself, a browser keeps the scroll of the entry it\n * leaves when a link pushes a new one, and on Back restores the old offset\n * before the application has drawn the entry it returns to, so the offset is\n * clamped to a page still loading. Here a pushed entry starts at the top,\n * and Back, Forward or a reload return where that entry was, once the page\n * is tall enough to hold it.\n *\n * Three rules, each found the slow way:\n *\n * 1. The offset is taken when the reader acts. The next screen is drawn\n * before a Navigate Command pushes the address, so by the time the URL\n * changes the window is already clamped to the new, shorter page. The\n * scroll is recorded at the reader's click or key (capture phase), and at\n * the start of Back and Forward, which begin in the browser before\n * anything is drawn.\n * 2. A restore holds its place while the next screen settles. A restore made\n * when the entry changes is undone by the loading state drawn right after\n * it, so the target is held on each frame for a moment, until the reader\n * scrolls, touches or types, or another navigation starts.\n * 3. Entries are keyed by the Navigation API's entry key, which a reload and\n * a full navigation keep; offsets rest in `sessionStorage`, so Back into\n * another document restores too. Where the browser has no Navigation API\n * the stream attaches nothing and leaves the browser's own behaviour\n * alone.\n *\n * Lift with `Subscription.persistentEntry`; the stream sends no Messages. Only\n * the window scrolls: a nested container is the route graph's to know, when\n * `Site.transition` arrives.\n */\nimport { Effect, Option, Stream } from 'effect'\n\n/** How `keepScroll` remembers and restores each history entry's offset. */\nexport interface KeepScrollOptions {\n /** The `sessionStorage` key holding entry offsets. Default `'foldkit:scroll'`. */\n readonly storageKey?: string | undefined\n /** Entries remembered at most: the oldest are forgotten first. Default 50. */\n readonly kept?: number | undefined\n /**\n * How long a restore holds its place while the next screen settles, in\n * milliseconds. Default 2000.\n */\n readonly patience?: number | undefined\n}\n\nconst navigationTypeOf = (event: Event): Option.Option<unknown> =>\n 'navigationType' in event ? Option.some(event.navigationType) : Option.none()\n\nconst readSaved = (storageKey: string): Map<string, number> => {\n try {\n const stored: unknown = JSON.parse(sessionStorage.getItem(storageKey) ?? '[]')\n return new Map(\n Array.isArray(stored)\n ? stored.filter(\n (pair): pair is [string, number] =>\n Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'number',\n )\n : [],\n )\n } catch {\n // Storage blocked, or something else wrote the key: nothing is remembered.\n return new Map()\n }\n}\n\n/** Starts keeping the scroll; the release undoes everything it installed. */\nconst attach = (storageKey: string, kept: number, patience: number): (() => void) => {\n // TypeScript's DOM library declares it on every window; a browser without\n // the Navigation API has none.\n const navigation: Navigation | undefined = window.navigation\n if (navigation === undefined) return () => {}\n const restoration = window.history.scrollRestoration\n window.history.scrollRestoration = 'manual'\n const saved = readSaved(storageKey)\n const detaches: Array<() => void> = []\n const on = (target: EventTarget, type: string, listener: EventListener, capture?: boolean) => {\n target.addEventListener(type, listener, capture)\n detaches.push(() => target.removeEventListener(type, listener, capture))\n }\n const keyOf = () =>\n Option.fromNullOr(navigation.currentEntry).pipe(Option.map(entry => entry.key))\n /** Where the current entry was left, if it has been. */\n const savedHere = () => Option.flatMap(keyOf(), key => Option.fromUndefinedOr(saved.get(key)))\n const remember = () => {\n const key = keyOf()\n if (Option.isNone(key)) return\n // Moved to the end, so it is the last to be forgotten.\n saved.delete(key.value)\n saved.set(key.value, window.scrollY)\n for (const old of saved.keys()) {\n if (saved.size <= kept) break\n saved.delete(old)\n }\n }\n\n let settling = new AbortController()\n /**\n * Holds the window at `top` while the next screen settles, whenever the page\n * can hold it, until the reader scrolls, touches or types, or another\n * navigation starts.\n */\n const settle = (top: number) => {\n settling.abort()\n settling = new AbortController()\n const { signal } = settling\n for (const input of ['wheel', 'touchstart', 'keydown', 'pointerdown'])\n window.addEventListener(input, () => settling.abort(), { passive: true, signal })\n const until = performance.now() + patience\n const step = () => {\n if (signal.aborted) return\n const room = document.documentElement.scrollHeight - window.innerHeight\n if (room >= top && Math.round(window.scrollY) !== top)\n window.scrollTo({ top, behavior: 'instant' })\n if (performance.now() < until) requestAnimationFrame(step)\n else settling.abort()\n }\n step()\n }\n\n // Remembered where the reader acts, before the application does: by the time\n // it pushes an address it has drawn the next screen, and the window is\n // already clamped to it. Back and Forward start in the browser, before\n // anything is drawn, so they are remembered as they begin.\n const acted = () => remember()\n for (const input of ['click', 'keydown']) on(window, input, acted, true)\n const traversing = (event: Event) => {\n if (Option.contains(navigationTypeOf(event), 'traverse')) remember()\n }\n on(navigation, 'navigate', traversing)\n const changed = (event: Event) => {\n const type = navigationTypeOf(event)\n if (Option.contains(type, 'push')) settle(0)\n else if (Option.contains(type, 'traverse')) settle(Option.getOrElse(savedHere(), () => 0))\n // A replace is the same entry, narrowed or selected in place: it stays where it is.\n }\n on(navigation, 'currententrychange', changed)\n const hidden = () => {\n remember()\n try {\n sessionStorage.setItem(storageKey, JSON.stringify([...saved]))\n } catch {\n // Storage blocked: the next document starts at the top.\n }\n }\n on(window, 'pagehide', hidden)\n // A reload, or Back into this document from another: where this entry was.\n const here = savedHere()\n if (Option.isSome(here)) settle(here.value)\n return () => {\n settling.abort()\n for (const detach of detaches) detach()\n window.history.scrollRestoration = restoration\n }\n}\n\n/**\n * Keeps the window's scroll across same-document navigations: a pushed entry\n * starts at the top, and Back, Forward or a reload return where that entry\n * was. Without a window the stream is empty instead of throwing. Lift with\n * `Subscription.persistentEntry`; the stream sends no Messages.\n */\nexport const keepScroll = (options: KeepScrollOptions = {}): Stream.Stream<never> => {\n if (typeof window === 'undefined') return Stream.empty\n const storageKey = options.storageKey ?? 'foldkit:scroll'\n const kept = options.kept ?? 50\n const patience = options.patience ?? 2000\n return Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => attach(storageKey, kept, patience)),\n detach => Effect.sync(detach),\n ),\n )\n}\n","import { Option, Stream } from 'effect'\nimport * as Dom from 'foldkit/dom'\nimport { fromString, toString as urlToString, type Url } from 'foldkit/url'\n\n/**\n * The address of the link a reader points at or tabs to, on the page's own\n * origin: where they may go next, for a `Data.preload` before they go. Each\n * link once while they stay on it, since crossing its text sends one event\n * per element inside it. Without a document the stream is empty. Lift with\n * `Subscription.persistentEntry`, mapping each address to a Message.\n */\nexport const pointedLinks = (): Stream.Stream<Url> => {\n if (typeof document === 'undefined') return Stream.empty\n const linkOf = (event: Event): Option.Option<Url> => {\n const link = event.target instanceof Element ? event.target.closest('a[href]') : null\n return link instanceof HTMLAnchorElement && link.origin === window.location.origin\n ? fromString(link.href)\n : Option.none()\n }\n return Stream.merge(\n Dom.streamFromEventFilterMap({\n target: document,\n type: 'pointerover',\n filterMapEvent: linkOf,\n }),\n Dom.streamFromEventFilterMap({ target: document, type: 'focusin', filterMapEvent: linkOf }),\n ).pipe(Stream.changesWith((a, b) => urlToString(a) === urlToString(b)))\n}\n"],"mappings":"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"}
|
|
@@ -3,6 +3,13 @@ import * as Mount from "foldkit/mount";
|
|
|
3
3
|
//#region src/dom/focus-scope.d.ts
|
|
4
4
|
/** Tabbable descendants in DOM order: focusable, visible, and not `tabindex="-1"`. */
|
|
5
5
|
declare const tabbableWithin: (root: Element) => ReadonlyArray<HTMLElement>;
|
|
6
|
+
/**
|
|
7
|
+
* A focus scope as a Mount: on insert it remembers what was focused, focuses
|
|
8
|
+
* an initial element inside, and with `contain` keeps Tab and Shift+Tab
|
|
9
|
+
* cycling inside; on unmount it gives focus back. Nothing crosses to the
|
|
10
|
+
* Model: which element had focus is a DOM fact, so this is element lifecycle,
|
|
11
|
+
* not state. It emits no Messages.
|
|
12
|
+
*/
|
|
6
13
|
declare const FocusScope: Mount.MountDefinitionWithArgs<"FocusScope", {
|
|
7
14
|
/** Keep Tab, Shift+Tab, and a stray focus inside. */
|
|
8
15
|
contain: Schema.Boolean;
|
|
@@ -14,7 +21,9 @@ declare const FocusScope: Mount.MountDefinitionWithArgs<"FocusScope", {
|
|
|
14
21
|
}, never>;
|
|
15
22
|
//#endregion
|
|
16
23
|
//#region src/dom/layers.d.ts
|
|
24
|
+
/** Locks the document's scroll while the element is mounted; nested locks release with the last. */
|
|
17
25
|
declare const ScrollLock: Mount.MountDefinitionNoArgs<"ScrollLock", never>;
|
|
26
|
+
/** Makes everything outside the element inert while it is mounted, and restores it after. */
|
|
18
27
|
declare const HideOutside: Mount.MountDefinitionNoArgs<"HideOutside", never>;
|
|
19
28
|
//#endregion
|
|
20
29
|
//#region src/dom/move.d.ts
|
|
@@ -38,6 +47,13 @@ declare const MoveEnded: Schema.TaggedStruct<"MoveEnded", {
|
|
|
38
47
|
}>;
|
|
39
48
|
type MoveEnded = typeof MoveEnded.Type;
|
|
40
49
|
type Fact = MoveStarted | Moved | MoveEnded;
|
|
50
|
+
/**
|
|
51
|
+
* Pointer movement on an element as a Mount: a primary-button pointer down
|
|
52
|
+
* captures the pointer, each move reports the delta from where it started,
|
|
53
|
+
* and up, cancel, or lost capture ends it. Nothing is decided here: a drag
|
|
54
|
+
* threshold, a snap, or a constraint is the parent's `update`. Capture is
|
|
55
|
+
* released with the Mount.
|
|
56
|
+
*/
|
|
41
57
|
declare const Move: Mount.MountDefinitionNoArgs<"Move", {
|
|
42
58
|
readonly _tag: "MoveStarted";
|
|
43
59
|
readonly pointerId: number;
|
|
@@ -71,6 +87,16 @@ type TargetPressed = typeof TargetPressed.Type;
|
|
|
71
87
|
type TargetFact = TargetHovered | TargetPressed;
|
|
72
88
|
/** The id of the nearest marked element at or above `from`, inside `container`. */
|
|
73
89
|
declare const targetOf: (container: Element, from: EventTarget | null, attribute: string) => string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Which marked descendant of an element the pointer is over, and which one was
|
|
92
|
+
* pressed, as a Mount: one set of listeners on the container, not one per item.
|
|
93
|
+
* A descendant is marked by an attribute whose value is its id, such as a
|
|
94
|
+
* canvas's `data-composition-node` or a table's `data-row`. The nearest marked
|
|
95
|
+
* ancestor of the event's target, inside the container, is the one reported.
|
|
96
|
+
*
|
|
97
|
+
* What hovering or pressing means (a selection, an outline, a menu) is the
|
|
98
|
+
* parent's `update`.
|
|
99
|
+
*/
|
|
74
100
|
declare const Targets: Mount.MountDefinitionWithArgs<"Targets", {
|
|
75
101
|
/** The attribute that marks a descendant, holding its id. */
|
|
76
102
|
attribute: Schema.String;
|
|
@@ -151,6 +177,20 @@ declare const zoneOf: (box: Box, y: number) => DragZone;
|
|
|
151
177
|
* first child's stands for it, as an editor's node wrapper's does.
|
|
152
178
|
*/
|
|
153
179
|
declare const boxOf: (element: Element) => Box;
|
|
180
|
+
/**
|
|
181
|
+
* Dragging one marked descendant of an element onto another, as a Mount. A
|
|
182
|
+
* descendant is marked by an attribute whose value is its id, as `Targets`
|
|
183
|
+
* marks them; a press on one that then moves past a threshold starts a drag,
|
|
184
|
+
* and the pointer's place over another marked descendant is reported with the
|
|
185
|
+
* zone of it the pointer is in: its first third (`before`), its middle
|
|
186
|
+
* (`inside`), or its last third (`after`). Releasing drops, and Escape or a
|
|
187
|
+
* cancelled pointer ends the drag with nothing dropped.
|
|
188
|
+
*
|
|
189
|
+
* It writes no roles, no `tabindex` and no key handling, so the marked
|
|
190
|
+
* elements can be anything: a tree's rows, a canvas's nodes. The keyboard's way
|
|
191
|
+
* to do what a drag does is the parent's to give. Geometry is measured when the
|
|
192
|
+
* pointer moves and never kept; what a drop means is the parent's `update`.
|
|
193
|
+
*/
|
|
154
194
|
declare const PointerDrag: Mount.MountDefinitionWithArgs<"PointerDrag", {
|
|
155
195
|
/** The attribute that marks a descendant, holding its id. */
|
|
156
196
|
attribute: Schema.String;
|
|
@@ -178,7 +218,7 @@ declare const PointerDrag: Mount.MountDefinitionWithArgs<"PointerDrag", {
|
|
|
178
218
|
readonly _tag: "DraggedOver";
|
|
179
219
|
readonly over: {
|
|
180
220
|
readonly id: string;
|
|
181
|
-
readonly zone: "
|
|
221
|
+
readonly zone: "after" | "before" | "inside";
|
|
182
222
|
readonly part?: string;
|
|
183
223
|
} | null;
|
|
184
224
|
readonly region?: boolean;
|
|
@@ -187,7 +227,7 @@ declare const PointerDrag: Mount.MountDefinitionWithArgs<"PointerDrag", {
|
|
|
187
227
|
readonly id: string;
|
|
188
228
|
readonly over: {
|
|
189
229
|
readonly id: string;
|
|
190
|
-
readonly zone: "
|
|
230
|
+
readonly zone: "after" | "before" | "inside";
|
|
191
231
|
readonly part?: string;
|
|
192
232
|
} | null;
|
|
193
233
|
readonly region?: boolean;
|
|
@@ -225,6 +265,28 @@ type TextFact = TextEdited | TextCommitted | TextCancelled | EditAsked;
|
|
|
225
265
|
* itself, which is not the author's; in one line, the author's breaks are spaces.
|
|
226
266
|
*/
|
|
227
267
|
declare const textOf: (element: HTMLElement) => string;
|
|
268
|
+
/**
|
|
269
|
+
* Text typed into an editable descendant of an element, as a Mount: one set of
|
|
270
|
+
* listeners on the container, like `Targets`. A field is a descendant marked
|
|
271
|
+
* by an attribute whose value names it, while it is `contenteditable`. The
|
|
272
|
+
* view decides which one is; the Mount focuses it as it becomes so (made
|
|
273
|
+
* editable, or drawn editable), with the caret at its end, and at no other
|
|
274
|
+
* time. When Enter or Escape ends an edit and the view then removes the field
|
|
275
|
+
* or makes it no longer editable, focus, left on nothing, comes back to the
|
|
276
|
+
* container, so the next key reaches it; give the container a `tabindex`. A double-click on a marked field that is not editable yet asks for it
|
|
277
|
+
* to be (`EditAsked`).
|
|
278
|
+
*
|
|
279
|
+
* It reads `innerText`, never `innerHTML`, so what arrives is text. A field is
|
|
280
|
+
* one line unless it carries `aria-multiline="true"`: in one line, a line
|
|
281
|
+
* break becomes a space. Nothing is reported while an input method composes;
|
|
282
|
+
* the composed text arrives once, when it is done.
|
|
283
|
+
*
|
|
284
|
+
* Enter commits (Shift+Enter breaks a line in a multiline field), and so does
|
|
285
|
+
* leaving the field. Escape cancels, and puts the text the field had when
|
|
286
|
+
* editing began back in the DOM, since a view that stopped redrawing the field
|
|
287
|
+
* while it was edited will not. Where the browser lacks `plaintext-only`, a
|
|
288
|
+
* paste is inserted as text. What an edit means is the parent's `update`.
|
|
289
|
+
*/
|
|
228
290
|
declare const EditableText: Mount.MountDefinitionWithArgs<"EditableText", {
|
|
229
291
|
/** The attribute that marks a field, holding its name. */
|
|
230
292
|
attribute: Schema.String;
|
|
@@ -246,4 +308,4 @@ declare const EditableText: Mount.MountDefinitionWithArgs<"EditableText", {
|
|
|
246
308
|
}>;
|
|
247
309
|
//#endregion
|
|
248
310
|
export { ScrollLock as A, targetOf as C, MoveStarted as D, MoveEnded as E, tabbableWithin as M, Moved as O, Targets as S, Move as T, boxOf as _, TextEdited as a, TargetHovered as b, DRAG_THRESHOLD as c, DragFact as d, DragPlace as f, PointerDrag as g, DraggedOver as h, TextCommitted as i, FocusScope as j, HideOutside as k, DragCancelled as l, DragZone as m, EditableText as n, TextFact as o, DragStarted as p, TextCancelled as r, textOf as s, EditAsked as t, DragDropped as u, zoneOf as v, Fact as w, TargetPressed as x, TargetFact as y };
|
|
249
|
-
//# sourceMappingURL=editable-text-
|
|
311
|
+
//# sourceMappingURL=editable-text-Dvm-Fuqs.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editable-text-Dvm-Fuqs.d.mts","names":[],"sources":["../src/dom/focus-scope.ts","../src/dom/layers.ts","../src/dom/move.ts","../src/dom/targets.ts","../src/dom/pointer-drag.ts","../src/dom/editable-text.ts"],"mappings":";;;;cAmBa,iBAAc,MAAU,YAAU,cAAc;;;;;;;;cAmBhD,YAAU,MAAA;;;;;;;;;;;;cCtBV,YAAU,MAAA;;cAWV,aAAW,MAAA;;;cCxBX,aAAW,OAAA;;;;KAIZ,qBAAqB,YAAY;cAEhC,OAAK,OAAA;;;;;;;KAON,eAAe,MAAM;cAEpB,WAAS,OAAA;;;;;KAKV,mBAAmB,UAAU;KAE7B,OAAO,cAAc,QAAQ;;;;;;;;cAsB5B,MAAI,MAAA;;;;;;;;;;;;;;;;;cC5CJ,eAAa,OAAA;;;;KAId,uBAAuB,cAAc;cAEpC,eAAa,OAAA;;;;;;;KAOd,uBAAuB,cAAc;KAErC,aAAa,gBAAgB;;cAU5B,WAAQ,WAAe,SAAO,MAAQ,oBAAkB;;;;;;;;;;;cAgBxD,SAAO,MAAA;;;;;;;;;;;;;;;;;;cCxCP,UAAQ,OAAA;KACT,kBAAkB,SAAS;;;;;;cAO1B,WAAS,OAAA;;;;;KAKV,mBAAmB,UAAU;cAE5B,aAAW,OAAA;;eAAX,OAAA;;KAID,qBAAqB,YAAY;cAEhC,aAAW,OAAA;;;;;;;;;;;;;;KAUZ,qBAAqB,YAAY;cAEhC,aAAW,OAAA;;;;;;;;;;KAMZ,qBAAqB,YAAY;cAEhC,eAAa,OAAA;eAAb,OAAA;;KACD,uBAAuB,cAAc;KAErC,WAAW,cAAc,cAAc,cAAc;;cAGpD;UAEH;WACC;WACA;;;cAIE,SAAM,KAAS,KAAG,cAAc;;;;;cAUhC,QAAK,SAAa,YAAU;;;;;;;;;;;;;;;cA8B5B,aAAW,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cChGX,YAAU,OAAA;;;;;KAKX,oBAAoB,WAAW;cAE9B,eAAa,OAAA;;;;KAId,uBAAuB,cAAc;cAEpC,eAAa,OAAA;;;;;KAKd,uBAAuB,cAAc;cAEpC,WAAS,OAAA;;kBAAT,OAAA;;KAID,mBAAmB,UAAU;KAE7B,WAAW,aAAa,gBAAgB,gBAAgB;;;;;cASvD,SAAM,SAAa;;;;;;;;;;;;;;;;;;;;;;;cA0CnB,cAAY,MAAA;;aAAZ,OAAA"}
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import { a as modalityOfKey, i as Modality, n as InputModalityMessage, r as InputModalityModel, t as InputModality } from "../input-modality-
|
|
1
|
+
import { a as modalityOfKey, i as Modality, n as InputModalityMessage, r as InputModalityModel, t as InputModality } from "../input-modality-BY-bSrEc.mjs";
|
|
2
2
|
import { Schema, Stream } from "effect";
|
|
3
3
|
import { Bundle } from "foldkit-bundle";
|
|
4
4
|
//#region src/events/active-element.d.ts
|
|
5
5
|
export declare const ActiveElementMessage: import("foldkit/message").MessageUnion<{
|
|
6
6
|
readonly Changed: {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
tag: Schema.String;
|
|
8
|
+
id: Schema.NullOr<Schema.String>;
|
|
9
9
|
};
|
|
10
10
|
}>;
|
|
11
11
|
export type ActiveElementMessage = typeof ActiveElementMessage.Type;
|
|
12
12
|
/**
|
|
13
13
|
* Focus changes on the document. Without a document the stream is empty
|
|
14
|
-
* instead of throwing. Lift with `Subscription.
|
|
14
|
+
* instead of throwing. Lift with `Subscription.persistentEntry`, mapping into
|
|
15
15
|
* the parent's Message.
|
|
16
16
|
*/
|
|
17
17
|
export declare const activeElementEvents: () => Stream.Stream<ActiveElementMessage>;
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/events/hotkeys.d.ts
|
|
20
|
+
export interface KeyPress {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly repeat: boolean;
|
|
23
|
+
readonly ctrl: boolean;
|
|
24
|
+
readonly shift: boolean;
|
|
25
|
+
readonly alt: boolean;
|
|
26
|
+
readonly meta: boolean;
|
|
27
|
+
}
|
|
20
28
|
/**
|
|
21
29
|
* Hotkey matching as a pure function over a `Pressed` payload: `matchHotkey`
|
|
22
30
|
* answers whether a press is the shortcut, so `update` stays a table of
|
|
@@ -29,14 +37,6 @@ export declare const activeElementEvents: () => Stream.Stream<ActiveElementMessa
|
|
|
29
37
|
* no key, or two keys, matches nothing: predicates return false, they do
|
|
30
38
|
* not throw on the hot path.
|
|
31
39
|
*/
|
|
32
|
-
export interface KeyPress {
|
|
33
|
-
readonly key: string;
|
|
34
|
-
readonly repeat: boolean;
|
|
35
|
-
readonly ctrl: boolean;
|
|
36
|
-
readonly shift: boolean;
|
|
37
|
-
readonly alt: boolean;
|
|
38
|
-
readonly meta: boolean;
|
|
39
|
-
}
|
|
40
40
|
export declare const matchHotkey: (pattern: string, press: KeyPress) => boolean;
|
|
41
41
|
//#endregion
|
|
42
42
|
//#region src/events/idle.d.ts
|
|
@@ -49,34 +49,42 @@ export declare const IdleMessage: import("foldkit/message").MessageUnion<{
|
|
|
49
49
|
readonly BecameActive: {};
|
|
50
50
|
}>;
|
|
51
51
|
export type IdleMessage = typeof IdleMessage.Type;
|
|
52
|
+
/**
|
|
53
|
+
* Idleness as a bundle: `idle` in the Model, flipped by activity and
|
|
54
|
+
* silence. While active, activity debounced past `timeoutMs` settles to
|
|
55
|
+
* `BecameIdle`; while idle, the first activity wakes to `BecameActive` and
|
|
56
|
+
* the dependency flip restarts the watch. Starts active — a lurker idles
|
|
57
|
+
* when the timeout elapses. The clock is Effect's, so tests drive it with
|
|
58
|
+
* TestClock.
|
|
59
|
+
*/
|
|
52
60
|
export declare const Idle: Bundle.Bundle<"Idle", {
|
|
53
61
|
readonly timeoutMs: number;
|
|
54
62
|
}, {
|
|
55
63
|
readonly idle: boolean;
|
|
56
64
|
}, {
|
|
57
|
-
readonly _tag: "BecameIdle";
|
|
58
|
-
} | {
|
|
59
65
|
readonly _tag: "BecameActive";
|
|
66
|
+
} | {
|
|
67
|
+
readonly _tag: "BecameIdle";
|
|
60
68
|
}, never, never, never, void, {}, {}>;
|
|
61
69
|
//#endregion
|
|
62
70
|
//#region src/events/keyboard.d.ts
|
|
63
71
|
export declare const KeyboardMessage: import("foldkit/message").MessageUnion<{
|
|
64
72
|
readonly Pressed: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
key: Schema.String;
|
|
74
|
+
repeat: Schema.Boolean;
|
|
75
|
+
ctrl: Schema.Boolean;
|
|
76
|
+
shift: Schema.Boolean;
|
|
77
|
+
alt: Schema.Boolean;
|
|
78
|
+
meta: Schema.Boolean;
|
|
71
79
|
};
|
|
72
80
|
readonly Released: {
|
|
73
|
-
|
|
81
|
+
key: Schema.String;
|
|
74
82
|
};
|
|
75
83
|
}>;
|
|
76
84
|
export type KeyboardMessage = typeof KeyboardMessage.Type;
|
|
77
85
|
/**
|
|
78
86
|
* Key presses and releases on the window. Without a window the stream is
|
|
79
|
-
* empty instead of throwing. Lift with `Subscription.
|
|
87
|
+
* empty instead of throwing. Lift with `Subscription.persistentEntry`, mapping
|
|
80
88
|
* into the parent's Message.
|
|
81
89
|
*/
|
|
82
90
|
export declare const keyboardEvents: (options?: {
|
|
@@ -91,14 +99,14 @@ export declare const keyboardEvents: (options?: {
|
|
|
91
99
|
//#region src/events/pointer.d.ts
|
|
92
100
|
export declare const PointerMessage: import("foldkit/message").MessageUnion<{
|
|
93
101
|
readonly Moved: {
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
x: Schema.Number;
|
|
103
|
+
y: Schema.Number;
|
|
96
104
|
};
|
|
97
105
|
}>;
|
|
98
106
|
export type PointerMessage = typeof PointerMessage.Type;
|
|
99
107
|
/**
|
|
100
108
|
* Pointer moves on the window. Without a window the stream is empty instead
|
|
101
|
-
* of throwing. Lift with `Subscription.
|
|
109
|
+
* of throwing. Lift with `Subscription.persistentEntry`, mapping into the
|
|
102
110
|
* parent's Message.
|
|
103
111
|
*/
|
|
104
112
|
export declare const pointerEvents: () => Stream.Stream<PointerMessage>;
|
|
@@ -106,15 +114,15 @@ export declare const pointerEvents: () => Stream.Stream<PointerMessage>;
|
|
|
106
114
|
//#region src/events/scroll.d.ts
|
|
107
115
|
export declare const ScrollMessage: import("foldkit/message").MessageUnion<{
|
|
108
116
|
readonly Scrolled: {
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
x: Schema.Number;
|
|
118
|
+
y: Schema.Number;
|
|
111
119
|
};
|
|
112
120
|
}>;
|
|
113
121
|
export type ScrollMessage = typeof ScrollMessage.Type;
|
|
114
122
|
/**
|
|
115
123
|
* Scrolls on the window, in the capture phase so container scrolls (which
|
|
116
124
|
* do not bubble) report too. Without a window the stream is empty instead
|
|
117
|
-
* of throwing. Lift with `Subscription.
|
|
125
|
+
* of throwing. Lift with `Subscription.persistentEntry`, mapping into the
|
|
118
126
|
* parent's Message.
|
|
119
127
|
*/
|
|
120
128
|
export declare const scrollEvents: () => Stream.Stream<ScrollMessage>;
|
|
@@ -126,10 +134,16 @@ export declare const VisibilityModel: Schema.Struct<{
|
|
|
126
134
|
export type VisibilityModel = typeof VisibilityModel.Type;
|
|
127
135
|
export declare const VisibilityMessage: import("foldkit/message").MessageUnion<{
|
|
128
136
|
readonly Changed: {
|
|
129
|
-
|
|
137
|
+
visible: Schema.Boolean;
|
|
130
138
|
};
|
|
131
139
|
}>;
|
|
132
140
|
export type VisibilityMessage = typeof VisibilityMessage.Type;
|
|
141
|
+
/**
|
|
142
|
+
* Page visibility as a bundle: `visible` in the Model, read from the
|
|
143
|
+
* document at startup and kept current by `visibilitychange`. A hidden page
|
|
144
|
+
* is the application's cue to pause polling, animations, and realtime
|
|
145
|
+
* streams; the decision stays in application `update`, not here.
|
|
146
|
+
*/
|
|
133
147
|
export declare const Visibility: Bundle.Bundle<"Visibility", void, {
|
|
134
148
|
readonly visible: boolean;
|
|
135
149
|
}, {
|
|
@@ -145,11 +159,18 @@ export declare const WindowSizeModel: Schema.Struct<{
|
|
|
145
159
|
export type WindowSizeModel = typeof WindowSizeModel.Type;
|
|
146
160
|
export declare const WindowSizeMessage: import("foldkit/message").MessageUnion<{
|
|
147
161
|
readonly Changed: {
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
width: Schema.Number;
|
|
163
|
+
height: Schema.Number;
|
|
150
164
|
};
|
|
151
165
|
}>;
|
|
152
166
|
export type WindowSizeMessage = typeof WindowSizeMessage.Type;
|
|
167
|
+
/**
|
|
168
|
+
* Window size as a bundle: `{ width, height }` in the Model, read from the
|
|
169
|
+
* window at startup and kept current by one `resize` listener. Breakpoints
|
|
170
|
+
* owns its own listener and derives names; place this one when views need
|
|
171
|
+
* the raw pixels. Placing both doubles resize listeners — pick the one the
|
|
172
|
+
* view reads.
|
|
173
|
+
*/
|
|
153
174
|
export declare const WindowSize: Bundle.Bundle<"WindowSize", void, {
|
|
154
175
|
readonly width: number;
|
|
155
176
|
readonly height: number;
|
|
@@ -1 +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
|
|
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;;IACpB,KAAG,OAAA;IAAiB,IAAE,OAAA,OAAA,OAAA;;;YAEvB,8BAA8B,qBAAqB;;;;;;qBAOlD,2BAA0B,OAAO,OAAO;;;iBCnBpC;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;;;;;qBA8CE,cAAW,iBAAmB,OAAS;;;qBC9CvC,WAAS,OAAA;iBAAT,OAAA;;YACD,mBAAmB,UAAU;qBAE5B,uCAAW;;;;YACZ,qBAAqB,YAAY;;;;;;;;;qBAsBhC,MAAI,OAAA;;;;;;;;;;;qBCtBJ,2CAAe;;IAExB,KAAG,OAAA;IACH,QAAM,OAAA;IACN,MAAI,OAAA;IACJ,OAAK,OAAA;IACL,KAAG,OAAA;IACH,MAAI,OAAA;;;IAEM,KAAG,OAAA;;;YAEL,yBAAyB,gBAAgB;;;;;;qBAOxC,iBAAc;;;;;;WAOd,kBAAkB,OAAO;MAEnC,OAAO,OAAO;;;qBC5BJ,0CAAc;;IAChB,GAAC,OAAA;IAAiB,GAAC,OAAA;;;YAElB,wBAAwB,eAAe;;;;;;qBAOtC,qBAAoB,OAAO,OAAO;;;qBCVlC,yCAAa;;IACZ,GAAC,OAAA;IAAiB,GAAC,OAAA;;;YAErB,uBAAuB,cAAc;;;;;;;qBAQpC,oBAAmB,OAAO,OAAO;;;qBCfjC,iBAAe,OAAA;oBAAf,OAAA;;YACD,yBAAyB,gBAAgB;qBAExC,6CAAiB;;IAAmC,SAAO,OAAA;;;YAC5D,2BAA2B,kBAAkB;;;;;;;qBAuB5C,YAAU,OAAA;;;;;;;;qBC1BV,iBAAe,OAAA;;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;IACjB,OAAK,OAAA;IAAiB,QAAM,OAAA;;;YAE7B,2BAA2B,kBAAkB;;;;;;;;qBA0C5C,YAAU,OAAA"}
|