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 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
- Note the direction: init is pure, so SSR renders the default; the stream corrects it live.
64
+ The parent Model owns the recorded fact; subscriptions report environment changes.
61
65
  ```
62
66
 
63
- Every primitive follows one lifecycle: `init` returns a safe default without
64
- touching the environment (SSR-safe by construction). Streams report live
65
- facts — MediaQuery reads the current match first, then changes; Timer ticks
66
- while running. A primitive that cannot observe (no `window`, no API) yields
67
- an empty stream instead of throwing, so the slice keeps its default.
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
- **Declare where it lives.** A declaration names the Model field and the
98
- Message variant, by Foldkit's `Got<Field>Message` convention:
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 { defineMessageUnion } from 'foldkit/message'
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 Dark = Bundle.declare(MediaQuery, 'dark')
107
-
108
- const Model = Schema.Struct({ ...Dark.fields, theme: Schema.String })
109
- type Model = typeof Model.Type
110
- const Message = defineMessageUnion({ ...Dark.cases, ThemeSet: { theme: Schema.String } })
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
- **Place it.** The scope types everything from the parent's Schemas; the
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 Page = Bundle.parent({ Model, Message })
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 the placement calls perform I/O:
132
- `declare` names a slot, `at` binds config to it, `assemble` derives the
133
- folding and the streams — the browser is touched only when the runtime
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 placements = Page.assemble(Page.place(PrefersDark, 'dark'))
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. They keep observing across time-travel pause — replay traffic
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. Both are pure logic, no streams.
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
- | A failing device (denied camera, unknown permission) | parks at `denied`/`idle` with requirements cleared: no acquire loop |
787
+ | MediaStream acquisition fails | parks at `denied`/`idle` with requirements cleared; `Started` retries |
506
788
 
507
789
  ## Limits
508
790
 
@@ -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" | "ready" | "denied";
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" | "ready" | "denied";
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]: "denied" | "granted" | "prompt";
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: "denied" | "granted" | "prompt";
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]: "denied" | "granted" | "prompt";
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
  }>, {}>;
@@ -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
@@ -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":";;;;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"}
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"}
@@ -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
@@ -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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"}