foldkit-primitives 0.2.1 → 0.3.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 +314 -32
- package/dist/device/index.d.mts +11 -11
- package/dist/dom/index.d.mts +2 -0
- package/dist/dom/index.d.mts.map +1 -1
- package/dist/dom/index.mjs +2 -1
- package/dist/dom/index.mjs.map +1 -1
- package/dist/events/index.d.mts +2 -0
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +2 -1
- package/dist/events/index.mjs.map +1 -1
- package/dist/index-Cppf-lBx.d.mts +130 -0
- package/dist/index-Cppf-lBx.d.mts.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +4 -2
- package/dist/input-modality-DGOhAknO.d.mts +26 -0
- package/dist/input-modality-DGOhAknO.d.mts.map +1 -0
- package/dist/input-modality-m6v7__Xs.mjs +43 -0
- package/dist/input-modality-m6v7__Xs.mjs.map +1 -0
- package/dist/interaction/index.d.mts +862 -0
- package/dist/interaction/index.d.mts.map +1 -0
- package/dist/interaction/index.mjs +1364 -0
- package/dist/interaction/index.mjs.map +1 -0
- package/dist/motion/index.d.mts +2 -112
- package/dist/motion/index.mjs +2 -208
- package/dist/motion-LfJYAMnP.mjs +239 -0
- package/dist/motion-LfJYAMnP.mjs.map +1 -0
- package/dist/move-CZwHyTGk.mjs +215 -0
- package/dist/move-CZwHyTGk.mjs.map +1 -0
- package/dist/move-DNiMPcuf.d.mts +58 -0
- package/dist/move-DNiMPcuf.d.mts.map +1 -0
- package/dist/net/index.d.mts +5 -5
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +20 -8
- package/dist/motion/index.d.mts.map +0 -1
- package/dist/motion/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -21,11 +21,15 @@ import { MediaQuery } from 'foldkit-primitives/media'
|
|
|
21
21
|
A primitive belongs here when it is **reused across applications** and
|
|
22
22
|
**stateful**: something observes it, something reacts to it, and replay sees
|
|
23
23
|
the same transitions. That is exactly the bundle shape, which is why this
|
|
24
|
-
package depends on `foldkit-bundle` and nothing else (besides peers).
|
|
24
|
+
package depends on `foldkit-bundle` and nothing else (besides peers). The one
|
|
25
|
+
exception is `interaction`, whose Bundles ship with a `foldkit-mixins` Behavior
|
|
26
|
+
that wires them to a view's slots; `foldkit-mixins` is an optional peer that
|
|
27
|
+
only that subpath needs.
|
|
25
28
|
|
|
26
29
|
| Kind | Form | Example |
|
|
27
30
|
| --- | --- | --- |
|
|
28
31
|
| Stateful + effectful | bundle | MediaQuery, Timer, WebSocket, Pagination |
|
|
32
|
+
| Interaction state a view's slots must reflect | bundle + Behavior | RovingTabindex, Typeahead, ListNavigation, GridNavigation, Press |
|
|
29
33
|
| Keyed collections of stateful items | bundle per key | uploads, sockets, timers (later) |
|
|
30
34
|
| Stream source with a stored fact | bundle with one boolean/scalar slice | Online, Visibility, WindowSize |
|
|
31
35
|
| Stream source only | Subscription entry, not a bundle | keyboard, pointer, scroll, broadcast |
|
|
@@ -57,14 +61,14 @@ accident — one state machine leaves nowhere for a second one to live.
|
|
|
57
61
|
```text
|
|
58
62
|
browser / clock / server ──facts as Messages──▶ update ──▶ Model slice
|
|
59
63
|
▲
|
|
60
|
-
|
|
64
|
+
The parent Model owns the recorded fact; subscriptions report environment changes.
|
|
61
65
|
```
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
Initialization creates the Model slice; subscriptions observe ongoing facts and
|
|
68
|
+
report Messages; `update` stores them. Defaults differ by primitive:
|
|
69
|
+
MediaQuery starts at `false`, while Online, Visibility, and Locale read an
|
|
70
|
+
available platform value during initialization. Missing browser APIs have
|
|
71
|
+
primitive-specific fallbacks; do not infer readiness from an SSR default.
|
|
68
72
|
|
|
69
73
|
## Install
|
|
70
74
|
|
|
@@ -87,51 +91,52 @@ Each subpath is one concern, one import:
|
|
|
87
91
|
- `time` — clock facts: Timer, Interval, Debounce, Throttle, relative time
|
|
88
92
|
- `state` — owned UI state: Pagination, History, Locale, SelectionSet, Virtual, range
|
|
89
93
|
- `motion` — animation state: Tween, Spring, Presence
|
|
94
|
+
- `interaction` — a Bundle (or Mount) and its `foldkit-mixins` Behavior: RovingTabindex, Typeahead, ListNavigation, GridNavigation, FocusScope, Press, LongPress, Move, FocusVisible, DismissLayer, ScrollLock, HideOutside, Selection, LiveAnnounce
|
|
90
95
|
- `device` — hardware: Geolocation, MediaDevices, MediaStream, Permissions, Fullscreen
|
|
91
|
-
- `events` — raw browser events: Visibility, WindowSize, Idle, keyboard, pointer, scroll, focus
|
|
96
|
+
- `events` — raw browser events: Visibility, WindowSize, Idle, InputModality, keyboard, pointer, scroll, focus
|
|
92
97
|
- `observers` — element Mounts: Resize, Intersection, Mutation, Bounds
|
|
93
|
-
- `dom` — element Mounts and one-shot Commands: Autofocus, InputMask, clipboard, share, script loading
|
|
98
|
+
- `dom` — element Mounts and one-shot Commands: Autofocus, FocusScope, Move, ScrollLock, HideOutside, InputMask, clipboard, share, script loading
|
|
94
99
|
|
|
95
100
|
## Sixty seconds: follow the color scheme
|
|
96
101
|
|
|
97
|
-
**
|
|
98
|
-
Message
|
|
102
|
+
**Place it in a parent.** `Bundle.compose` states the parent's own field and
|
|
103
|
+
Message, and places the primitive under `dark`, with the wrapper Message
|
|
104
|
+
`GotDarkMessage` by Foldkit's `Got<Field>Message` convention:
|
|
99
105
|
|
|
100
106
|
```ts
|
|
101
107
|
import { Schema } from 'effect'
|
|
102
|
-
import {
|
|
108
|
+
import type { HtmlBuilder } from 'foldkit/html'
|
|
103
109
|
import { Bundle } from 'foldkit-bundle'
|
|
104
110
|
import { MediaQuery } from 'foldkit-primitives/media'
|
|
105
111
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
type Message = typeof Message.Type
|
|
112
|
+
const Page = Bundle.compose({ theme: Schema.String }).pipe(
|
|
113
|
+
Bundle.withMessages({ ThemeSet: { theme: Schema.String } }),
|
|
114
|
+
Bundle.withChild('dark', MediaQuery, { args: { query: '(prefers-color-scheme: dark)' } }),
|
|
115
|
+
)
|
|
116
|
+
type Model = typeof Page.Model.Type
|
|
117
|
+
type Message = typeof Page.Message.Type
|
|
112
118
|
```
|
|
113
119
|
|
|
114
|
-
**
|
|
115
|
-
assembly derives the update and the Subscriptions:
|
|
120
|
+
**Run it.** The parent's assembly derives the update and the Subscriptions:
|
|
116
121
|
|
|
117
122
|
```ts
|
|
118
|
-
const
|
|
119
|
-
const placements = Page.assemble(Page.at(Dark, { args: { query: '(prefers-color-scheme: dark)' } }))
|
|
123
|
+
const { placements } = Page
|
|
120
124
|
|
|
121
125
|
const config = placements.complete({
|
|
122
126
|
init: () => placements.initial({ theme: 'light' }),
|
|
123
127
|
update: placements.update(model => ({ model })),
|
|
124
|
-
view,
|
|
128
|
+
view: (model: Model, h: HtmlBuilder<Message>) =>
|
|
129
|
+
h.div([], [model.dark.matches ? 'Dark mode' : 'Light mode']),
|
|
125
130
|
subscriptions: placements.subscriptions(),
|
|
126
131
|
})
|
|
127
132
|
```
|
|
128
133
|
|
|
129
134
|
`update` routes `GotDarkMessage` to the bundle; `subscriptions` runs the
|
|
130
135
|
`matchMedia` stream; `initial` starts `matches` at `false` and the stream
|
|
131
|
-
corrects it on subscribe. None of
|
|
132
|
-
`
|
|
133
|
-
folding and the streams
|
|
134
|
-
subscribes. The Solid equivalent this replaces:
|
|
136
|
+
corrects it on subscribe. None of these calls perform I/O: `compose` states
|
|
137
|
+
the parent, `withChild` places the primitive with its config, and the
|
|
138
|
+
assembly derives the folding and the streams. The browser is touched only
|
|
139
|
+
when the runtime subscribes. The Solid equivalent this replaces:
|
|
135
140
|
|
|
136
141
|
```ts
|
|
137
142
|
// solid-primitives: const dark = createMediaQuery('(prefers-color-scheme: dark)')
|
|
@@ -165,7 +170,7 @@ Bound presets place with no args:
|
|
|
165
170
|
```ts
|
|
166
171
|
import { PrefersDark, PrefersReducedMotion } from 'foldkit-primitives/media'
|
|
167
172
|
|
|
168
|
-
const
|
|
173
|
+
const Page = Bundle.compose({ theme: Schema.String }).pipe(Bundle.withChild('dark', PrefersDark))
|
|
169
174
|
```
|
|
170
175
|
|
|
171
176
|
`Breakpoints` derives names from one `resize` listener: args
|
|
@@ -305,8 +310,7 @@ skipped. `Bounds()` re-measures `Measured { x, y, width, height }` on
|
|
|
305
310
|
observer, scroll, and resize, starting with the current rect; without a
|
|
306
311
|
ResizeObserver the window events still measure. Without the observer API
|
|
307
312
|
(SSR, old browser) they emit nothing instead of throwing; teardown
|
|
308
|
-
disconnects.
|
|
309
|
-
is same-valued and harmless.
|
|
313
|
+
disconnects. Treat repeated measurements as observations, not proof that a user action occurred.
|
|
310
314
|
|
|
311
315
|
## Device: `foldkit-primitives/device`
|
|
312
316
|
|
|
@@ -401,7 +405,7 @@ const Doc = Bundle.declare(EditHistory, 'doc')
|
|
|
401
405
|
startup with the configured `default` as fallback; `SetLocale` switches it.
|
|
402
406
|
`SelectionSet` keeps string ids in first-selection order: `Select` (keeps
|
|
403
407
|
position), `Deselect`, `Toggle` (re-appends), `ReplaceAll` (deduped), and
|
|
404
|
-
`Clear`. `isSelected` reads membership.
|
|
408
|
+
`Clear`. `isSelected` reads membership. Neither subscribes to changes; Locale reads the initial browser language when available.
|
|
405
409
|
Keyed children — lists with stable identity — place through the bundle
|
|
406
410
|
mechanism's `each`.
|
|
407
411
|
`range(start, end, step?)` counts half-open numbers — the pagination page
|
|
@@ -446,6 +450,16 @@ should survive reload, and keep this package's bundles for live facts.
|
|
|
446
450
|
|
|
447
451
|
> Reference: [`./motion/README.md`](./motion/README.md)
|
|
448
452
|
|
|
453
|
+
Whether motion should be reduced is a service, `Motion`, read when a
|
|
454
|
+
transition starts rather than sniffed once. `Presence` then exits at once,
|
|
455
|
+
and `Tween` and `Spring` jump to `to`, in the same Messages, so the Model sees
|
|
456
|
+
the same transitions. Provide `Motion.live` (the user's
|
|
457
|
+
`prefers-reduced-motion`) through the assembly's resources, or `Motion.reduced`
|
|
458
|
+
and `Motion.full` in a test or for a setting the application owns. With no
|
|
459
|
+
service provided, motion is full, so a placement that provides nothing behaves
|
|
460
|
+
as before. `Motion.reducedMotion` is the Effect the bundles read, for a
|
|
461
|
+
transition of your own.
|
|
462
|
+
|
|
449
463
|
`Tween` animates one number from `from` to `to` over `ms` milliseconds.
|
|
450
464
|
Model `{ value, running }`, Messages `Started`/`Ticked`/`Finished`, args
|
|
451
465
|
`{ from, to, ms }` (a non-positive or non-finite duration is rejected at
|
|
@@ -471,6 +485,274 @@ exact end value even when an underdamped spring overshoots on the way.
|
|
|
471
485
|
`Tween` (fixed duration, linear) versus `Spring` (physics, settles) — pick
|
|
472
486
|
the motion, not both.
|
|
473
487
|
|
|
488
|
+
## Interaction: `foldkit-primitives/interaction`
|
|
489
|
+
|
|
490
|
+
Interaction state that a view's slots must reflect: the Bundle holds it in the
|
|
491
|
+
Model, and a matching [`foldkit-mixins`](../mixins) Behavior writes the
|
|
492
|
+
attributes and handlers on the slots. Importing this subpath needs
|
|
493
|
+
`foldkit-mixins`; the other subpaths do not.
|
|
494
|
+
|
|
495
|
+
`RovingTabindex` is one tab stop for a set of items: arrows move focus between
|
|
496
|
+
them, the rest stay out of the tab order. The Model slice is `{ current }`, the
|
|
497
|
+
current item's **id**, so a reorder keeps the same item current and a resumed
|
|
498
|
+
page knows where focus was. Args: `orientation` (`'vertical' | 'horizontal' |
|
|
499
|
+
'both'`), `loop`, and `virtual` (focus stays on the container and
|
|
500
|
+
`aria-activedescendant` points at the current item).
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
import { Bundle } from 'foldkit-bundle'
|
|
504
|
+
import { Behavior, Behaviors, Capability, Slot, Slots, SlotView } from 'foldkit-mixins'
|
|
505
|
+
import { RovingTabindex } from 'foldkit-primitives/interaction'
|
|
506
|
+
|
|
507
|
+
const Roving = Bundle.declare(RovingTabindex.bundle, 'toolbarFocus')
|
|
508
|
+
const Model = Schema.Struct({ ...Roving.fields, tools: Schema.Array(Tool) })
|
|
509
|
+
const Message = defineMessageUnion({ ...Roving.cases })
|
|
510
|
+
const Page = Bundle.parent({ Model, Message })
|
|
511
|
+
const args = { orientation: 'horizontal', loop: true, virtual: false } as const
|
|
512
|
+
const placements = Page.assemble(Page.at(Roving, { args }))
|
|
513
|
+
|
|
514
|
+
const ToolbarSlots = Slots.define({
|
|
515
|
+
root: Slot.make({ capability: Capability.Container }),
|
|
516
|
+
tool: Slot.make({ capability: Capability.Focusable }),
|
|
517
|
+
})
|
|
518
|
+
const describeTools = (tools: ReadonlyArray<Tool>) =>
|
|
519
|
+
Behaviors.Collection.of(tools, { id: tool => tool.id, disabled: tool => tool.disabled })
|
|
520
|
+
|
|
521
|
+
// Ids on each item come from Collection; RovingTabindex reads them.
|
|
522
|
+
const Ids = Behaviors.Collection.behavior(ToolbarSlots)<Model, Message>({
|
|
523
|
+
item: 'tool',
|
|
524
|
+
items: model => describeTools(model.tools),
|
|
525
|
+
})
|
|
526
|
+
const Focus = RovingTabindex.behavior(Roving, args)(ToolbarSlots)<Model, Message>({
|
|
527
|
+
container: 'root',
|
|
528
|
+
item: 'tool',
|
|
529
|
+
items: model => describeTools(model.tools),
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
const Toolbar = SlotView.forMessages<Message>()
|
|
533
|
+
.define(ToolbarSlots, (model, slots, h) => {
|
|
534
|
+
const items = describeTools(model.tools)
|
|
535
|
+
return h.div(
|
|
536
|
+
slots.root.attrs([h.Role('toolbar')]),
|
|
537
|
+
model.tools.map((tool, index) =>
|
|
538
|
+
h.button(slots.tool.attrs([h.Key(tool.id)], items.slotItem(index)), [tool.label]),
|
|
539
|
+
),
|
|
540
|
+
)
|
|
541
|
+
})
|
|
542
|
+
.pipe(Behavior.attach(Ids), Behavior.attach(Focus))
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
What each half does: the container gets `OnKeyDownFocus`, which on an arrow,
|
|
546
|
+
Home or End focuses the next **enabled** item synchronously by its id, prevents
|
|
547
|
+
the default, and dispatches `Focused { id }`. Each item gets `tabindex` `0` when
|
|
548
|
+
it is the tab stop and `-1` otherwise, and `OnFocus` reporting `Focused`, so a
|
|
549
|
+
click makes an item current too. Before anything is current, or when the
|
|
550
|
+
current item is gone or disabled, the first enabled item is the tab stop. Keys
|
|
551
|
+
with ctrl, alt or meta held are left alone; `direction: model => 'rtl'` swaps
|
|
552
|
+
left and right. The pure `move(enabled, current, key, modifiers, options)` and
|
|
553
|
+
`tabStop(items, current)` are exported for a view that wires its own.
|
|
554
|
+
|
|
555
|
+
Under `virtual` the items get no `tabindex`, the container gets
|
|
556
|
+
`aria-activedescendant`, and a key keeps DOM focus where it is and only moves
|
|
557
|
+
the pointer. Nothing is written on dispose: the attributes are data, so a view
|
|
558
|
+
that no longer attaches the Behavior leaves no `tabindex` behind. PageUp and
|
|
559
|
+
PageDown are handled when `move` is given a `page`; `ListNavigation` below
|
|
560
|
+
does that.
|
|
561
|
+
|
|
562
|
+
`Typeahead` is type-to-find for a host that has no roving tab stop, or whose
|
|
563
|
+
focus is managed elsewhere. The Model slice is `{ query, generation }`:
|
|
564
|
+
printable keys extend the query, a timer of `timeoutMs` on Effect's clock
|
|
565
|
+
clears it (`generation` lets a superseded timer change nothing), and `Cleared`
|
|
566
|
+
drops it on purpose. Which item a query picks is the pure
|
|
567
|
+
`Typeahead.match(texts, enabled, query, current)`: one character, or one
|
|
568
|
+
character repeated, starts *after* the current item so repeated presses cycle;
|
|
569
|
+
a longer query starts *at* it, since the user is refining. Case and leading
|
|
570
|
+
whitespace are ignored and disabled items are skipped. The Behavior
|
|
571
|
+
(`Typeahead.behavior(Declared)(Slots)<Model, Message>({ host, items, text,
|
|
572
|
+
current })`) gives the host `OnKeyDownFocus`: a printable key with no ctrl, alt
|
|
573
|
+
or meta extends the query, focuses the match by id, and dispatches `Typed`;
|
|
574
|
+
with no match the key is still recorded and focus stays put; a space with an
|
|
575
|
+
empty query is left to the host.
|
|
576
|
+
|
|
577
|
+
`ListNavigation` is what a list host takes when it wants both: arrows, Home,
|
|
578
|
+
End, PageUp and PageDown by `page`, and typeahead, in **one placement**. It
|
|
579
|
+
exists for two reasons. The resolver allows one owner per event on a slot, so
|
|
580
|
+
`RovingTabindex` and `Typeahead` cannot both own the host's `OnKeyDownFocus`;
|
|
581
|
+
and under `virtual` a typed key must move the pointer and extend the query in
|
|
582
|
+
one transition, which two placements cannot do. Its Model slice is `{ current,
|
|
583
|
+
query, generation }`, its args are `RovingTabindex`'s plus `timeoutMs` and
|
|
584
|
+
`page`, and its Behavior takes `{ container, item, items, text, direction? }`.
|
|
585
|
+
`Typed { char, match }` carries the item the query now picks, so `update` sets
|
|
586
|
+
`current` and `query` together.
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
const Nav = Bundle.declare(ListNavigation.bundle, 'nav')
|
|
590
|
+
// place with { args: { orientation: 'vertical', loop: false, virtual: false, timeoutMs: 500, page: 10 } }
|
|
591
|
+
const Keys = ListNavigation.behavior(Nav, args)(ListSlots)<Model, Message>({
|
|
592
|
+
container: 'list',
|
|
593
|
+
item: 'option',
|
|
594
|
+
items: model => describeFruits(model.fruits),
|
|
595
|
+
text: (model, index) => model.fruits[index]?.label ?? '',
|
|
596
|
+
})
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
`GridNavigation` is the two-dimensional counterpart for cells laid out in rows
|
|
600
|
+
of `columns` (a calendar grid, a swatch picker, an emoji palette). Its Model
|
|
601
|
+
slice and item attributes are `RovingTabindex`'s, so the two are
|
|
602
|
+
interchangeable on a view; only the pure `move` differs. Left and right step
|
|
603
|
+
within the row and up and down within the column, skipping disabled cells;
|
|
604
|
+
under `wrap` a horizontal key continues into the next row and a vertical key
|
|
605
|
+
into the next column, otherwise the key is consumed at the edge. Home and End
|
|
606
|
+
are the row's first and last enabled cell, Ctrl+Home and Ctrl+End the grid's.
|
|
607
|
+
RTL swaps left and right, and `virtual` works as it does for `RovingTabindex`.
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
const Cells = Bundle.declare(GridNavigation.bundle, 'cells')
|
|
611
|
+
// place with { args: { columns: 7, wrap: false, virtual: false } }
|
|
612
|
+
const Keys = GridNavigation.behavior(Cells, args)(CalendarSlots)<Model, Message>({
|
|
613
|
+
container: 'grid',
|
|
614
|
+
item: 'day',
|
|
615
|
+
items: model => describeDays(model.days),
|
|
616
|
+
})
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
`FocusScope` is the one entry here that is a Mount, not a Bundle: which
|
|
620
|
+
element has focus is a DOM fact, so nothing crosses to the Model. The Mount
|
|
621
|
+
lives in `foldkit-primitives/dom`; `FocusScope.behavior(Slots)<Input,
|
|
622
|
+
Message>({ container, contain?, restore?, initialFocus? })` attaches it to a
|
|
623
|
+
container slot. On insert the container focuses `initialFocus`, else its first
|
|
624
|
+
tabbable descendant, else itself. With `contain` (default), Tab from the last
|
|
625
|
+
tabbable wraps to the first, Shift+Tab from the first wraps to the last, and
|
|
626
|
+
focus that lands outside comes straight back; Tab in the middle is the
|
|
627
|
+
browser's. On unmount, with `restore` (default), focus returns to the element
|
|
628
|
+
that had it, if it is still in the document. A native `<dialog>` does all of
|
|
629
|
+
this itself; this is for a custom overlay, a menu, or a command palette.
|
|
630
|
+
`tabbableWithin(element)` is exported: focusable, visible descendants with a
|
|
631
|
+
non-negative `tabindex`, in order.
|
|
632
|
+
|
|
633
|
+
`Press` turns pointer and keyboard activation of one element into one fact.
|
|
634
|
+
Foldkit's declarative pointer attributes carry no button, pointer id, or click
|
|
635
|
+
detail, so `Press.events` is a Mount that reports what the element saw
|
|
636
|
+
(`PointerDown`, `PointerUp`, `PointerCancelled`, `KeyDown`, `KeyUp`,
|
|
637
|
+
`Clicked`), and the Bundle's `update` decides: primary button only, one
|
|
638
|
+
pointer at a time, `pointerleave` and `pointercancel` cancel, Enter and Space
|
|
639
|
+
with a repeat ignored, a click with `detail` 0 (keyboard on a native control,
|
|
640
|
+
or assistive technology) counts, and the ghost click that follows a touch is
|
|
641
|
+
ignored inside a window of `clickSuppressionMs` that a Command on Effect's
|
|
642
|
+
clock closes. Enter and Space are default-prevented on the element, so a
|
|
643
|
+
native control does not also click and Space does not scroll. Activation is
|
|
644
|
+
the OutMessage `Pressed { pointerType, shiftKey }`, and the placement must handle it:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
const Button = Bundle.declare(Press.bundle, 'saveButton')
|
|
648
|
+
const placements = Page.assemble(
|
|
649
|
+
Page.at(Button, {
|
|
650
|
+
args: { clickSuppressionMs: 50 },
|
|
651
|
+
onOut: () => model => ({ model, commands: [save(model)] }),
|
|
652
|
+
}),
|
|
653
|
+
)
|
|
654
|
+
const Activate = Press.behavior(Button)(CardSlots)<Model, Message>({
|
|
655
|
+
target: 'save',
|
|
656
|
+
disabled: model => model.saving,
|
|
657
|
+
})
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
The Behavior attaches the Mount to the target slot, writes `data-pressed`
|
|
661
|
+
while the element is down for styling, and marks a disabled target
|
|
662
|
+
`aria-disabled`, which the Mount reads at event time so nothing is reported
|
|
663
|
+
and no remount is needed. The Model slice is `{ pressed, pointerId, key,
|
|
664
|
+
suppressing, generation }`; only `pressed` is meant for a view.
|
|
665
|
+
|
|
666
|
+
`LongPress` is holding for `thresholdMs`. It reads the same facts
|
|
667
|
+
`Press.events` reports, so it needs no Mount of its own; the threshold is a
|
|
668
|
+
Command on Effect's clock carrying a generation, and a release before it fires
|
|
669
|
+
makes its `Elapsed` a no-op. `LongPressed { pointerType }` is the OutMessage,
|
|
670
|
+
required at placement. The Behavior writes `data-holding` while down. `Press`
|
|
671
|
+
and `LongPress` on one slot are refused by the resolver, since both would mount
|
|
672
|
+
`PressEvents`; a slot takes one of them.
|
|
673
|
+
|
|
674
|
+
`Move` is pointer movement as facts, a Mount in `foldkit-primitives/dom`: a
|
|
675
|
+
primary-button pointer down captures the pointer and reports `MoveStarted`,
|
|
676
|
+
each move reports `Moved { deltaX, deltaY }` from where it went down, and up,
|
|
677
|
+
cancel, or lost capture reports `MoveEnded { completed }`. A second pointer
|
|
678
|
+
and a secondary button are ignored; capture is released with the Mount.
|
|
679
|
+
`Move.behavior(Slots)<Input, Message>({ handle, toMessage })` attaches it to a
|
|
680
|
+
`Draggable` slot and maps each fact into the view's Messages; a drag's meaning
|
|
681
|
+
(a threshold, a snap, a reorder) is the parent's `update`.
|
|
682
|
+
|
|
683
|
+
`FocusVisible` is the one entry whose Bundle lives elsewhere: `InputModality`
|
|
684
|
+
in `foldkit-primitives/events` keeps `{ modality }` (`'keyboard'`, `'pointer'`,
|
|
685
|
+
or `'unknown'` before any input), fed by the window's `keydown` (a modifier
|
|
686
|
+
alone says nothing) and `pointerdown`. `FocusVisible.behavior(Declared)(Slots)
|
|
687
|
+
<Model, Message>({ target })` writes `data-focus-visible` on the target while
|
|
688
|
+
the page is driven by keyboard, so a stylesheet shows a ring with
|
|
689
|
+
`[data-focus-visible]:focus`. CSS `:focus-visible` does this with no Model at
|
|
690
|
+
all; this is for a design system that must decide in the Model, or show the
|
|
691
|
+
same answer somewhere other than the focused element.
|
|
692
|
+
|
|
693
|
+
`DismissLayer` closes overlays that are not native `<dialog>` or `popover`
|
|
694
|
+
elements: Escape closes the topmost open layer, and a pointer press closes
|
|
695
|
+
the layers it is outside of. One Bundle, **placed once**, owns the document
|
|
696
|
+
listeners; each layer's Behavior marks its container with
|
|
697
|
+
`data-foldkit-plus-layer="<id>"` and its trigger with the matching trigger
|
|
698
|
+
attribute. The stack is the DOM order of the marked elements at the moment of
|
|
699
|
+
the event, so a layer takes part exactly while it is rendered and an `open`
|
|
700
|
+
flag in the parent Model is its whole lifecycle; nothing registers. The rules,
|
|
701
|
+
each a test: a press inside a parent layer is outside its children, so the
|
|
702
|
+
parent stays and the children go; a press on a layer's trigger counts as
|
|
703
|
+
inside it, so a click on the trigger never dismisses and reopens; a layer
|
|
704
|
+
placed with `outsidePress: false` or `escape: false` opts out of that path.
|
|
705
|
+
`Dismiss { ids }` is the OutMessage, and the placement's `onOut` closes them:
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
const Layers = Bundle.declare(DismissLayer.bundle, 'layers')
|
|
709
|
+
const placements = Page.assemble(
|
|
710
|
+
Page.at(Layers, {
|
|
711
|
+
onOut: ({ ids }) => model => ({ model: { ...model, menuOpen: ids.includes('menu') ? false : model.menuOpen } }),
|
|
712
|
+
}),
|
|
713
|
+
)
|
|
714
|
+
const Dismissable = DismissLayer.behavior(Layers)(MenuSlots)<Model, Message>({
|
|
715
|
+
layer: 'panel',
|
|
716
|
+
trigger: 'button',
|
|
717
|
+
id: () => 'menu',
|
|
718
|
+
})
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
The Model slice is `{ layers }`, the open layers as the last event saw them,
|
|
722
|
+
for DevTools and agents. `toDismiss(layers, inside)` is the pure rule.
|
|
723
|
+
|
|
724
|
+
`Layers.scrollLock(Slots)({ container })` and `Layers.hideOutside(Slots)({
|
|
725
|
+
container })` are Mounts over Foldkit's own `Dom.lockScroll` and
|
|
726
|
+
`Dom.inertOthers`: the first locks the document's scroll while the container is
|
|
727
|
+
mounted, refcounted so nested overlays release together, with Foldkit's iOS
|
|
728
|
+
handling; the second marks everything outside the container inert while it is
|
|
729
|
+
mounted, keyed by an id the Mount mints so two overlays restore independently.
|
|
730
|
+
Both live in `foldkit-primitives/dom` as `ScrollLock` and `HideOutside`. A
|
|
731
|
+
native `<dialog>` shown modally needs neither.
|
|
732
|
+
|
|
733
|
+
`Selection` is which items are selected, with `mode` `'single'` (a click
|
|
734
|
+
replaces; `allowEmpty` says whether clicking the selected item deselects it),
|
|
735
|
+
`'multiple'` (a click toggles), or `'none'`, and the `anchor` a range extends
|
|
736
|
+
from. The Model slice is `{ selected, anchor }`. A range needs the items'
|
|
737
|
+
order, which the view knows and the Bundle does not, so `Ranged { id, order }`
|
|
738
|
+
carries it; `Selection.between(order, from, to)` is the pure span. The Behavior
|
|
739
|
+
(`Selection.behavior(Declared, args)(Slots)<Model, Message>({ container?,
|
|
740
|
+
item, items, click? })`) writes `aria-selected` on each item and
|
|
741
|
+
`aria-multiselectable` on the container, and wires a plain click on each
|
|
742
|
+
enabled item to `Activated`; pass `click: false` when `Press` or the view owns
|
|
743
|
+
the click. For a Shift range, `Press`'s `Pressed { pointerType, shiftKey }`
|
|
744
|
+
says whether Shift was held, and the placement's `onOut` dispatches `Ranged`.
|
|
745
|
+
|
|
746
|
+
`LiveAnnounce` speaks to assistive technology: one Bundle, placed once, holds
|
|
747
|
+
the text of a polite and an assertive live region. `say(Declared)(text,
|
|
748
|
+
politeness?)` builds the Message to return from `update` or an `onOut`; an
|
|
749
|
+
announcement waits `debounceMs` so a burst reads once, then clears after
|
|
750
|
+
`clearAfterMs`, both on Effect's clock with a generation so a superseded timer
|
|
751
|
+
changes nothing; the same text twice gets a trailing no-break space toggled,
|
|
752
|
+
which is what makes a screen reader read it again. `LiveAnnounce.view(slice,
|
|
753
|
+
h)` renders the two regions: put it once in the page and hide them visually
|
|
754
|
+
with a rule on `[data-foldkit-plus-live]`, never `display: none`.
|
|
755
|
+
|
|
474
756
|
## Testing placements
|
|
475
757
|
|
|
476
758
|
Every primitive is testable without the platform, following one pattern
|
|
@@ -502,7 +784,7 @@ application uses.
|
|
|
502
784
|
| A Command without its platform API | failure Message (`CopyFailed`, `ShareFailed`, `BroadcastFailed`), never a throw |
|
|
503
785
|
| Listener removed (unmount, gate closed) | finalizer disconnects; resubscribing re-reads the current value |
|
|
504
786
|
| A Message for an unplaced child | never routes: wrappers only match placed variants |
|
|
505
|
-
|
|
|
787
|
+
| MediaStream acquisition fails | parks at `denied`/`idle` with requirements cleared; `Started` retries |
|
|
506
788
|
|
|
507
789
|
## Limits
|
|
508
790
|
|
package/dist/device/index.d.mts
CHANGED
|
@@ -59,7 +59,7 @@ export declare const GeolocationMessage: import("foldkit/message").MessageUnion<
|
|
|
59
59
|
}>;
|
|
60
60
|
export type GeolocationMessage = typeof GeolocationMessage.Type;
|
|
61
61
|
export declare const Geolocation: Bundle.Bundle<"Geolocation", void, {
|
|
62
|
-
readonly status: "unknown" | "
|
|
62
|
+
readonly status: "unknown" | "denied" | "ready";
|
|
63
63
|
readonly coords: {
|
|
64
64
|
readonly latitude: number;
|
|
65
65
|
readonly longitude: number;
|
|
@@ -67,8 +67,6 @@ export declare const Geolocation: Bundle.Bundle<"Geolocation", void, {
|
|
|
67
67
|
} | null;
|
|
68
68
|
readonly lastError: string | null;
|
|
69
69
|
}, {
|
|
70
|
-
readonly _tag: "Denied";
|
|
71
|
-
} | {
|
|
72
70
|
readonly _tag: "Failed";
|
|
73
71
|
readonly message: string;
|
|
74
72
|
} | {
|
|
@@ -76,6 +74,8 @@ export declare const Geolocation: Bundle.Bundle<"Geolocation", void, {
|
|
|
76
74
|
readonly latitude: number;
|
|
77
75
|
readonly longitude: number;
|
|
78
76
|
readonly accuracy: number;
|
|
77
|
+
} | {
|
|
78
|
+
readonly _tag: "Denied";
|
|
79
79
|
}, never, never, never, void, {}, {}>;
|
|
80
80
|
//#endregion
|
|
81
81
|
//#region src/device/media-devices.d.ts
|
|
@@ -130,7 +130,7 @@ export declare const mediaDevices: <const Name extends string>(config: {
|
|
|
130
130
|
/** Read lazily so tests can substitute a double, like the SSE bundle. */
|
|
131
131
|
readonly create?: ((navigator: Navigator) => MediaDevicesHandle | null) | undefined;
|
|
132
132
|
}) => Bundle.Bundle<Name, void, {
|
|
133
|
-
readonly status: "unknown" | "
|
|
133
|
+
readonly status: "unknown" | "denied" | "ready";
|
|
134
134
|
readonly devices: readonly {
|
|
135
135
|
readonly deviceId: string;
|
|
136
136
|
readonly groupId: string;
|
|
@@ -139,6 +139,8 @@ export declare const mediaDevices: <const Name extends string>(config: {
|
|
|
139
139
|
}[];
|
|
140
140
|
readonly lastError: string | null;
|
|
141
141
|
}, {
|
|
142
|
+
readonly _tag: "Denied";
|
|
143
|
+
} | {
|
|
142
144
|
readonly _tag: "Failed";
|
|
143
145
|
readonly message: string;
|
|
144
146
|
} | {
|
|
@@ -153,8 +155,6 @@ export declare const mediaDevices: <const Name extends string>(config: {
|
|
|
153
155
|
readonly kind: "audioinput" | "audiooutput" | "videoinput";
|
|
154
156
|
readonly label: string;
|
|
155
157
|
}[];
|
|
156
|
-
} | {
|
|
157
|
-
readonly _tag: "Denied";
|
|
158
158
|
}, never, never, never, void, {}, {}>;
|
|
159
159
|
//#endregion
|
|
160
160
|
//#region src/device/media-stream.d.ts
|
|
@@ -288,23 +288,23 @@ export declare const permissions: <const Name extends string>(config: {
|
|
|
288
288
|
readonly names: ReadonlyArray<string>;
|
|
289
289
|
}, {
|
|
290
290
|
readonly states: {
|
|
291
|
-
readonly [x: string]: "
|
|
291
|
+
readonly [x: string]: "granted" | "denied" | "prompt";
|
|
292
292
|
};
|
|
293
293
|
readonly lastError: string | null;
|
|
294
294
|
}, {
|
|
295
|
+
readonly _tag: "Cleared";
|
|
296
|
+
} | {
|
|
295
297
|
readonly _tag: "Changed";
|
|
296
298
|
readonly name: string;
|
|
297
|
-
readonly state: "
|
|
299
|
+
readonly state: "granted" | "denied" | "prompt";
|
|
298
300
|
} | {
|
|
299
301
|
readonly _tag: "Failed";
|
|
300
302
|
readonly message: string;
|
|
301
303
|
} | {
|
|
302
304
|
readonly _tag: "Snapshot";
|
|
303
305
|
readonly states: {
|
|
304
|
-
readonly [x: string]: "
|
|
306
|
+
readonly [x: string]: "granted" | "denied" | "prompt";
|
|
305
307
|
};
|
|
306
|
-
} | {
|
|
307
|
-
readonly _tag: "Cleared";
|
|
308
308
|
}, never, never, ManagedResource.ManagedResourceService<"permissions">, void, Readonly<{
|
|
309
309
|
watch: ManagedResource.Entry<PermissionsModel, PermissionsMessage, Option.Option<ReadonlyArray<string>>, Acquired, WatchService, (acquired: Acquired) => PermissionsMessage>;
|
|
310
310
|
}>, {}>;
|
package/dist/dom/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as Moved, c as FocusScope, i as MoveStarted, l as tabbableWithin, n as Move, o as HideOutside, r as MoveEnded, s as ScrollLock, t as Fact } from "../move-DNiMPcuf.mjs";
|
|
1
2
|
import { Schema } from "effect";
|
|
2
3
|
import * as Mount from "foldkit/mount";
|
|
3
4
|
import { Command } from "foldkit/command";
|
|
@@ -70,4 +71,5 @@ export type ShareMessage = typeof ShareMessage.Type;
|
|
|
70
71
|
/** Shares data through the platform sheet. */
|
|
71
72
|
export declare const share: (data: ShareData) => Command<ShareMessage, never, never>;
|
|
72
73
|
//#endregion
|
|
74
|
+
export { Fact, FocusScope, HideOutside, Move, MoveEnded, MoveStarted, Moved, ScrollLock, tabbableWithin };
|
|
73
75
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/dom/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts"],"mappings":";;;;;qBAUa,SAAO,OAAA;YACR,iBAAiB,QAAQ;qBAExB,WAAS,MAAA;;;;;qBCJT,4CAAgB;;;sBAAhB,OAAA;;;YAID,0BAA0B,iBAAiB;;;;;;qBAU1C,WAAY,iBAAe,QAAQ;;;qBCXnC,OAAK,OAAA;;;;YAIN,eAAe,MAAM;qBA2BpB,YACX,eACA;WACY;WAAyB;;qBAoC1B,WAAS,MAAA;;;;;;;;;qBCvET,yCAAa;;;sBAAb,OAAA;;;YAID,uBAAuB,cAAc;;qBASpC,aAAc,gBAAc,QAAQ;;;qBCbpC,WAAS,OAAA;;;;;YAKV,mBAAmB,UAAU;qBAE5B,wCAAY;;;;sBAAZ,OAAA;;;YAKD,sBAAsB,aAAa;;qBAUlC,QAAS,MAAM,cAAY,QAAQ"}
|
package/dist/dom/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as HideOutside, c as tabbableWithin, i as Moved, n as MoveEnded, o as ScrollLock, r as MoveStarted, s as FocusScope, t as Move } from "../move-CZwHyTGk.mjs";
|
|
1
2
|
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
3
|
import * as Mount from "foldkit/mount";
|
|
3
4
|
import { defineMessageUnion } from "foldkit/message";
|
|
@@ -213,6 +214,6 @@ const share = (data) => ({
|
|
|
213
214
|
}) : Effect.succeed(ShareMessage.ShareFailed({ message: "share is unavailable" }))
|
|
214
215
|
});
|
|
215
216
|
//#endregion
|
|
216
|
-
export { Autofocus, ClipboardMessage, Focused, Input, InputMask, ScriptMessage, ShareData, ShareMessage, applyMask, copyText, loadScript, share };
|
|
217
|
+
export { Autofocus, ClipboardMessage, FocusScope, Focused, HideOutside, Input, InputMask, Move, MoveEnded, MoveStarted, Moved, ScriptMessage, ScrollLock, ShareData, ShareMessage, applyMask, copyText, loadScript, share, tabbableWithin };
|
|
217
218
|
|
|
218
219
|
//# sourceMappingURL=index.mjs.map
|
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/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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"}
|
|
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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"}
|