foldkit-primitives 0.2.0 → 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
@@ -1,6 +1,6 @@
1
1
  # `foldkit-primitives`
2
2
 
3
- Ready-made [`foldkit-bundle`](./bundle) primitives: media queries, breakpoints,
3
+ Ready-made [`foldkit-bundle`](../bundle) primitives: media queries, breakpoints,
4
4
  presence, timers, intervals, debounce, tweens, springs, pagination, history,
5
5
  locales, selections, ranges, geolocation, cameras, permissions, sockets,
6
6
  broadcasts, observers, and clipboard. Most are ordinary bundles — Model,
@@ -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 |
@@ -33,6 +37,15 @@ package depends on `foldkit-bundle` and nothing else (besides peers).
33
37
  | One-shot actions | Command, not a bundle | clipboard copy, share, script load |
34
38
  | Derived data | pure function, not a bundle | range, relative time, platform |
35
39
 
40
+ Choose by what you need to own. State that outlives the moment — a match,
41
+ a count, a page, a position — wants a bundle: the Model keeps it, replay
42
+ sees it. A stream you only react to wants an entry: map it into the
43
+ parent's Message and keep nothing. Work bound to one element wants a
44
+ Mount. Work that runs once and reports back wants a Command. A value
45
+ computed from data you already have wants a pure function. When in doubt,
46
+ start with the lighter form; promote to a bundle the day the state needs
47
+ a name in the Model.
48
+
36
49
  A bundle holds no state and performs no I/O by itself. The parent Model owns
37
50
  the placed slice; the browser (or server, or clock) only reports facts as
38
51
  Messages. The same rule as everywhere else: observation is not ownership.
@@ -48,14 +61,14 @@ accident — one state machine leaves nowhere for a second one to live.
48
61
  ```text
49
62
  browser / clock / server ──facts as Messages──▶ update ──▶ Model slice
50
63
  ▲
51
- 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.
52
65
  ```
53
66
 
54
- Every primitive follows one lifecycle: `init` returns a safe default without
55
- touching the environment (SSR-safe by construction). Streams report live
56
- facts — MediaQuery reads the current match first, then changes; Timer ticks
57
- while running. A primitive that cannot observe (no `window`, no API) yields
58
- 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.
59
72
 
60
73
  ## Install
61
74
 
@@ -69,49 +82,80 @@ pnpm add foldkit-primitives foldkit-bundle effect foldkit
69
82
  `foldkit-primitives/events`, `foldkit-primitives/observers`, `foldkit-primitives/dom` —
70
83
  so bundlers drop the primitives you never import.
71
84
 
85
+ ## Map of the package
86
+
87
+ Each subpath is one concern, one import:
88
+
89
+ - `media` — environment facts: MediaQuery (+presets), Breakpoints, platform
90
+ - `net` — remote facts: Online, WebSocket, SSE, BroadcastChannel
91
+ - `time` — clock facts: Timer, Interval, Debounce, Throttle, relative time
92
+ - `state` — owned UI state: Pagination, History, Locale, SelectionSet, Virtual, range
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
95
+ - `device` — hardware: Geolocation, MediaDevices, MediaStream, Permissions, Fullscreen
96
+ - `events` — raw browser events: Visibility, WindowSize, Idle, InputModality, keyboard, pointer, scroll, focus
97
+ - `observers` — element Mounts: Resize, Intersection, Mutation, Bounds
98
+ - `dom` — element Mounts and one-shot Commands: Autofocus, FocusScope, Move, ScrollLock, HideOutside, InputMask, clipboard, share, script loading
99
+
72
100
  ## Sixty seconds: follow the color scheme
73
101
 
74
- **Declare where it lives.** A declaration names the Model field and the
75
- 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:
76
105
 
77
106
  ```ts
78
107
  import { Schema } from 'effect'
79
- import { defineMessageUnion } from 'foldkit/message'
108
+ import type { HtmlBuilder } from 'foldkit/html'
80
109
  import { Bundle } from 'foldkit-bundle'
81
110
  import { MediaQuery } from 'foldkit-primitives/media'
82
111
 
83
- const Dark = Bundle.declare(MediaQuery, 'dark')
84
-
85
- const Model = Schema.Struct({ ...Dark.fields, theme: Schema.String })
86
- type Model = typeof Model.Type
87
- const Message = defineMessageUnion({ ...Dark.cases, ThemeSet: { theme: Schema.String } })
88
- 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
89
118
  ```
90
119
 
91
- **Place it.** The scope types everything from the parent's Schemas; the
92
- assembly derives the update and the Subscriptions:
120
+ **Run it.** The parent's assembly derives the update and the Subscriptions:
93
121
 
94
122
  ```ts
95
- const Page = Bundle.parent({ Model, Message })
96
- const placements = Page.assemble(Page.at(Dark, { args: { query: '(prefers-color-scheme: dark)' } }))
123
+ const { placements } = Page
97
124
 
98
125
  const config = placements.complete({
99
126
  init: () => placements.initial({ theme: 'light' }),
100
127
  update: placements.update(model => ({ model })),
101
- view,
128
+ view: (model: Model, h: HtmlBuilder<Message>) =>
129
+ h.div([], [model.dark.matches ? 'Dark mode' : 'Light mode']),
102
130
  subscriptions: placements.subscriptions(),
103
131
  })
104
132
  ```
105
133
 
106
134
  `update` routes `GotDarkMessage` to the bundle; `subscriptions` runs the
107
135
  `matchMedia` stream; `initial` starts `matches` at `false` and the stream
108
- corrects it on subscribe. 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:
109
140
 
110
141
  ```ts
111
142
  // solid-primitives: const dark = createMediaQuery('(prefers-color-scheme: dark)')
112
143
  // Here the fact lives in the Model: replay, DevTools, and time travel see it.
113
144
  ```
