foldkit-primitives 0.3.0 → 0.4.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 +202 -13
- package/dist/device/index.d.mts.map +1 -1
- package/dist/device/index.mjs +95 -87
- package/dist/device/index.mjs.map +1 -1
- package/dist/dom/index.d.mts +71 -3
- package/dist/dom/index.d.mts.map +1 -1
- package/dist/dom/index.mjs +303 -6
- package/dist/dom/index.mjs.map +1 -1
- package/dist/editable-text-CKkS_mWt.d.mts +235 -0
- package/dist/editable-text-CKkS_mWt.d.mts.map +1 -0
- package/dist/events/index.d.mts +8 -1
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +32 -17
- package/dist/events/index.mjs.map +1 -1
- package/dist/follow-tab-stop-DpxwLXvU.mjs +800 -0
- package/dist/follow-tab-stop-DpxwLXvU.mjs.map +1 -0
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -7
- package/dist/interaction/index.d.mts +247 -92
- package/dist/interaction/index.d.mts.map +1 -1
- package/dist/interaction/index.mjs +507 -224
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/internal-DP-xLKbT.mjs +37 -0
- package/dist/internal-DP-xLKbT.mjs.map +1 -0
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +3 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/motion/index.mjs +1 -1
- package/dist/{motion-LfJYAMnP.mjs → motion-DGGBZ0wt.mjs} +14 -13
- package/dist/motion-DGGBZ0wt.mjs.map +1 -0
- package/dist/net/index.d.mts +69 -5
- package/dist/net/index.d.mts.map +1 -1
- package/dist/net/index.mjs +139 -56
- package/dist/net/index.mjs.map +1 -1
- package/dist/observers/index.d.mts +1 -1
- package/dist/observers/index.mjs +3 -0
- package/dist/observers/index.mjs.map +1 -1
- package/dist/state/index.d.mts +57 -15
- package/dist/state/index.d.mts.map +1 -1
- package/dist/state/index.mjs +198 -90
- package/dist/state/index.mjs.map +1 -1
- package/dist/time/index.d.mts +21 -1
- package/dist/time/index.d.mts.map +1 -1
- package/dist/time/index.mjs +48 -19
- package/dist/time/index.mjs.map +1 -1
- package/package.json +16 -5
- package/dist/motion-LfJYAMnP.mjs.map +0 -1
- package/dist/move-CZwHyTGk.mjs +0 -215
- package/dist/move-CZwHyTGk.mjs.map +0 -1
- package/dist/move-DNiMPcuf.d.mts +0 -58
- package/dist/move-DNiMPcuf.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -50,6 +50,12 @@ A bundle holds no state and performs no I/O by itself. The parent Model owns
|
|
|
50
50
|
the placed slice; the browser (or server, or clock) only reports facts as
|
|
51
51
|
Messages. The same rule as everywhere else: observation is not ownership.
|
|
52
52
|
|
|
53
|
+
A Message that changes nothing (a repeated resize, `Started` while running,
|
|
54
|
+
a total that is already held) returns the Model it was given, and a
|
|
55
|
+
placement then returns the parent unchanged. Foldkit renders only when the
|
|
56
|
+
root Model changes identity, so such a Message renders nothing, and a
|
|
57
|
+
`createLazy` view over a placed slice is skipped while the slice is the same.
|
|
58
|
+
|
|
53
59
|
Solid developers will notice missing plumbing: there is no event bus
|
|
54
60
|
because Messages are the bus, no memo because derivations are pure reads
|
|
55
61
|
over the Model, and no reactive map or store because the Model holds plain
|
|
@@ -91,11 +97,11 @@ Each subpath is one concern, one import:
|
|
|
91
97
|
- `time` — clock facts: Timer, Interval, Debounce, Throttle, relative time
|
|
92
98
|
- `state` — owned UI state: Pagination, History, Locale, SelectionSet, Virtual, range
|
|
93
99
|
- `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
|
|
100
|
+
- `interaction` — a Bundle (or Mount) and its `foldkit-mixins` Behavior: RovingTabindex, Typeahead, ListNavigation, GridNavigation, TreeNavigation, FocusScope, Press, LongPress, Move, Targets, PointerDrag, EditableText, FocusVisible, DismissLayer, ScrollLock, HideOutside, Selection, LiveAnnounce
|
|
95
101
|
- `device` — hardware: Geolocation, MediaDevices, MediaStream, Permissions, Fullscreen
|
|
96
102
|
- `events` — raw browser events: Visibility, WindowSize, Idle, InputModality, keyboard, pointer, scroll, focus
|
|
97
103
|
- `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
|
|
104
|
+
- `dom` — element Mounts and one-shot Commands: Autofocus, FocusScope, KeepInView, Measure, Move, Targets, PointerDrag, EditableText, ScrollLock, HideOutside, InputMask, clipboard, share, script loading
|
|
99
105
|
|
|
100
106
|
## Sixty seconds: follow the color scheme
|
|
101
107
|
|
|
@@ -207,8 +213,12 @@ SSE spec.
|
|
|
207
213
|
holds `{ url, status, lastError }` with `status` moving
|
|
208
214
|
closed → connecting → open. The resource owns the socket (one assembly holds
|
|
209
215
|
one); `send` is a placed helper whose command writes through the resource tag
|
|
210
|
-
and yields `Sent` on dispatch,
|
|
211
|
-
closed socket's `send` is a
|
|
216
|
+
and yields `Sent { data }` on dispatch, carrying what was sent, and
|
|
217
|
+
`SendFailed` when no connection is open — a closed socket's `send` is a
|
|
218
|
+
silent no-op per spec, so the bundle checks first. Args `{ url,
|
|
219
|
+
connectTimeoutMs? }`: with a timeout, a socket still connecting when it runs
|
|
220
|
+
out is closed and reported as `TimedOut` (status closed, `lastError` set);
|
|
221
|
+
without one, an attempt waits as long as the browser does.
|
|
212
222
|
Incoming `Received` notifies without storing: project the payload into your
|
|
213
223
|
own field to keep it. `createSocket` defaults to the platform WebSocket, read
|
|
214
224
|
lazily so tests substitute a double; the socket service rides the assembly
|
|
@@ -230,14 +240,41 @@ the Command fails, instead of throwing.
|
|
|
230
240
|
`Started`/`Stopped`/`Ticked`, args `{ intervalMs }` (positive and finite —
|
|
231
241
|
anything else is rejected at placement). The tick stream runs on
|
|
232
242
|
Effect's clock, so tests advance it with TestClock instead of waiting; while
|
|
233
|
-
stopped the stream is empty.
|
|
234
|
-
advances it.
|
|
243
|
+
stopped the stream is empty. The first tick comes one interval after
|
|
244
|
+
`Started`. Restarting keeps the count; only `Ticked` advances it.
|
|
235
245
|
|
|
236
246
|
`Interval` is the wall-clock sibling: Model `{ running, lastAt }`,
|
|
237
247
|
`Ticked { at }` stamped from Effect's clock. `Timer` counts ticks,
|
|
238
248
|
`Interval` records when — views render clocks and elapsed times from
|
|
239
249
|
`lastAt`. Same args, same TestClock story, same silence while stopped.
|
|
240
250
|
|
|
251
|
+
Both own their `running` flag, at an interval fixed where they are placed.
|
|
252
|
+
When the parent's Model already decides whether the clock runs and how fast
|
|
253
|
+
(a game's phase and its score), a second flag would be a second owner of that
|
|
254
|
+
fact. `ticks` is the entry for that case: `intervalMs` reads the interval from
|
|
255
|
+
the Model, `None` while stopped, and `onTick` makes the parent's Message with
|
|
256
|
+
the tick's time:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { Option } from 'effect'
|
|
260
|
+
import * as Subscription from 'foldkit/subscription'
|
|
261
|
+
import { ticks } from 'foldkit-primitives/time'
|
|
262
|
+
|
|
263
|
+
const subscriptions = Subscription.make<Game, GameMessage>()(() => ({
|
|
264
|
+
clock: ticks({
|
|
265
|
+
intervalMs: (game: Game) =>
|
|
266
|
+
game.playing ? Option.some(Math.max(80, 150 - game.points)) : Option.none(),
|
|
267
|
+
onTick: () => GameMessage.TickedClock(),
|
|
268
|
+
}),
|
|
269
|
+
}))
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A Model change that leaves the interval alone does nothing. A new interval
|
|
273
|
+
applies from the next tick, without restarting the clock, so a game that
|
|
274
|
+
speeds up keeps its rhythm instead of ticking at once; `None` stops it, and
|
|
275
|
+
`Some` again starts afresh, one interval before the first tick. `Timer` and
|
|
276
|
+
`Interval` are built on it.
|
|
277
|
+
|
|
241
278
|
`debounce({ name, value })` is a factory over any value Schema (like
|
|
242
279
|
`history`): `Changed` restarts a `{ delayMs }` timer, and only the latest
|
|
243
280
|
value settles — as an OutMessage the placement must handle with `onOut`, so
|
|
@@ -285,9 +322,11 @@ focus `{ tag, id }` — elements cross as tag and id, never as live nodes.
|
|
|
285
322
|
`matchHotkey("ctrl+shift+k", press)` answers whether a press is a shortcut,
|
|
286
323
|
so `update` stays a table of chords; matching is exact and auto-repeat never
|
|
287
324
|
matches. When the observed target itself depends on state, scope the entry
|
|
288
|
-
through subscription dependencies and it restreams on change.
|
|
289
|
-
|
|
290
|
-
|
|
325
|
+
through subscription dependencies and it restreams on change.
|
|
326
|
+
`keyboardEvents({ preventDefault: press => ... })` cancels the default of the
|
|
327
|
+
presses the predicate picks (the arrows scrolling a game's page,
|
|
328
|
+
`matchHotkey('ctrl+s', press)`), decided in the listener, because a default
|
|
329
|
+
can only be cancelled while the event is dispatching. Lift
|
|
291
330
|
with `Subscription.persistent`, mapping into the parent's Message; without
|
|
292
331
|
a window each stream is empty instead of throwing.
|
|
293
332
|
|
|
@@ -357,7 +396,10 @@ owns fullscreen state.
|
|
|
357
396
|
`copyText` copies text as a Command: use it in `update` beside any bundle.
|
|
358
397
|
It yields `Copied` on success and `CopyFailed` otherwise — denial, insecure
|
|
359
398
|
context, or no clipboard API (SSR) all become the failure Message instead of
|
|
360
|
-
throwing.
|
|
399
|
+
throwing. `readText()` reads it the same way, yielding a
|
|
400
|
+
`ClipboardReadMessage`: `Read { text }`, or `ReadFailed` when the browser
|
|
401
|
+
refuses. No Model involved: the clipboard is not
|
|
402
|
+
application state.
|
|
361
403
|
|
|
362
404
|
`share(data)` posts `{ title?, text?, url? }` to the platform sheet: `Shared`
|
|
363
405
|
on success, `Dismissed` on sheet cancel (its own outcome, not a failure),
|
|
@@ -373,6 +415,26 @@ rewrites the field with approximate caret restore, and emits
|
|
|
373
415
|
`Input { value, raw }` with masked and unmasked text. The parent owns the
|
|
374
416
|
state, like any controlled input.
|
|
375
417
|
|
|
418
|
+
`KeepInView({ selector })` keeps what is marked in view: whenever an element in
|
|
419
|
+
its subtree newly matches `selector` (the row just selected, the node just
|
|
420
|
+
inserted), it is scrolled into view the least amount that shows it. It sends no
|
|
421
|
+
Message and redraws nothing, so a focused row stays focused. Where there is no
|
|
422
|
+
layout (jsdom), it scrolls nothing.
|
|
423
|
+
|
|
424
|
+
`Measure({ targets: { selected: '[aria-selected="true"]' } })` measures, for
|
|
425
|
+
each named target, the first element in its subtree that matches, relative to
|
|
426
|
+
its scroll box, and writes `--fk-selected-x`, `-y`, `-w`, `-h` (pixels) and
|
|
427
|
+
`--fk-selected-display` (`block`, or `none` while nothing matches) on the
|
|
428
|
+
element (`measured('selected')` names them). A child placed absolutely from
|
|
429
|
+
them, such as an editor's selection outline, sits over the target and scrolls
|
|
430
|
+
with it. Where an element is, is presentation: it sends no Message and keeps
|
|
431
|
+
nothing in the Model. It measures again when the subtree changes, as the
|
|
432
|
+
records arrive, so the box moves in the frame the change is drawn in; and, at
|
|
433
|
+
most once a frame, when the element scrolls, when it or a target changes size,
|
|
434
|
+
when the window resizes, when an image or a font inside loads, and on each
|
|
435
|
+
frame of a transition or an animation inside. Anything else that moves a
|
|
436
|
+
target without resizing it (a stylesheet added) is seen at the next of these.
|
|
437
|
+
|
|
376
438
|
## State: `foldkit-primitives/state`
|
|
377
439
|
|
|
378
440
|
> Reference: [`./state/README.md`](./state/README.md)
|
|
@@ -387,9 +449,18 @@ item's index for a slice or a query. Loading data stays the application's job:
|
|
|
387
449
|
this bundle owns the page, not the items.
|
|
388
450
|
|
|
389
451
|
`history({ name, value, capacity })` makes an undo/redo bundle over any value
|
|
390
|
-
Schema. The Model holds `{ past, present, future }`; `Push` records and
|
|
391
|
-
the redo future
|
|
392
|
-
|
|
452
|
+
Schema. The Model holds `{ past, present, future, group }`; `Push` records and
|
|
453
|
+
drops the redo future (an empty future is kept as it was, so a view reading it
|
|
454
|
+
is not drawn again per push), `Undo`/`Redo` move one step, `GoTo { step }`
|
|
455
|
+
jumps to any kept value, counted from the oldest (0) so the present is at
|
|
456
|
+
`past.length`, and `Clear` empties both sides while keeping the present. A `Push` may name a `group`: consecutive pushes of
|
|
457
|
+
the same group are one step, so typing a word undoes as a whole, with no clock.
|
|
458
|
+
The steps are also pure functions, `History.start`, `push`, `undo`, `redo`,
|
|
459
|
+
`goTo` and `clear`, for a parent that records an edit in the same transition that makes it
|
|
460
|
+
(the page Builder keeps its page this way). Two more are only functions:
|
|
461
|
+
`History.close(model)` ends the group, so the same group pushed again is a step
|
|
462
|
+
of its own, and `History.revert(model, group)` takes back the step `group` is
|
|
463
|
+
making with nothing left to redo, which is what a cancelled edit leaves. The past holds at most `capacity` entries (default 100);
|
|
393
464
|
a negative or fractional capacity throws at the factory, naming it. The
|
|
394
465
|
factory attaches the Message union, so placements dispatch
|
|
395
466
|
`EditHistory.Message.Push(...)`. `canUndo`/`canRedo` read the edges:
|
|
@@ -425,6 +496,11 @@ the spacer height, `isAtEnd(model, keys, viewportHeight, threshold)` is the
|
|
|
425
496
|
infinite-scroll check (an empty list counts as ended), `distanceToEnd`
|
|
426
497
|
answers the pixels remaining for prefetch thresholds, and `offsetFor`
|
|
427
498
|
computes programmatic scroll targets the application actuates itself.
|
|
499
|
+
These share one table of row offsets, built once per `keys` array and
|
|
500
|
+
`heights` record and then searched, so keep the keys array between renders
|
|
501
|
+
(derive it where the list changes, not in the view) and a scroll costs
|
|
502
|
+
O(log n) instead of a pass over every row. Both are compared by identity:
|
|
503
|
+
never change a keys array in place.
|
|
428
504
|
`Prune` drops heights for departed keys — the bundle never sees key order.
|
|
429
505
|
Poisoned positions and heights are ignored, never stored. For window-
|
|
430
506
|
scrolled lists, map the scroll entry into `Scrolled`; for follow-bottom,
|
|
@@ -616,6 +692,39 @@ const Keys = GridNavigation.behavior(Cells, args)(CalendarSlots)<Model, Message>
|
|
|
616
692
|
})
|
|
617
693
|
```
|
|
618
694
|
|
|
695
|
+
`TreeNavigation` is the tree counterpart, after the WAI-ARIA tree pattern,
|
|
696
|
+
for a file explorer, a page's layers, or a nested menu: Up and Down through
|
|
697
|
+
the rows that are showing, Right opens a row or steps into it, Left closes it or
|
|
698
|
+
steps out to its parent, Home and End go to the first and last row. The view
|
|
699
|
+
gives every row, open or not, in tree order, each with its `parent` and whether
|
|
700
|
+
it is a `branch`; `TreeNavigation.shown(rows, model, args)` is the rows that are
|
|
701
|
+
showing, with their level and place among their siblings. The Model slice is
|
|
702
|
+
`{ current, toggled }`: openness is stored as the rows toggled away from
|
|
703
|
+
`openByDefault`, so a layers panel that starts open and a file tree that starts
|
|
704
|
+
closed are one bundle with a different arg. The Behavior writes each showing
|
|
705
|
+
row's `id` (through `domId`, by default the row's own), `role="treeitem"`,
|
|
706
|
+
`aria-level`, `aria-posinset`, `aria-setsize`, `aria-expanded` on a branch,
|
|
707
|
+
`aria-disabled`, a roving `tabindex` and `OnFocus`; the container's keys move
|
|
708
|
+
focus by id, or open and close the current row in place. Focus in the tree
|
|
709
|
+
follows its stop when a transition it did not see moves it or removes the
|
|
710
|
+
focused row (the `FollowTabStop` Mount, in `foldkit-primitives/dom`, which
|
|
711
|
+
never takes focus from outside the container). The level is also the
|
|
712
|
+
custom property `--fk-tree-level`, 1 at the top, so one rule indents any depth:
|
|
713
|
+
`padding-inline-start: calc(var(--fk-tree-level) * 1rem)`.
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
const Layers = Bundle.declare(TreeNavigation.bundle, 'layers')
|
|
717
|
+
// place with { args: { openByDefault: true } }
|
|
718
|
+
const Keys = TreeNavigation.behavior(Layers, { openByDefault: true })(TreeSlots)<Model, Message>({
|
|
719
|
+
container: 'tree',
|
|
720
|
+
item: 'row',
|
|
721
|
+
rows: model => model.folders.map(folder => ({ id: folder.id, parent: folder.parent, branch: folder.hasChildren })),
|
|
722
|
+
})
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
The pure `move(shown, current, key, modifiers, model, args, direction?)` answers
|
|
726
|
+
with `Focus`, `Open` or `Close`, for a view that wires its own.
|
|
727
|
+
|
|
619
728
|
`FocusScope` is the one entry here that is a Mount, not a Bundle: which
|
|
620
729
|
element has focus is a DOM fact, so nothing crosses to the Model. The Mount
|
|
621
730
|
lives in `foldkit-primitives/dom`; `FocusScope.behavior(Slots)<Input,
|
|
@@ -680,6 +789,86 @@ and a secondary button are ignored; capture is released with the Mount.
|
|
|
680
789
|
`Draggable` slot and maps each fact into the view's Messages; a drag's meaning
|
|
681
790
|
(a threshold, a snap, a reorder) is the parent's `update`.
|
|
682
791
|
|
|
792
|
+
`Targets` is which marked descendant of a container the pointer is over, and
|
|
793
|
+
which one was pressed, as one Mount on the container rather than one per item.
|
|
794
|
+
A descendant is marked by an attribute holding its id (a canvas's
|
|
795
|
+
`data-composition-node`, a table's `data-row`), and the nearest marked ancestor
|
|
796
|
+
of the event's target, inside the container, is the one reported:
|
|
797
|
+
`TargetHovered { id }` once per change, `null` when there is none or the
|
|
798
|
+
pointer leaves, and `TargetPressed { id, shiftKey, altKey, ctrlKey, metaKey }`
|
|
799
|
+
on a click. `preventDefault: true` stops a press's default, such as a link
|
|
800
|
+
navigating inside an editor's canvas. The Mount is `Targets({ attribute,
|
|
801
|
+
preventDefault })` in `foldkit-primitives/dom`; `Targets.behavior(Slots)<Input,
|
|
802
|
+
Message>({ container, attribute, preventDefault?, toMessage })` attaches it.
|
|
803
|
+
`targetOf(container, from, attribute)` is the pure lookup.
|
|
804
|
+
|
|
805
|
+
`EditableText` is text typed into a marked descendant of a container while it
|
|
806
|
+
is `contenteditable`, as one Mount on the container. The view decides which
|
|
807
|
+
field is editable; the Mount focuses it as it becomes so (made editable, or
|
|
808
|
+
drawn editable), with the caret at its end, and at no other time: a field
|
|
809
|
+
editable all along, or another going away, moves no focus. It reads what is
|
|
810
|
+
typed:
|
|
811
|
+
|
|
812
|
+
- **Text, never markup.** It reads `innerText`. A field is one line unless it
|
|
813
|
+
carries `aria-multiline="true"`, and in one line a line break becomes a
|
|
814
|
+
space. Where the browser lacks `contenteditable="plaintext-only"`, a paste is
|
|
815
|
+
inserted as its text.
|
|
816
|
+
- **`TextEdited { field, text }`** on each change, `field` being the marking
|
|
817
|
+
attribute's value. Nothing is reported while an input method composes; the
|
|
818
|
+
composed text arrives once.
|
|
819
|
+
- **`TextCommitted { field, text }`** on Enter (Shift+Enter breaks a line in a
|
|
820
|
+
multiline field) or on leaving the field; **`TextCancelled { field, initial }`**
|
|
821
|
+
on Escape, which also puts the text the field had when the edit began (on
|
|
822
|
+
focus, or on the first keystroke after an edit ended) back in the DOM, the
|
|
823
|
+
caret at its end. A view that stopped redrawing the field while it was edited
|
|
824
|
+
would not. An edit ends once: the blur after Enter commits nothing more.
|
|
825
|
+
- **Focus comes back to the container** when Enter or Escape ended an edit and
|
|
826
|
+
the view then removes the field or makes it no longer editable, which leaves
|
|
827
|
+
focus on nothing; give the container a `tabindex`, and the next key (an
|
|
828
|
+
undo) reaches it. A field left by the author keeps no such claim.
|
|
829
|
+
- **`EditAsked { field }`** on a double-click over a marked field that is not
|
|
830
|
+
editable yet: the view's cue to make it so.
|
|
831
|
+
|
|
832
|
+
The Mount is `EditableText({ attribute })` in `foldkit-primitives/dom`;
|
|
833
|
+
`EditableText.behavior(Slots)<Input, Message>({ container, attribute, toMessage })`
|
|
834
|
+
attaches it. What a change means (a prop set, an undo group) is the parent's
|
|
835
|
+
`update`.
|
|
836
|
+
|
|
837
|
+
`PointerDrag` is dragging one marked descendant onto another, marked the way
|
|
838
|
+
`Targets` marks them. A primary press on one that moves more than
|
|
839
|
+
`DRAG_THRESHOLD` (4px) starts a drag (`DragStarted { id }`); then, once per
|
|
840
|
+
change, `DraggedOver { over }` says which other marked descendant the pointer
|
|
841
|
+
is over and in which third of its box, `{ id, zone: 'before' | 'inside' |
|
|
842
|
+
'after' }`, or `null`. Releasing is `DragDropped { id, over }`, and Escape, a
|
|
843
|
+
cancelled pointer, or a button found released mid-drag is `DragCancelled { id }`.
|
|
844
|
+
An Escape that cancels is heard on the way down and goes no further, so the
|
|
845
|
+
focused element's own Escape (a canvas's deselect) does not also run.
|
|
846
|
+
It follows the pointer that pressed and ignores a second one; the element under
|
|
847
|
+
it is found by position, so a touch or a pen, which the browser captures to
|
|
848
|
+
where it went down, drags too, once the marked elements have `touch-action:
|
|
849
|
+
none` so a finger drags rather than scrolls. The click a drop ends with is
|
|
850
|
+
swallowed, so a `Targets` on the same container does not also press. A press
|
|
851
|
+
in `contenteditable` text selects text and starts no drag. It writes
|
|
852
|
+
no roles, `tabindex` or keys, so it sits beside a tree's or a listbox's own;
|
|
853
|
+
the keyboard's way to do what a drag does is yours to give. Boxes are measured
|
|
854
|
+
as the pointer moves and never kept; an element drawn as `display: contents`
|
|
855
|
+
is measured by its first child (`boxOf`). The Mount is
|
|
856
|
+
`PointerDrag({ attribute })` in `foldkit-primitives/dom`;
|
|
857
|
+
`PointerDrag.behavior(Slots)<Input, Message>({ container, attribute, toMessage })`
|
|
858
|
+
attaches it, and `zoneOf(box, y)` is the pure split.
|
|
859
|
+
|
|
860
|
+
A drag may also land somewhere other than among its own: with
|
|
861
|
+
`targets: { attribute: 'data-composition-node', within: '#page' }`, what is
|
|
862
|
+
dragged is still one of the container's marked descendants (a palette's
|
|
863
|
+
tiles), and `over` is an element marked by that attribute inside the one
|
|
864
|
+
`within` selects (the page's nodes). Its own tiles, and a matching element
|
|
865
|
+
outside that one, are then over nothing. `within` is looked for nearest
|
|
866
|
+
first, under the container's closest ancestor that holds a match, so two
|
|
867
|
+
editors on one page each drop onto their own page. Such a drag's
|
|
868
|
+
`DraggedOver` and `DragDropped` also carry `region`: whether the pointer is
|
|
869
|
+
inside that element, so `over: null` with `region: true` is its empty space
|
|
870
|
+
(an empty page, the space below its last node), and with `false`, elsewhere.
|
|
871
|
+
|
|
683
872
|
`FocusVisible` is the one entry whose Bundle lives elsewhere: `InputModality`
|
|
684
873
|
in `foldkit-primitives/events` keeps `{ modality }` (`'keyboard'`, `'pointer'`,
|
|
685
874
|
or `'unknown'` before any input), fed by the window's `keydown` (a modifier
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/device/fullscreen.ts","../../src/device/geolocation.ts","../../src/device/media-devices.ts","../../src/device/media-stream.ts","../../src/device/permissions.ts"],"mappings":";;;;;qBAWa,6CAAiB;;;;;;;;;;YAMlB,2BAA2B,kBAAkB;;;;;;;qBAW5C,kBAAmB,SAAS,YAAU,QAAQ;;qBA0B9C,sBAAqB,QAAQ;;;;;qBAgC7B,yBAAwB,OAAO,OAAO;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/device/fullscreen.ts","../../src/device/geolocation.ts","../../src/device/media-devices.ts","../../src/device/media-stream.ts","../../src/device/permissions.ts"],"mappings":";;;;;qBAWa,6CAAiB;;;;;;;;;;YAMlB,2BAA2B,kBAAkB;;;;;;;qBAW5C,kBAAmB,SAAS,YAAU,QAAQ;;qBA0B9C,sBAAqB,QAAQ;;;;;qBAgC7B,yBAAwB,OAAO,OAAO;;;qBCzEtC,aAAW,OAAA;;;;;YAKZ,qBAAqB,YAAY;qBAEhC,kBAAgB,OAAA;;;;;;;;;YAKjB,0BAA0B,iBAAiB;qBAE1C,8CAAkB;;;;;;;;;;;YASnB,4BAA4B,mBAAmB;qBAmD9C,aAAW,OAAA;;;;;;;;;;;;;;;;;;;;;qBCxEX,aAAW,OAAA;;;;;;YAMZ,qBAAqB,YAAY;qBAEhC,mBAAiB,OAAA;;;;;;;;;;YAKlB,2BAA2B,kBAAkB;qBAE5C,+CAAmB;;;;;;;;;;;;;;;;YAOpB,6BAA6B,oBAAoB;;iBAa5C;EACf,oBAAoB,QAClB;aACW;aACA;aACA;aACA;;EAGb,iBAAiB,cAAc;EAC/B,oBAAoB,cAAc;;qBAUvB,qBAAsB,qBAAqB;WAC7C,MAAM;;WAEN,WAAW,WAAW,cAAc;MAC9C,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBC1DY,kBAAgB,OAAA;;;;YAIjB,0BAA0B,iBAAiB;qBAE1C,8CAAkB;;;;;;;sBAAlB,OAAA;;;YAQD,4BAA4B,mBAAmB;;iBAG1C;EACf;;;iBAIe;EACf,aAAa,cAAc;;iBAGZ;WACN;WACA;;;qBAIE,YAAU,gBAAA,gBAAA,mBAAA,gBAAA;;YAGX,oBAAoB,gBAAgB,iBAAiB;qBAoBpD,oBAAqB,qBAAqB;WAC5C,MAAM;;WAEN,YACL,aAAa,2BAA2B,QAAQ;MACrD,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBC5DY,iBAAe,OAAA;YAChB,yBAAyB,gBAAgB;qBAExC,kBAAgB,OAAA;;;;YAIjB,0BAA0B,iBAAiB;qBAE1C,8CAAkB;;;;;;;;;;;;;YAMnB,4BAA4B,mBAAmB;;iBAG1C;WACN;EACT;;;iBAIe;EACf,MAAM;aAAuB;MAAiB,QAAQ;;UAG9C;WACC,UAAU,oBAAoB;WAC9B,UAAU,SAAS,eAAe;WAClC,QAAQ,MAAM,MAAM;;;qBAIlB,OAAK,gBAAA,gBAAA,UAAA,gBAAA;;YAGN,eAAe,gBAAgB,iBAAiB;qBAuB/C,oBAAqB,qBAAqB;WAC5C,MAAM;;WAEN,WAAW,WAAW,cAAc;MAC9C,OAAA,OAAA;WAYqB,OAAA;;;;;;;;;;;;;;;;;;;;;EApCb,OAAA,gBAAgB,MACrB,kBACA,oBACA,OAAO,OAAO,wBACd,UACA,eACC,UAAU,aAAa"}
|
package/dist/device/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { i as unlessSame } from "../internal-DP-xLKbT.mjs";
|
|
1
2
|
import { Effect, Option, Queue, Schema, Stream } from "effect";
|
|
2
3
|
import { defineMessageUnion } from "foldkit/message";
|
|
3
4
|
import * as Subscription from "foldkit/subscription";
|
|
@@ -127,7 +128,7 @@ const Geolocation = Bundle.make("Geolocation", {
|
|
|
127
128
|
lastError: null
|
|
128
129
|
} }),
|
|
129
130
|
update: (model, message) => GeolocationMessage.match(message, {
|
|
130
|
-
Located: ({ latitude, longitude, accuracy }) =>
|
|
131
|
+
Located: ({ latitude, longitude, accuracy }) => model.status === "ready" && model.coords !== null && model.coords.latitude === latitude && model.coords.longitude === longitude && model.coords.accuracy === accuracy ? { model } : { model: {
|
|
131
132
|
...model,
|
|
132
133
|
status: "ready",
|
|
133
134
|
coords: {
|
|
@@ -135,15 +136,15 @@ const Geolocation = Bundle.make("Geolocation", {
|
|
|
135
136
|
longitude,
|
|
136
137
|
accuracy
|
|
137
138
|
}
|
|
138
|
-
} }
|
|
139
|
-
Denied: () => ({ model: {
|
|
139
|
+
} },
|
|
140
|
+
Denied: () => ({ model: unlessSame(model, {
|
|
140
141
|
...model,
|
|
141
142
|
status: "denied"
|
|
142
|
-
} }),
|
|
143
|
-
Failed: ({ message }) => ({ model: {
|
|
143
|
+
}) }),
|
|
144
|
+
Failed: ({ message }) => ({ model: unlessSame(model, {
|
|
144
145
|
...model,
|
|
145
146
|
lastError: message
|
|
146
|
-
} })
|
|
147
|
+
}) })
|
|
147
148
|
}),
|
|
148
149
|
subscriptions: () => Subscription.make()(() => ({ watch: Subscription.persistent(watchStream()) }))
|
|
149
150
|
});
|
|
@@ -183,6 +184,7 @@ const MediaDevicesMessage = defineMessageUnion({
|
|
|
183
184
|
Denied: {},
|
|
184
185
|
Failed: { message: Schema.String }
|
|
185
186
|
});
|
|
187
|
+
const sameDevices = (a, b) => a.length === b.length && a.every((device, index) => device.deviceId === b[index].deviceId && device.groupId === b[index].groupId && device.kind === b[index].kind && device.label === b[index].label);
|
|
186
188
|
const failMessage$2 = (error) => error instanceof Error ? error.message : String(error);
|
|
187
189
|
const isDenied$1 = (error) => typeof error?.name === "string" && ["NotAllowedError", "SecurityError"].includes(error.name);
|
|
188
190
|
const mediaDevices = (config) => {
|
|
@@ -239,19 +241,19 @@ const mediaDevices = (config) => {
|
|
|
239
241
|
model,
|
|
240
242
|
commands: [scan()]
|
|
241
243
|
}),
|
|
242
|
-
Refreshed: ({ devices }) => ({ model: {
|
|
244
|
+
Refreshed: ({ devices }) => model.status === "ready" && sameDevices(model.devices, devices) ? { model } : { model: {
|
|
243
245
|
...model,
|
|
244
246
|
status: "ready",
|
|
245
|
-
devices
|
|
246
|
-
} }
|
|
247
|
-
Denied: () => ({ model: {
|
|
247
|
+
devices
|
|
248
|
+
} },
|
|
249
|
+
Denied: () => ({ model: unlessSame(model, {
|
|
248
250
|
...model,
|
|
249
251
|
status: "denied"
|
|
250
|
-
} }),
|
|
251
|
-
Failed: ({ message }) => ({ model: {
|
|
252
|
+
}) }),
|
|
253
|
+
Failed: ({ message }) => ({ model: unlessSame(model, {
|
|
252
254
|
...model,
|
|
253
255
|
lastError: message
|
|
254
|
-
} })
|
|
256
|
+
}) })
|
|
255
257
|
}),
|
|
256
258
|
subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(changedStream()) }))
|
|
257
259
|
});
|
|
@@ -306,32 +308,32 @@ const mediaStream = (config) => {
|
|
|
306
308
|
lastError: null
|
|
307
309
|
} }),
|
|
308
310
|
update: (model, message) => MediaStreamMessage.match(message, {
|
|
309
|
-
Started: () =>
|
|
311
|
+
Started: () => model.status === "requesting" || model.status === "live" ? { model } : { model: {
|
|
310
312
|
...model,
|
|
311
313
|
status: "requesting"
|
|
312
|
-
} }
|
|
313
|
-
Stopped: () => ({ model: {
|
|
314
|
+
} },
|
|
315
|
+
Stopped: () => ({ model: unlessSame(model, {
|
|
314
316
|
...model,
|
|
315
317
|
status: "idle"
|
|
316
|
-
} }),
|
|
317
|
-
Live: () => ({ model: {
|
|
318
|
+
}) }),
|
|
319
|
+
Live: () => ({ model: unlessSame(model, {
|
|
318
320
|
...model,
|
|
319
321
|
status: "live",
|
|
320
322
|
lastError: null
|
|
321
|
-
} }),
|
|
322
|
-
Ended: () => ({ model: {
|
|
323
|
+
}) }),
|
|
324
|
+
Ended: () => ({ model: unlessSame(model, {
|
|
323
325
|
...model,
|
|
324
326
|
status: "idle"
|
|
325
|
-
} }),
|
|
326
|
-
Denied: () => ({ model: {
|
|
327
|
+
}) }),
|
|
328
|
+
Denied: () => ({ model: unlessSame(model, {
|
|
327
329
|
...model,
|
|
328
330
|
status: "denied"
|
|
329
|
-
} }),
|
|
330
|
-
Failed: ({ message }) => ({ model: {
|
|
331
|
+
}) }),
|
|
332
|
+
Failed: ({ message }) => ({ model: unlessSame(model, {
|
|
331
333
|
...model,
|
|
332
334
|
status: "idle",
|
|
333
335
|
lastError: message
|
|
334
|
-
} })
|
|
336
|
+
}) })
|
|
335
337
|
}),
|
|
336
338
|
resources: (args) => ManagedResource.make()((entry) => ({ stream: entry(Schema.Option(Schema.Struct({
|
|
337
339
|
audio: Schema.Boolean,
|
|
@@ -405,79 +407,85 @@ const permissions = (config) => {
|
|
|
405
407
|
lastError: null
|
|
406
408
|
} }),
|
|
407
409
|
update: (model, message) => PermissionsMessage.match(message, {
|
|
408
|
-
Snapshot: ({ states }) =>
|
|
409
|
-
|
|
410
|
-
states: {
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
Snapshot: ({ states }) => {
|
|
411
|
+
const next = unlessSame(model.states, { ...states });
|
|
412
|
+
return next === model.states ? { model } : { model: {
|
|
413
|
+
...model,
|
|
414
|
+
states: next
|
|
415
|
+
} };
|
|
416
|
+
},
|
|
417
|
+
Changed: ({ name, state }) => Object.hasOwn(model.states, name) && model.states[name] === state ? { model } : { model: {
|
|
413
418
|
...model,
|
|
414
419
|
states: {
|
|
415
420
|
...model.states,
|
|
416
421
|
[name]: state
|
|
417
422
|
}
|
|
418
|
-
} }
|
|
419
|
-
Cleared: () => ({ model: {
|
|
423
|
+
} },
|
|
424
|
+
Cleared: () => Object.keys(model.states).length === 0 ? { model } : { model: {
|
|
420
425
|
...model,
|
|
421
426
|
states: {}
|
|
422
|
-
} }
|
|
423
|
-
Failed: ({ message }) => ({ model: {
|
|
427
|
+
} },
|
|
428
|
+
Failed: ({ message }) => ({ model: unlessSame(model, {
|
|
424
429
|
...model,
|
|
425
430
|
lastError: message
|
|
426
|
-
} })
|
|
431
|
+
}) })
|
|
427
432
|
}),
|
|
428
|
-
resources: (args) =>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
for (const name of names) {
|
|
443
|
-
const status = yield* Effect.tryPromise({
|
|
444
|
-
try: () => api.query({ name }),
|
|
445
|
-
catch: (error) => error
|
|
446
|
-
});
|
|
447
|
-
queried.push([
|
|
448
|
-
name,
|
|
449
|
-
status,
|
|
450
|
-
readState(status.state, name)
|
|
451
|
-
]);
|
|
452
|
-
}
|
|
453
|
-
const snapshot = {};
|
|
454
|
-
for (const [name, status, state] of queried) {
|
|
455
|
-
status.onchange = () => {
|
|
456
|
-
try {
|
|
457
|
-
offer(PermissionsMessage.Changed({
|
|
458
|
-
name,
|
|
459
|
-
state: readState(status.state, name)
|
|
460
|
-
}));
|
|
461
|
-
} catch {
|
|
462
|
-
offer(PermissionsMessage.Failed({ message: `unknown permission state for ${name}` }));
|
|
463
|
-
}
|
|
433
|
+
resources: (args) => {
|
|
434
|
+
const names = Option.some([...new Set(args.names)]);
|
|
435
|
+
return ManagedResource.make()((entry) => ({ watch: entry(Schema.Option(Schema.Array(Schema.String)), {
|
|
436
|
+
resource: Watch,
|
|
437
|
+
modelToMaybeRequirements: () => names,
|
|
438
|
+
acquire: (names) => Effect.gen(function* () {
|
|
439
|
+
const api = handle();
|
|
440
|
+
if (api === null) throw new Error("permissions are unavailable");
|
|
441
|
+
const events = yield* Queue.unbounded();
|
|
442
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
443
|
+
let tail = Promise.resolve();
|
|
444
|
+
const offer = (message) => {
|
|
445
|
+
const run = () => Effect.runPromise(Queue.offer(events, message)).then(() => void 0, () => void 0);
|
|
446
|
+
tail = tail.then(run, run);
|
|
464
447
|
};
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
448
|
+
const queried = [];
|
|
449
|
+
for (const name of names) {
|
|
450
|
+
const status = yield* Effect.tryPromise({
|
|
451
|
+
try: () => api.query({ name }),
|
|
452
|
+
catch: (error) => error
|
|
453
|
+
});
|
|
454
|
+
queried.push([
|
|
455
|
+
name,
|
|
456
|
+
status,
|
|
457
|
+
readState(status.state, name)
|
|
458
|
+
]);
|
|
459
|
+
}
|
|
460
|
+
const snapshot = {};
|
|
461
|
+
for (const [name, status, state] of queried) {
|
|
462
|
+
status.onchange = () => {
|
|
463
|
+
try {
|
|
464
|
+
offer(PermissionsMessage.Changed({
|
|
465
|
+
name,
|
|
466
|
+
state: readState(status.state, name)
|
|
467
|
+
}));
|
|
468
|
+
} catch {
|
|
469
|
+
offer(PermissionsMessage.Failed({ message: `unknown permission state for ${name}` }));
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
statuses.set(name, status);
|
|
473
|
+
snapshot[name] = state;
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
statuses,
|
|
477
|
+
snapshot,
|
|
478
|
+
events
|
|
479
|
+
};
|
|
480
|
+
}),
|
|
481
|
+
onAcquired: ({ snapshot }) => PermissionsMessage.Snapshot({ states: { ...snapshot } }),
|
|
482
|
+
onReleased: () => PermissionsMessage.Cleared(),
|
|
483
|
+
onAcquireError: (error) => PermissionsMessage.Failed({ message: failMessage(error) }),
|
|
484
|
+
release: ({ statuses, events }) => Effect.asVoid(Effect.andThen(Effect.sync(() => {
|
|
485
|
+
for (const status of statuses.values()) status.onchange = null;
|
|
486
|
+
}), Queue.shutdown(events)))
|
|
487
|
+
}) }));
|
|
488
|
+
},
|
|
481
489
|
subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(Stream.unwrap(Effect.matchEffect(Watch.get, {
|
|
482
490
|
onFailure: () => Effect.succeed(Stream.empty),
|
|
483
491
|
onSuccess: ({ events }) => Effect.succeed(Stream.fromQueue(events))
|