114
145
 
146
+ ## Composing placements
147
+
148
+ One assembly holds every placement: spread its update, init,
149
+ subscriptions, and resources once per application, as above. Two
150
+ placements of one bundle observe twice — share the field instead, so one
151
+ stream feeds every reader. One assembly holds one socket, stream, or
152
+ watch: resource tags are per module, and `assemble` refuses the second.
153
+ Bundles whose settled value is the whole point (`Debounce`, `Throttle`)
154
+ surface it as an OutMessage the placement handles with `onOut`, so the
155
+ signal can never be dropped by omission. Entries lift with
156
+ `Subscription.persistent`, mapping into the parent's Message; Mounts
157
+ attach in views with `h.OnMount`.
158
+
115
159
  ## Media: `foldkit-primitives/media`
116
160
 
117
161
  > Reference: [`./media/README.md`](./media/README.md)
@@ -126,7 +170,7 @@ Bound presets place with no args:
126
170
  ```ts
127
171
  import { PrefersDark, PrefersReducedMotion } from 'foldkit-primitives/media'
128
172
 
129
- const placements = Page.assemble(Page.place(PrefersDark, 'dark'))
173
+ const Page = Bundle.compose({ theme: Schema.String }).pipe(Bundle.withChild('dark', PrefersDark))
130
174
  ```
131
175
 
132
176
  `Breakpoints` derives names from one `resize` listener: args
@@ -266,8 +310,7 @@ skipped. `Bounds()` re-measures `Measured { x, y, width, height }` on
266
310
  observer, scroll, and resize, starting with the current rect; without a
267
311
  ResizeObserver the window events still measure. Without the observer API
268
312
  (SSR, old browser) they emit nothing instead of throwing; teardown
269
- disconnects. They keep observing across time-travel pause — replay traffic
270
- is same-valued and harmless.
313
+ disconnects. Treat repeated measurements as observations, not proof that a user action occurred.
271
314
 
272
315
  ## Device: `foldkit-primitives/device`
273
316
 
@@ -362,7 +405,7 @@ const Doc = Bundle.declare(EditHistory, 'doc')
362
405
  startup with the configured `default` as fallback; `SetLocale` switches it.
363
406
  `SelectionSet` keeps string ids in first-selection order: `Select` (keeps
364
407
  position), `Deselect`, `Toggle` (re-appends), `ReplaceAll` (deduped), and
365
- `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.
366
409
  Keyed children — lists with stable identity — place through the bundle
367
410
  mechanism's `each`.
368
411
  `range(start, end, step?)` counts half-open numbers — the pagination page
@@ -403,19 +446,20 @@ browser), restored through a `MirrorRestored` Message the application
403
446
  reduces. Nothing here duplicates it — reach for the mirror when a slice
404
447
  should survive reload, and keep this package's bundles for live facts.
405
448
 
406
- ## With Surface and Mirror
407
-
408
- Placed state is ordinary Model, so the surrounding tools apply unchanged —
409
- no bundle-specific Surface or Mirror API exists, by design. Declare a Surface
410
- over the placed fields to render them or expose them to an agent; point
411
- `Mirror.url` at them to link them; spread the bundle's cases into the same
412
- unions. The field refs and wrapper Messages are the same ones the rest of the
413
- application uses.
414
-
415
449
  ## Motion: `foldkit-primitives/motion`
416
450
 
417
451
  > Reference: [`./motion/README.md`](./motion/README.md)
418
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
+
419
463
  `Tween` animates one number from `from` to `to` over `ms` milliseconds.
420
464
  Model `{ value, running }`, Messages `Started`/`Ticked`/`Finished`, args
421
465
  `{ from, to, ms }` (a non-positive or non-finite duration is rejected at
@@ -441,6 +485,274 @@ exact end value even when an underdamped spring overshoots on the way.
441
485
  `Tween` (fixed duration, linear) versus `Spring` (physics, settles) — pick
442
486
  the motion, not both.
443
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
+
444
756
  ## Testing placements
445
757
 
446
758
  Every primitive is testable without the platform, following one pattern
@@ -455,6 +767,15 @@ forking before the first `TestClock.adjust`, or dispatched events hit
455
767
  unregistered listeners. Each ingredient is demonstrated in this package's
456
768
  `test/` directory, named after its primitive.
457
769
 
770
+ ## With Surface and Mirror
771
+
772
+ Placed state is ordinary Model, so the surrounding tools apply unchanged —
773
+ no bundle-specific Surface or Mirror API exists, by design. Declare a Surface
774
+ over the placed fields to render them or expose them to an agent; point
775
+ `Mirror.url` at them to link them; spread the bundle's cases into the same
776
+ unions. The field refs and wrapper Messages are the same ones the rest of the
777
+ application uses.
778
+
458
779
  ## Failure and recovery
459
780
 
460
781
  | Failure | Behaviour |
@@ -463,7 +784,7 @@ unregistered listeners. Each ingredient is demonstrated in this package's
463
784
  | A Command without its platform API | failure Message (`CopyFailed`, `ShareFailed`, `BroadcastFailed`), never a throw |
464
785
  | Listener removed (unmount, gate closed) | finalizer disconnects; resubscribing re-reads the current value |
465
786
  | A Message for an unplaced child | never routes: wrappers only match placed variants |
466
- | 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 |
467
788
 
468
789
  ## Limits
469
790
 
@@ -292,6 +292,8 @@ export declare const permissions: <const Name extends string>(config: {
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
299
  readonly state: "granted" | "denied" | "prompt";
@@ -303,8 +305,6 @@ export declare const permissions: <const Name extends string>(config: {
303
305
  readonly states: {
304
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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"}