foldkit-primitives 0.2.1 → 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.
Files changed (57) hide show
  1. package/README.md +514 -43
  2. package/dist/device/index.d.mts +11 -11
  3. package/dist/device/index.d.mts.map +1 -1
  4. package/dist/device/index.mjs +95 -87
  5. package/dist/device/index.mjs.map +1 -1
  6. package/dist/dom/index.d.mts +71 -1
  7. package/dist/dom/index.d.mts.map +1 -1
  8. package/dist/dom/index.mjs +303 -5
  9. package/dist/dom/index.mjs.map +1 -1
  10. package/dist/editable-text-CKkS_mWt.d.mts +235 -0
  11. package/dist/editable-text-CKkS_mWt.d.mts.map +1 -0
  12. package/dist/events/index.d.mts +10 -1
  13. package/dist/events/index.d.mts.map +1 -1
  14. package/dist/events/index.mjs +34 -18
  15. package/dist/events/index.mjs.map +1 -1
  16. package/dist/follow-tab-stop-DpxwLXvU.mjs +800 -0
  17. package/dist/follow-tab-stop-DpxwLXvU.mjs.map +1 -0
  18. package/dist/index-Cppf-lBx.d.mts +130 -0
  19. package/dist/index-Cppf-lBx.d.mts.map +1 -0
  20. package/dist/index.d.mts +8 -6
  21. package/dist/index.mjs +8 -6
  22. package/dist/input-modality-DGOhAknO.d.mts +26 -0
  23. package/dist/input-modality-DGOhAknO.d.mts.map +1 -0
  24. package/dist/input-modality-m6v7__Xs.mjs +43 -0
  25. package/dist/input-modality-m6v7__Xs.mjs.map +1 -0
  26. package/dist/interaction/index.d.mts +1017 -0
  27. package/dist/interaction/index.d.mts.map +1 -0
  28. package/dist/interaction/index.mjs +1647 -0
  29. package/dist/interaction/index.mjs.map +1 -0
  30. package/dist/internal-DP-xLKbT.mjs +37 -0
  31. package/dist/internal-DP-xLKbT.mjs.map +1 -0
  32. package/dist/media/index.d.mts.map +1 -1
  33. package/dist/media/index.mjs +3 -3
  34. package/dist/media/index.mjs.map +1 -1
  35. package/dist/motion/index.d.mts +2 -112
  36. package/dist/motion/index.mjs +2 -208
  37. package/dist/motion-DGGBZ0wt.mjs +240 -0
  38. package/dist/motion-DGGBZ0wt.mjs.map +1 -0
  39. package/dist/net/index.d.mts +68 -4
  40. package/dist/net/index.d.mts.map +1 -1
  41. package/dist/net/index.mjs +139 -56
  42. package/dist/net/index.mjs.map +1 -1
  43. package/dist/observers/index.d.mts +1 -1
  44. package/dist/observers/index.mjs +3 -0
  45. package/dist/observers/index.mjs.map +1 -1
  46. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  47. package/dist/state/index.d.mts +57 -15
  48. package/dist/state/index.d.mts.map +1 -1
  49. package/dist/state/index.mjs +198 -90
  50. package/dist/state/index.mjs.map +1 -1
  51. package/dist/time/index.d.mts +21 -1
  52. package/dist/time/index.d.mts.map +1 -1
  53. package/dist/time/index.mjs +48 -19
  54. package/dist/time/index.mjs.map +1 -1
  55. package/package.json +31 -8
  56. package/dist/motion/index.d.mts.map +0 -1
  57. package/dist/motion/index.mjs.map +0 -1
@@ -0,0 +1,1647 @@
1
+ import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
+ import { C as MoveEnded, D as ScrollLock, E as HideOutside, O as FocusScope, S as Move, T as Moved, _ as zoneOf, a as TextCommitted, b as Targets, d as DragPlace, f as DragStarted, g as boxOf, h as PointerDrag, i as TextCancelled, k as tabbableWithin, l as DragCancelled, m as DraggedOver, n as EditAsked, o as TextEdited, p as DragZone, r as EditableText, s as textOf, t as FollowTabStop, u as DragDropped, v as TargetHovered, w as MoveStarted, x as targetOf, y as TargetPressed } from "../follow-tab-stop-DpxwLXvU.mjs";
3
+ import { n as perInput, r as sameIds, t as clearQuery } from "../internal-DP-xLKbT.mjs";
4
+ import { a as modalityOfKey, i as Modality, t as InputModality } from "../input-modality-m6v7__Xs.mjs";
5
+ import { Effect, Option, Queue, Schema, Stream } from "effect";
6
+ import * as Mount from "foldkit/mount";
7
+ import { defineMessageUnion } from "foldkit/message";
8
+ import * as Subscription from "foldkit/subscription";
9
+ import { Bundle } from "foldkit-bundle";
10
+ import { Behavior, Capability } from "foldkit-mixins";
11
+ //#region src/interaction/dismiss-layer.ts
12
+ /**
13
+ * Dismissable layers: Escape closes the topmost open layer, a pointer press
14
+ * outside closes the layers it is outside of. One Bundle, placed once, owns
15
+ * the document listeners; each layer's Behavior marks its container (and its
16
+ * trigger) with data attributes. The stack is the DOM order of the marked
17
+ * elements at the moment of the event, so a layer opening or closing needs no
18
+ * registration. A press inside a parent layer is outside its children, so the
19
+ * parent stays and the children go; a press on a layer's trigger counts as
20
+ * inside it, so a trigger never dismisses and reopens in one click. What a
21
+ * dismissal does is the placement's `onOut`.
22
+ */
23
+ var dismiss_layer_exports = /* @__PURE__ */ __exportAll({
24
+ Dismiss: () => Dismiss,
25
+ LAYER_ATTRIBUTE: () => LAYER_ATTRIBUTE,
26
+ Layer: () => Layer$1,
27
+ Message: () => Message$9,
28
+ Model: () => Model$9,
29
+ TRIGGER_ATTRIBUTE: () => TRIGGER_ATTRIBUTE,
30
+ behavior: () => behavior$14,
31
+ bundle: () => bundle$9,
32
+ layersOnPath: () => layersOnPath,
33
+ toDismiss: () => toDismiss
34
+ });
35
+ const LAYER_ATTRIBUTE = "data-foldkit-plus-layer";
36
+ const TRIGGER_ATTRIBUTE = "data-foldkit-plus-layer-trigger";
37
+ const OUTSIDE_ATTRIBUTE = "data-foldkit-plus-layer-outside";
38
+ const ESCAPE_ATTRIBUTE = "data-foldkit-plus-layer-escape";
39
+ /** An open layer as the document showed it: bottom to top is DOM order. */
40
+ const Layer$1 = Schema.Struct({
41
+ id: Schema.String,
42
+ /** Dismiss on a press outside it. */
43
+ outside: Schema.Boolean,
44
+ /** Dismiss on Escape when topmost. */
45
+ escape: Schema.Boolean
46
+ });
47
+ const Model$9 = Schema.Struct({
48
+ /** The open layers at the last event, bottom to top. */
49
+ layers: Schema.Array(Layer$1) });
50
+ const Message$9 = defineMessageUnion({
51
+ /** A pointer went down; `inside` are the layers whose element or trigger was on the event's path. */
52
+ PressedAt: {
53
+ layers: Schema.Array(Layer$1),
54
+ inside: Schema.Array(Schema.String)
55
+ },
56
+ PressedEscape: { layers: Schema.Array(Layer$1) }
57
+ });
58
+ const Dismiss = Schema.TaggedStruct("Dismiss", { ids: Schema.Array(Schema.String) });
59
+ /** Which layers a press inside `inside` dismisses: every layer above the topmost one hit, or all when none was. */
60
+ const toDismiss = (layers, inside) => {
61
+ let topmostHit = -1;
62
+ layers.forEach((layer, index) => {
63
+ if (inside.includes(layer.id)) topmostHit = index;
64
+ });
65
+ return layers.slice(topmostHit + 1).filter((layer) => layer.outside).map((layer) => layer.id);
66
+ };
67
+ const sameLayers = (a, b) => a.length === b.length && a.every((layer, index) => layer.id === b[index].id && layer.outside === b[index].outside && layer.escape === b[index].escape);
68
+ const bundle$9 = Bundle.make("DismissLayer", {
69
+ Model: Model$9,
70
+ Message: Message$9,
71
+ init: () => ({ model: { layers: [] } }),
72
+ update: (model, message) => {
73
+ const next = sameLayers(model.layers, message.layers) ? model : { layers: message.layers };
74
+ return Message$9.match(message, {
75
+ PressedAt: ({ layers, inside }) => {
76
+ const ids = toDismiss(layers, inside);
77
+ return ids.length === 0 ? { model: next } : {
78
+ model: next,
79
+ outMessage: Dismiss.make({ ids })
80
+ };
81
+ },
82
+ PressedEscape: ({ layers }) => {
83
+ const top = layers[layers.length - 1];
84
+ return top !== void 0 && top.escape ? {
85
+ model: next,
86
+ outMessage: Dismiss.make({ ids: [top.id] })
87
+ } : { model: next };
88
+ }
89
+ });
90
+ },
91
+ subscriptions: () => Subscription.make()(() => ({ document: Subscription.persistent(documentEvents()) }))
92
+ });
93
+ const openLayers = (root) => Array.from(root.querySelectorAll(`[${LAYER_ATTRIBUTE}]`)).map((element) => ({
94
+ id: element.getAttribute("data-foldkit-plus-layer") ?? "",
95
+ outside: element.getAttribute(OUTSIDE_ATTRIBUTE) !== "false",
96
+ escape: element.getAttribute(ESCAPE_ATTRIBUTE) !== "false"
97
+ }));
98
+ /** The layer ids whose element or trigger is on the event's composed path. */
99
+ const layersOnPath = (path) => {
100
+ const ids = [];
101
+ for (const target of path) {
102
+ if (!(target instanceof Element)) continue;
103
+ const id = target.getAttribute("data-foldkit-plus-layer") ?? target.getAttribute("data-foldkit-plus-layer-trigger");
104
+ if (id !== null && !ids.includes(id)) ids.push(id);
105
+ }
106
+ return ids;
107
+ };
108
+ /**
109
+ * The facts are read inside the listener, not after: `composedPath()` is
110
+ * empty once dispatch has finished, and the open layers must be the ones the
111
+ * press saw, not the ones a later render left.
112
+ */
113
+ const documentEvents = () => {
114
+ if (typeof document === "undefined") return Stream.empty;
115
+ return Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
116
+ const onPointerDown = (event) => Queue.offerUnsafe(queue, Message$9.PressedAt({
117
+ layers: openLayers(document),
118
+ inside: layersOnPath(event.composedPath())
119
+ }));
120
+ const onKeyDown = (event) => {
121
+ if (event.key !== "Escape") return;
122
+ Queue.offerUnsafe(queue, Message$9.PressedEscape({ layers: openLayers(document) }));
123
+ };
124
+ document.addEventListener("pointerdown", onPointerDown, { capture: true });
125
+ document.addEventListener("keydown", onKeyDown, { capture: true });
126
+ return {
127
+ onPointerDown,
128
+ onKeyDown
129
+ };
130
+ }), ({ onPointerDown, onKeyDown }) => Effect.sync(() => {
131
+ document.removeEventListener("pointerdown", onPointerDown, { capture: true });
132
+ document.removeEventListener("keydown", onKeyDown, { capture: true });
133
+ })));
134
+ };
135
+ /**
136
+ * Marks the layer's container and trigger for the placed stack. The layer
137
+ * takes part only while its element is rendered, so an `open` flag in the
138
+ * parent Model is the whole lifecycle. Not for an element with `popover`,
139
+ * which the browser dismisses itself.
140
+ */
141
+ const behavior$14 = (_declared) => (slots) => (options) => Behavior.forSlots(slots)({
142
+ [options.layer]: Behavior.slot({
143
+ requires: { capability: Capability.Container },
144
+ attributes: ({ input, h }) => [
145
+ h.Attribute(LAYER_ATTRIBUTE, options.id(input)),
146
+ ...options.outsidePress === false ? [h.Attribute(OUTSIDE_ATTRIBUTE, "false")] : [],
147
+ ...options.escape === false ? [h.Attribute(ESCAPE_ATTRIBUTE, "false")] : []
148
+ ]
149
+ }),
150
+ ...options.trigger === void 0 ? {} : { [options.trigger]: Behavior.slot({
151
+ requires: { capability: Capability.Interactive },
152
+ attributes: ({ input, h }) => [h.Attribute(TRIGGER_ATTRIBUTE, options.id(input))]
153
+ }) }
154
+ }, { name: "DismissLayer" });
155
+ //#endregion
156
+ //#region src/interaction/editable-text.ts
157
+ /**
158
+ * `EditableText` as a Behavior: attaches the `foldkit-primitives/dom` Mount to
159
+ * a container slot and maps what it reports into the view's Messages. No
160
+ * Bundle: the text typed is a fact, and what it changes is the parent's
161
+ * `update`.
162
+ */
163
+ var editable_text_exports = /* @__PURE__ */ __exportAll({
164
+ EditAsked: () => EditAsked,
165
+ TextCancelled: () => TextCancelled,
166
+ TextCommitted: () => TextCommitted,
167
+ TextEdited: () => TextEdited,
168
+ behavior: () => behavior$13,
169
+ mount: () => EditableText,
170
+ textOf: () => textOf
171
+ });
172
+ const behavior$13 = (slots) => (options) => Behavior.forSlots(slots)({ [options.container]: Behavior.slot({
173
+ requires: { capability: Capability.Container },
174
+ mount: () => Mount.mapMessage(EditableText({ attribute: options.attribute }), options.toMessage)
175
+ }) }, { name: "EditableText" });
176
+ //#endregion
177
+ //#region src/interaction/focus-scope.ts
178
+ /**
179
+ * `FocusScope` as a Behavior: attaches the `foldkit-primitives/dom` Mount to a
180
+ * container slot. No Bundle, because which element has focus is a DOM fact.
181
+ */
182
+ var focus_scope_exports = /* @__PURE__ */ __exportAll({
183
+ behavior: () => behavior$12,
184
+ mount: () => FocusScope,
185
+ tabbableWithin: () => tabbableWithin
186
+ });
187
+ /**
188
+ * On insert the container focuses `initialFocus`, else its first tabbable
189
+ * descendant, else itself; with `contain`, Tab and Shift+Tab wrap inside and
190
+ * focus that lands outside comes back to the first tabbable; on unmount, with
191
+ * `restore`, focus returns to the element that had it. A dialog that is not
192
+ * a native `<dialog>`, a menu, or a command palette takes this.
193
+ */
194
+ const behavior$12 = (slots) => (options) => Behavior.forSlots(slots)({ [options.container]: Behavior.slot({
195
+ requires: { capability: Capability.Container },
196
+ mount: () => FocusScope({
197
+ contain: options.contain ?? true,
198
+ restore: options.restore ?? true,
199
+ initialFocus: options.initialFocus ?? null
200
+ })
201
+ }) }, { name: "FocusScope" });
202
+ //#endregion
203
+ //#region src/interaction/focus-visible.ts
204
+ var focus_visible_exports = /* @__PURE__ */ __exportAll({
205
+ Modality: () => Modality,
206
+ behavior: () => behavior$11,
207
+ bundle: () => InputModality,
208
+ modalityOfKey: () => modalityOfKey
209
+ });
210
+ const behavior$11 = (declared) => (slots) => (options) => Behavior.forSlots(slots)({ [options.target]: Behavior.slot({
211
+ requires: { capability: Capability.Focusable },
212
+ attributes: ({ input, h }) => input[declared.field].modality === "keyboard" ? [h.DataAttribute("focus-visible", "true")] : []
213
+ }) }, { name: "FocusVisible" });
214
+ //#endregion
215
+ //#region src/interaction/roving-tabindex.ts
216
+ /**
217
+ * One tab stop for a set of items (a toolbar, a tab list, a listbox): arrows
218
+ * move focus between them, the rest stay out of the tab order. The current
219
+ * item's id is the Model slice, so a reorder keeps the same item current and a
220
+ * resumed page knows where focus was. Which item comes next is a pure
221
+ * function of the items and the key; the Behavior wires it to the slots.
222
+ */
223
+ var roving_tabindex_exports = /* @__PURE__ */ __exportAll({
224
+ Args: () => Args$8,
225
+ Message: () => Message$8,
226
+ Model: () => Model$8,
227
+ Orientation: () => Orientation,
228
+ behavior: () => behavior$10,
229
+ bundle: () => bundle$8,
230
+ idSelector: () => idSelector,
231
+ itemAttributes: () => itemAttributes,
232
+ move: () => move$2,
233
+ tabStop: () => tabStop$1
234
+ });
235
+ const Orientation = Schema.Literals([
236
+ "vertical",
237
+ "horizontal",
238
+ "both"
239
+ ]);
240
+ const Model$8 = Schema.Struct({
241
+ /** The current item's id, or `null` before any item has been focused. */
242
+ current: Schema.NullOr(Schema.String) });
243
+ const Message$8 = defineMessageUnion({
244
+ /** An item became current: the user focused it, or a key moved there. */
245
+ Focused: { id: Schema.String } });
246
+ const Args$8 = Schema.Struct({
247
+ orientation: Orientation,
248
+ /** Wrap from the last enabled item to the first, and back. */
249
+ loop: Schema.Boolean,
250
+ /** Keep DOM focus on the container and point at the current item with
251
+ * `aria-activedescendant` instead of moving focus. */
252
+ virtual: Schema.Boolean
253
+ });
254
+ const bundle$8 = Bundle.make("RovingTabindex", {
255
+ Model: Model$8,
256
+ Message: Message$8,
257
+ args: Args$8,
258
+ init: () => ({ model: { current: null } }),
259
+ update: (model, message) => model.current === message.id ? { model } : { model: { current: message.id } }
260
+ });
261
+ const isPlain$1 = (modifiers) => !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey;
262
+ /**
263
+ * The index the key moves to from `current`, over the enabled indices only, or
264
+ * `undefined` when the key is not a navigation key here. `current` may be
265
+ * `-1` (nothing current yet): arrows then land on the first or last enabled
266
+ * item. Under `rtl`, left and right swap. PageUp and PageDown move by `page`
267
+ * and clamp at the ends, never wrapping.
268
+ */
269
+ const move$2 = (enabled, current, key, modifiers, options) => {
270
+ if (enabled.length === 0 || !isPlain$1(modifiers)) return void 0;
271
+ const first = enabled[0];
272
+ const last = enabled[enabled.length - 1];
273
+ if (key === "Home") return first;
274
+ if (key === "End") return last;
275
+ if (options.page !== void 0 && (key === "PageUp" || key === "PageDown")) {
276
+ const position = enabled.indexOf(current);
277
+ if (position === -1) return key === "PageDown" ? first : last;
278
+ const jumped = position + (key === "PageDown" ? options.page : -options.page);
279
+ return enabled[Math.min(enabled.length - 1, Math.max(0, jumped))];
280
+ }
281
+ const vertical = options.orientation !== "horizontal";
282
+ const horizontal = options.orientation !== "vertical";
283
+ const forwardKey = options.direction === "rtl" ? "ArrowLeft" : "ArrowRight";
284
+ const backwardKey = options.direction === "rtl" ? "ArrowRight" : "ArrowLeft";
285
+ const step = vertical && key === "ArrowDown" || horizontal && key === forwardKey ? 1 : vertical && key === "ArrowUp" || horizontal && key === backwardKey ? -1 : 0;
286
+ if (step === 0) return void 0;
287
+ const position = enabled.indexOf(current);
288
+ if (position === -1) return step === 1 ? first : last;
289
+ const next = position + step;
290
+ if (next < 0) return options.loop ? last : first;
291
+ if (next >= enabled.length) return options.loop ? first : last;
292
+ return enabled[next];
293
+ };
294
+ /** The item that holds the tab stop: the current one, else the first enabled. */
295
+ const tabStop$1 = (items, current) => {
296
+ const index = current === null ? -1 : items.indexOf(current);
297
+ if (index !== -1 && !items.isDisabled(index)) return index;
298
+ return items.enabled[0] ?? -1;
299
+ };
300
+ /** A selector for an element by id, safe for any id. */
301
+ const idSelector = (id) => `[id="${id.replace(/["\\]/g, "\\$&")}"]`;
302
+ /**
303
+ * The attributes one item gets: `tabindex` 0 when it holds the tab stop and
304
+ * -1 otherwise (none under `virtual`), and `OnFocus` reporting it current.
305
+ * Shared with `ListNavigation`.
306
+ */
307
+ const itemAttributes = (h, items, current, item, virtual, focused) => {
308
+ const id = item.id ?? items.ids[item.index];
309
+ if (id === void 0) return [];
310
+ const isStop = tabStop$1(items, current) === item.index;
311
+ return [...virtual ? [] : [h.Tabindex(isStop ? 0 : -1)], h.OnFocus(focused(id))];
312
+ };
313
+ /**
314
+ * Wires a placed `RovingTabindex` to the slots: the container's arrow, Home
315
+ * and End keys focus the next item (synchronously, then dispatch), each item
316
+ * gets `tabindex` 0 or -1 and reports `Focused` when focused. Under `virtual`
317
+ * DOM focus never leaves the container: the key only dispatches, the
318
+ * container carries `aria-activedescendant`, and items keep no `tabindex`.
319
+ *
320
+ * Every handled key is default-prevented. Nothing is written on dispose: the
321
+ * attributes are data, so a view that no longer attaches this leaves no
322
+ * `tabindex` behind.
323
+ */
324
+ const behavior$10 = (declared, args) => (slots) => (options) => {
325
+ const wrap = (id) => declared.wrapper.make(Message$8.Focused({ id }));
326
+ const slice = (input) => input[declared.field];
327
+ const itemsOf = perInput(options.items);
328
+ return Behavior.forSlots(slots)({
329
+ [options.container]: Behavior.slot({
330
+ requires: { capability: Capability.Interactive },
331
+ attributes: ({ input, h }) => {
332
+ const items = itemsOf(input);
333
+ const current = slice(input).current;
334
+ const from = current === null ? -1 : items.indexOf(current);
335
+ const direction = options.direction?.(input) ?? "ltr";
336
+ const stop = tabStop$1(items, current);
337
+ const stopId = stop === -1 ? void 0 : items.ids[stop];
338
+ const target = (key, modifiers) => {
339
+ const next = move$2(items.enabled, from, key, modifiers, {
340
+ ...args,
341
+ direction
342
+ });
343
+ return next === void 0 ? void 0 : items.ids[next];
344
+ };
345
+ if (args.virtual) return [h.OnKeyDownPreventDefault((key, modifiers) => Option.map(Option.fromNullishOr(target(key, modifiers)), wrap)), ...stopId === void 0 ? [] : [h.AriaActiveDescendant(stopId)]];
346
+ return [h.OnKeyDownFocus((key, modifiers) => Option.map(Option.fromNullishOr(target(key, modifiers)), (id) => ({
347
+ focusSelector: idSelector(id),
348
+ message: wrap(id)
349
+ })))];
350
+ }
351
+ }),
352
+ [options.item]: Behavior.slot({
353
+ requires: { capability: Capability.Focusable },
354
+ attributes: ({ input, h, item }) => {
355
+ if (item === void 0) return [];
356
+ return itemAttributes(h, itemsOf(input), slice(input).current, item, args.virtual, wrap);
357
+ }
358
+ })
359
+ }, { name: "RovingTabindex" });
360
+ };
361
+ //#endregion
362
+ //#region src/interaction/grid-navigation.ts
363
+ /**
364
+ * Two-dimensional arrow keys over a set of items laid out in rows of
365
+ * `columns` (a calendar grid, a color swatch picker, an emoji palette). The
366
+ * Model slice is the current item's id, as with `RovingTabindex`, so a
367
+ * reorder keeps the same item current. Which cell a key lands on is the pure
368
+ * `move`; the Behavior wires it to a container slot and a per-item slot and
369
+ * shares `RovingTabindex`'s item attributes, so the two are interchangeable
370
+ * on a view.
371
+ */
372
+ var grid_navigation_exports = /* @__PURE__ */ __exportAll({
373
+ Args: () => Args$7,
374
+ Message: () => Message$7,
375
+ Model: () => Model$7,
376
+ behavior: () => behavior$9,
377
+ bundle: () => bundle$7,
378
+ move: () => move$1
379
+ });
380
+ const Model$7 = Schema.Struct({
381
+ /** The current cell's id, or `null` before any cell has been focused. */
382
+ current: Schema.NullOr(Schema.String) });
383
+ const Message$7 = defineMessageUnion({
384
+ /** A cell became current: the user focused it, or a key moved there. */
385
+ Focused: { id: Schema.String } });
386
+ const Args$7 = Schema.Struct({
387
+ /** Cells per row; the last row may be short. */
388
+ columns: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(1))),
389
+ /** Left and right continue into the previous and next row; up and down
390
+ * continue into the previous and next column. Off, they stop at the edge. */
391
+ wrap: Schema.Boolean,
392
+ /** Keep DOM focus on the container and point at the current cell with
393
+ * `aria-activedescendant` instead of moving focus. */
394
+ virtual: Schema.Boolean
395
+ });
396
+ const bundle$7 = Bundle.make("GridNavigation", {
397
+ Model: Model$7,
398
+ Message: Message$7,
399
+ args: Args$7,
400
+ init: () => ({ model: { current: null } }),
401
+ update: (model, message) => model.current === message.id ? { model } : { model: { current: message.id } }
402
+ });
403
+ /**
404
+ * The index the key moves to from `current` over a grid of `count` cells in
405
+ * rows of `columns`, skipping cells not in `enabled`, or `undefined` when the
406
+ * key is not a grid key. Left and right step within the row, and across rows
407
+ * under `wrap`; up and down step within the column, and across columns under
408
+ * `wrap`. Home and End are the row's first and last enabled cell; with Ctrl
409
+ * they are the grid's. At an edge without `wrap` the key is consumed and
410
+ * `current` is returned. From `-1` (nothing current) a forward key lands on
411
+ * the first enabled cell and a backward key on the last. Under `rtl`, left
412
+ * and right swap.
413
+ */
414
+ const move$1 = (enabled, count, current, key, modifiers, options) => {
415
+ if (enabled.length === 0 || modifiers.altKey || modifiers.metaKey) return void 0;
416
+ const first = enabled[0];
417
+ const last = enabled[enabled.length - 1];
418
+ const isEnabled = (index) => enabled.includes(index);
419
+ const { columns } = options;
420
+ const row = (index) => Math.floor(index / columns);
421
+ if (key === "Home" || key === "End") {
422
+ if (modifiers.ctrlKey || current === -1) return key === "Home" ? first : last;
423
+ const inRow = enabled.filter((index) => row(index) === row(current));
424
+ return key === "Home" ? inRow[0] ?? current : inRow[inRow.length - 1] ?? current;
425
+ }
426
+ if (modifiers.ctrlKey) return void 0;
427
+ const forwardKey = options.direction === "rtl" ? "ArrowLeft" : "ArrowRight";
428
+ const backwardKey = options.direction === "rtl" ? "ArrowRight" : "ArrowLeft";
429
+ const axis = key === forwardKey || key === backwardKey ? "row" : key === "ArrowDown" || key === "ArrowUp" ? "column" : void 0;
430
+ if (axis === void 0) return void 0;
431
+ const step = key === forwardKey || key === "ArrowDown" ? 1 : -1;
432
+ if (current === -1) return step === 1 ? first : last;
433
+ if (axis === "row") {
434
+ for (let index = current + step; index >= 0 && index < count; index += step) {
435
+ if (!options.wrap && row(index) !== row(current)) return current;
436
+ if (isEnabled(index)) return index;
437
+ }
438
+ return current;
439
+ }
440
+ const column = current % columns;
441
+ for (let index = current + step * columns;; index += step * columns) {
442
+ if (index < 0 || index >= count) {
443
+ if (!options.wrap) return current;
444
+ const nextColumn = column + step;
445
+ if (nextColumn < 0 || nextColumn >= columns) return current;
446
+ const start = step === 1 ? nextColumn : nextColumn + columns * Math.floor((count - 1 - nextColumn) / columns);
447
+ for (let next = start; next >= 0 && next < count; next += step * columns) if (isEnabled(next)) return next;
448
+ return current;
449
+ }
450
+ if (isEnabled(index)) return index;
451
+ }
452
+ };
453
+ /**
454
+ * Wires a placed `GridNavigation` to the slots exactly as `RovingTabindex`
455
+ * does, with `move` above deciding the cell: the container's arrows, Home and
456
+ * End focus the next cell (synchronously, then dispatch) or, under `virtual`,
457
+ * only dispatch and point with `aria-activedescendant`; each cell gets
458
+ * `tabindex` 0 or -1 and reports `Focused` when focused.
459
+ */
460
+ const behavior$9 = (declared, args) => (slots) => (options) => {
461
+ const wrap = (id) => declared.wrapper.make(Message$7.Focused({ id }));
462
+ const slice = (input) => input[declared.field];
463
+ const itemsOf = perInput(options.items);
464
+ return Behavior.forSlots(slots)({
465
+ [options.container]: Behavior.slot({
466
+ requires: { capability: Capability.Interactive },
467
+ attributes: ({ input, h }) => {
468
+ const items = itemsOf(input);
469
+ const current = slice(input).current;
470
+ const from = current === null ? -1 : items.indexOf(current);
471
+ const direction = options.direction?.(input) ?? "ltr";
472
+ const stop = tabStop$1(items, current);
473
+ const stopId = stop === -1 ? void 0 : items.ids[stop];
474
+ const target = (key, modifiers) => {
475
+ const next = move$1(items.enabled, items.size, from, key, modifiers, {
476
+ columns: args.columns,
477
+ wrap: args.wrap,
478
+ direction
479
+ });
480
+ return next === void 0 ? void 0 : items.ids[next];
481
+ };
482
+ if (args.virtual) return [h.OnKeyDownPreventDefault((key, modifiers) => Option.map(Option.fromNullishOr(target(key, modifiers)), wrap)), ...stopId === void 0 ? [] : [h.AriaActiveDescendant(stopId)]];
483
+ return [h.OnKeyDownFocus((key, modifiers) => Option.map(Option.fromNullishOr(target(key, modifiers)), (id) => ({
484
+ focusSelector: idSelector(id),
485
+ message: wrap(id)
486
+ })))];
487
+ }
488
+ }),
489
+ [options.item]: Behavior.slot({
490
+ requires: { capability: Capability.Focusable },
491
+ attributes: ({ input, h, item }) => {
492
+ if (item === void 0) return [];
493
+ return itemAttributes(h, itemsOf(input), slice(input).current, item, args.virtual, wrap);
494
+ }
495
+ })
496
+ }, { name: "GridNavigation" });
497
+ };
498
+ //#endregion
499
+ //#region src/interaction/layers.ts
500
+ var layers_exports = /* @__PURE__ */ __exportAll({
501
+ hideOutside: () => hideOutside,
502
+ hideOutsideMount: () => HideOutside,
503
+ scrollLock: () => scrollLock,
504
+ scrollLockMount: () => ScrollLock
505
+ });
506
+ const mountOn = (name, mount) => (slots) => (options) => Behavior.forSlots(slots)({ [options.container]: Behavior.slot({
507
+ requires: { capability: Capability.Container },
508
+ mount
509
+ }) }, { name });
510
+ /** Locks the document's scroll while the container is mounted; nested locks release together. */
511
+ const scrollLock = mountOn("ScrollLock", () => ScrollLock());
512
+ /** Marks everything outside the container inert while it is mounted. */
513
+ const hideOutside = mountOn("HideOutside", () => HideOutside());
514
+ //#endregion
515
+ //#region src/interaction/live-announce.ts
516
+ /**
517
+ * Announcements for assistive technology: one Bundle, placed once, holds the
518
+ * text of a polite and an assertive live region. An announcement waits
519
+ * `debounceMs` so a burst reads once, then clears after `clearAfterMs`, both
520
+ * on Effect's clock with a generation so a superseded timer changes nothing.
521
+ * The same text twice gets a trailing no-break space toggled, which is what
522
+ * makes a screen reader read it again. `view` renders the two regions; put it
523
+ * once in the page and style them visually hidden.
524
+ */
525
+ var live_announce_exports = /* @__PURE__ */ __exportAll({
526
+ Args: () => Args$6,
527
+ Message: () => Message$6,
528
+ Model: () => Model$6,
529
+ Politeness: () => Politeness,
530
+ REGION_ATTRIBUTE: () => REGION_ATTRIBUTE,
531
+ bundle: () => bundle$6,
532
+ reread: () => reread,
533
+ say: () => say,
534
+ view: () => view
535
+ });
536
+ const Politeness = Schema.Literals(["polite", "assertive"]);
537
+ const Model$6 = Schema.Struct({
538
+ polite: Schema.String,
539
+ assertive: Schema.String,
540
+ /** Waiting for the debounce to read. */
541
+ pending: Schema.NullOr(Schema.Struct({
542
+ message: Schema.String,
543
+ politeness: Politeness
544
+ })),
545
+ generation: Schema.Number
546
+ });
547
+ const Message$6 = defineMessageUnion({
548
+ Announced: {
549
+ message: Schema.String,
550
+ politeness: Politeness
551
+ },
552
+ Read: { generation: Schema.Number },
553
+ Cleared: { generation: Schema.Number }
554
+ });
555
+ const positive = Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite()));
556
+ const Args$6 = Schema.Struct({
557
+ /** A burst of announcements inside this window reads once, the last one. */
558
+ debounceMs: positive,
559
+ /** How long the text stays in the region. */
560
+ clearAfterMs: positive
561
+ });
562
+ const NBSP = "\xA0";
563
+ /** The text to put in the region so that `next` is read even when it equals `current`. */
564
+ const reread = (current, next) => current === next ? `${next}${NBSP}` : current === `${next}${NBSP}` ? next : next;
565
+ const after = (ms, message, name, generation) => ({
566
+ name,
567
+ args: { generation },
568
+ effect: Effect.as(Effect.sleep(ms), message)
569
+ });
570
+ const bundle$6 = Bundle.make("LiveAnnounce", {
571
+ Model: Model$6,
572
+ Message: Message$6,
573
+ args: Args$6,
574
+ init: () => ({ model: {
575
+ polite: "",
576
+ assertive: "",
577
+ pending: null,
578
+ generation: 0
579
+ } }),
580
+ update: (model, message, args) => Message$6.match(message, {
581
+ Announced: ({ message: text, politeness }) => {
582
+ const generation = model.generation + 1;
583
+ return {
584
+ model: {
585
+ ...model,
586
+ pending: {
587
+ message: text,
588
+ politeness
589
+ },
590
+ generation
591
+ },
592
+ commands: [after(args.debounceMs, Message$6.Read({ generation }), "LiveAnnounce.read", generation)]
593
+ };
594
+ },
595
+ Read: ({ generation }) => {
596
+ if (generation !== model.generation || model.pending === null) return { model };
597
+ const { message: text, politeness } = model.pending;
598
+ const region = politeness === "polite" ? "polite" : "assertive";
599
+ return {
600
+ model: {
601
+ ...model,
602
+ pending: null,
603
+ [region]: reread(model[region], text)
604
+ },
605
+ commands: [after(args.clearAfterMs, Message$6.Cleared({ generation }), "LiveAnnounce.clear", generation)]
606
+ };
607
+ },
608
+ Cleared: ({ generation }) => generation === model.generation ? { model: {
609
+ ...model,
610
+ polite: "",
611
+ assertive: ""
612
+ } } : { model }
613
+ })
614
+ });
615
+ /** The parent Message that announces `text`, to return from `update` or `onOut`. */
616
+ const say = (declared) => (text, politeness = "polite") => declared.wrapper.make(Message$6.Announced({
617
+ message: text,
618
+ politeness
619
+ }));
620
+ const REGION_ATTRIBUTE = "data-foldkit-plus-live";
621
+ /**
622
+ * The two live regions. Render once, anywhere in the page, and hide them
623
+ * visually with a rule on `[data-foldkit-plus-live]`; they must stay in the
624
+ * accessibility tree, so no `display: none`.
625
+ */
626
+ const view = (model, h) => h.div([h.Attribute(REGION_ATTRIBUTE, "regions")], [h.div([
627
+ h.Attribute(REGION_ATTRIBUTE, "polite"),
628
+ h.AriaLive("polite"),
629
+ h.AriaAtomic(true)
630
+ ], [model.polite]), h.div([
631
+ h.Attribute(REGION_ATTRIBUTE, "assertive"),
632
+ h.AriaLive("assertive"),
633
+ h.AriaAtomic(true)
634
+ ], [model.assertive])]);
635
+ //#endregion
636
+ //#region src/interaction/typeahead.ts
637
+ /**
638
+ * Typeahead over a set of items: printable keys build a query that a timer
639
+ * clears, and the query picks the item to move to. The query lives in the
640
+ * Model so a replay sees the same picks; the timer runs on Effect's clock so
641
+ * TestClock drives it. Which item matches is a pure function.
642
+ */
643
+ var typeahead_exports = /* @__PURE__ */ __exportAll({
644
+ Args: () => Args$5,
645
+ Message: () => Message$5,
646
+ Model: () => Model$5,
647
+ behavior: () => behavior$8,
648
+ bundle: () => bundle$5,
649
+ isPrintable: () => isPrintable,
650
+ match: () => match
651
+ });
652
+ const Model$5 = Schema.Struct({
653
+ query: Schema.String,
654
+ /** Bumped per keystroke, carried by the clear it schedules, so a superseded
655
+ * timer's `Expired` changes nothing. */
656
+ generation: Schema.Number
657
+ });
658
+ const Message$5 = defineMessageUnion({
659
+ /** A printable key was pressed with no ctrl, alt or meta. */
660
+ Typed: { char: Schema.String },
661
+ /** The clear timer fired for `generation`. */
662
+ Expired: { generation: Schema.Number },
663
+ /** The query was dropped on purpose (a blur, a selection). */
664
+ Cleared: {}
665
+ });
666
+ const Args$5 = Schema.Struct({
667
+ /** How long the query survives after the last key. */
668
+ timeoutMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) });
669
+ const bundle$5 = Bundle.make("Typeahead", {
670
+ Model: Model$5,
671
+ Message: Message$5,
672
+ args: Args$5,
673
+ init: () => ({ model: {
674
+ query: "",
675
+ generation: 0
676
+ } }),
677
+ update: (model, message, args) => Message$5.match(message, {
678
+ Typed: ({ char }) => {
679
+ const generation = model.generation + 1;
680
+ return {
681
+ model: {
682
+ query: model.query + char,
683
+ generation
684
+ },
685
+ commands: [{
686
+ name: "Typeahead.expire",
687
+ args: { generation },
688
+ effect: Effect.as(Effect.sleep(args.timeoutMs), Message$5.Expired({ generation }))
689
+ }]
690
+ };
691
+ },
692
+ Expired: ({ generation }) => generation === model.generation ? clearQuery(model) : { model },
693
+ Cleared: () => clearQuery(model)
694
+ })
695
+ });
696
+ const normalize = (text) => text.trimStart().toLocaleLowerCase();
697
+ const isRepeated = (query) => query.length > 1 && [...query].every((char) => char === query[0]);
698
+ /**
699
+ * The enabled index whose text starts with `query`, searching forward from
700
+ * `current` and wrapping, or `undefined`. A query of one character, or one
701
+ * character repeated (`aaa`), starts *after* the current item so repeated
702
+ * presses cycle through the items that begin with it; a longer query starts
703
+ * *at* the current item, since the user is refining a match. Case and leading
704
+ * whitespace are ignored.
705
+ */
706
+ const match = (texts, enabled, query, current) => {
707
+ if (query === "" || enabled.length === 0) return void 0;
708
+ const needle = normalize(isRepeated(query) ? query[0] : query);
709
+ const position = enabled.indexOf(current);
710
+ const startAfter = needle.length === 1 || isRepeated(query);
711
+ const start = position === -1 ? 0 : startAfter ? position + 1 : position;
712
+ for (let offset = 0; offset < enabled.length; offset += 1) {
713
+ const index = enabled[(start + offset) % enabled.length];
714
+ if (normalize(texts[index] ?? "").startsWith(needle)) return index;
715
+ }
716
+ };
717
+ /** A single character with no ctrl, alt or meta: a typeahead key. */
718
+ const isPrintable = (key, modifiers) => key.length === 1 && !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey;
719
+ /**
720
+ * Wires a placed `Typeahead` to the host slot: a printable key extends the
721
+ * query, focuses the matching item by its id (which then reports itself
722
+ * current, through `RovingTabindex`'s `OnFocus`), and dispatches `Typed`.
723
+ * With no match the key still extends the query, so the next one can match;
724
+ * a space with an empty query is left to the host (it activates).
725
+ */
726
+ const behavior$8 = (declared) => (slots) => (options) => {
727
+ const wrap = (char) => declared.wrapper.make(Message$5.Typed({ char }));
728
+ return Behavior.forSlots(slots)({ [options.host]: Behavior.slot({
729
+ requires: { capability: Capability.Interactive },
730
+ attributes: ({ input, h }) => {
731
+ const items = options.items(input);
732
+ const query = input[declared.field].query;
733
+ const currentId = options.current(input);
734
+ const current = currentId === null ? -1 : items.indexOf(currentId);
735
+ return [h.OnKeyDownFocus((key, modifiers) => {
736
+ if (!isPrintable(key, modifiers)) return Option.none();
737
+ if (key === " " && query === "") return Option.none();
738
+ const texts = items.ids.map((_, index) => options.text(input, index));
739
+ const found = match(texts, items.enabled, query + key, current);
740
+ const target = found === void 0 ? currentId : items.ids[found];
741
+ return Option.some({
742
+ focusSelector: target === null || target === void 0 ? ":not(*)" : idSelector(target),
743
+ message: wrap(key)
744
+ });
745
+ })];
746
+ }
747
+ }) }, { name: "Typeahead" });
748
+ };
749
+ //#endregion
750
+ //#region src/interaction/list-navigation.ts
751
+ /**
752
+ * Keyboard navigation of a list in one placement: a roving tab stop, type to
753
+ * find, and PageUp and PageDown. One Bundle holds the current item's id and
754
+ * the typeahead query, so a printable key can move the pointer and extend the
755
+ * query in one transition, which two placements cannot do under `virtual`.
756
+ * The resolver allows one owner per event on a slot, so a host that needs
757
+ * both arrows and typeahead takes this, not `RovingTabindex` plus `Typeahead`.
758
+ */
759
+ var list_navigation_exports = /* @__PURE__ */ __exportAll({
760
+ Args: () => Args$4,
761
+ Message: () => Message$4,
762
+ Model: () => Model$4,
763
+ behavior: () => behavior$7,
764
+ bundle: () => bundle$4,
765
+ keyOutcome: () => keyOutcome
766
+ });
767
+ const Model$4 = Schema.Struct({
768
+ current: Schema.NullOr(Schema.String),
769
+ query: Schema.String,
770
+ generation: Schema.Number
771
+ });
772
+ const Message$4 = defineMessageUnion({
773
+ /** An item became current: focused, or reached by a key. */
774
+ Focused: { id: Schema.String },
775
+ /** A printable key extended the query; `match` is the item it now picks, if any. */
776
+ Typed: {
777
+ char: Schema.String,
778
+ match: Schema.NullOr(Schema.String)
779
+ },
780
+ Expired: { generation: Schema.Number },
781
+ Cleared: {}
782
+ });
783
+ const Args$4 = Schema.Struct({
784
+ orientation: Orientation,
785
+ loop: Schema.Boolean,
786
+ virtual: Schema.Boolean,
787
+ /** How long the typeahead query survives after the last key. */
788
+ timeoutMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),
789
+ /** How many enabled items PageUp and PageDown move. */
790
+ page: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0)))
791
+ });
792
+ const bundle$4 = Bundle.make("ListNavigation", {
793
+ Model: Model$4,
794
+ Message: Message$4,
795
+ args: Args$4,
796
+ init: () => ({ model: {
797
+ current: null,
798
+ query: "",
799
+ generation: 0
800
+ } }),
801
+ update: (model, message, args) => Message$4.match(message, {
802
+ Focused: ({ id }) => model.current === id ? { model } : { model: {
803
+ ...model,
804
+ current: id
805
+ } },
806
+ Typed: ({ char, match }) => {
807
+ const generation = model.generation + 1;
808
+ return {
809
+ model: {
810
+ current: match ?? model.current,
811
+ query: model.query + char,
812
+ generation
813
+ },
814
+ commands: [{
815
+ name: "ListNavigation.expire",
816
+ args: { generation },
817
+ effect: Effect.as(Effect.sleep(args.timeoutMs), Message$4.Expired({ generation }))
818
+ }]
819
+ };
820
+ },
821
+ Expired: ({ generation }) => generation === model.generation ? clearQuery(model) : { model },
822
+ Cleared: () => clearQuery(model)
823
+ })
824
+ });
825
+ const keyOutcome = (items, texts, state, key, modifiers, options) => {
826
+ const from = state.current === null ? -1 : items.indexOf(state.current);
827
+ const moved = move$2(items.enabled, from, key, modifiers, options);
828
+ const movedId = moved === void 0 ? void 0 : items.ids[moved];
829
+ if (movedId !== void 0) return {
830
+ _tag: "Navigate",
831
+ id: movedId
832
+ };
833
+ if (!isPrintable(key, modifiers)) return void 0;
834
+ if (key === " " && state.query === "") return void 0;
835
+ const found = match(texts, items.enabled, state.query + key, from);
836
+ return {
837
+ _tag: "Type",
838
+ char: key,
839
+ match: found === void 0 ? null : items.ids[found] ?? null
840
+ };
841
+ };
842
+ /**
843
+ * Wires a placed `ListNavigation` to the slots. The container gets one key
844
+ * handler: arrows, Home, End, PageUp and PageDown move by id; a printable key
845
+ * extends the query and moves to its match. Each item gets `tabindex` and
846
+ * `OnFocus` as under `RovingTabindex`. Under `virtual` nothing takes DOM focus
847
+ * and the container carries `aria-activedescendant`.
848
+ */
849
+ const behavior$7 = (declared, args) => (slots) => (options) => {
850
+ const wrap = (message) => declared.wrapper.make(message);
851
+ const focused = (id) => wrap(Message$4.Focused({ id }));
852
+ const slice = (input) => input[declared.field];
853
+ const itemsOf = perInput(options.items);
854
+ const outcomeMessage = (outcome) => outcome._tag === "Navigate" ? focused(outcome.id) : wrap(Message$4.Typed({
855
+ char: outcome.char,
856
+ match: outcome.match
857
+ }));
858
+ return Behavior.forSlots(slots)({
859
+ [options.container]: Behavior.slot({
860
+ requires: { capability: Capability.Interactive },
861
+ attributes: ({ input, h }) => {
862
+ const items = itemsOf(input);
863
+ const state = slice(input);
864
+ const moveOptions = {
865
+ orientation: args.orientation,
866
+ loop: args.loop,
867
+ direction: options.direction?.(input) ?? "ltr",
868
+ page: args.page
869
+ };
870
+ const outcome = (key, modifiers) => Option.fromNullishOr(keyOutcome(items, items.ids.map((_, index) => options.text(input, index)), state, key, modifiers, moveOptions));
871
+ const stop = tabStop$1(items, state.current);
872
+ const stopId = stop === -1 ? void 0 : items.ids[stop];
873
+ if (args.virtual) return [h.OnKeyDownPreventDefault((key, modifiers) => Option.map(outcome(key, modifiers), outcomeMessage)), ...stopId === void 0 ? [] : [h.AriaActiveDescendant(stopId)]];
874
+ return [h.OnKeyDownFocus((key, modifiers) => Option.map(outcome(key, modifiers), (result) => {
875
+ const target = result._tag === "Navigate" ? result.id : result.match ?? state.current;
876
+ return {
877
+ focusSelector: target === null ? ":not(*)" : idSelector(target),
878
+ message: outcomeMessage(result)
879
+ };
880
+ }))];
881
+ }
882
+ }),
883
+ [options.item]: Behavior.slot({
884
+ requires: { capability: Capability.Focusable },
885
+ attributes: ({ input, h, item }) => item === void 0 ? [] : itemAttributes(h, itemsOf(input), slice(input).current, item, args.virtual, focused)
886
+ })
887
+ }, { name: "ListNavigation" });
888
+ };
889
+ //#endregion
890
+ //#region src/interaction/press.ts
891
+ /**
892
+ * Press: pointer and keyboard activation of one element as one fact. A Mount
893
+ * reports what the element saw (pointer down, up, cancel, key down, up,
894
+ * click) with the detail Foldkit's declarative attributes do not carry; the
895
+ * Bundle's `update` decides what counts as a press; the ghost click that
896
+ * follows a touch is suppressed by a timed Command on Effect's clock, never
897
+ * by a clock read in a handler. Activation is an OutMessage, `Pressed`, so
898
+ * the placement must say what a press does.
899
+ */
900
+ var press_exports = /* @__PURE__ */ __exportAll({
901
+ Args: () => Args$3,
902
+ Message: () => Message$3,
903
+ Model: () => Model$3,
904
+ PointerType: () => PointerType,
905
+ Pressed: () => Pressed,
906
+ behavior: () => behavior$6,
907
+ bundle: () => bundle$3,
908
+ events: () => events
909
+ });
910
+ const PointerType = Schema.Literals([
911
+ "mouse",
912
+ "touch",
913
+ "pen",
914
+ "keyboard",
915
+ "virtual"
916
+ ]);
917
+ const Model$3 = Schema.Struct({
918
+ /** Down and not yet released or cancelled. For styling. */
919
+ pressed: Schema.Boolean,
920
+ /** The pointer that pressed, so another pointer's up cannot release it. */
921
+ pointerId: Schema.NullOr(Schema.Number),
922
+ /** Enter or Space is down. */
923
+ key: Schema.NullOr(Schema.String),
924
+ /** A click arriving before this generation's window ends is the ghost of a pointer press. */
925
+ suppressing: Schema.NullOr(Schema.Number),
926
+ generation: Schema.Number
927
+ });
928
+ const Message$3 = defineMessageUnion({
929
+ PointerDown: {
930
+ pointerId: Schema.Number,
931
+ button: Schema.Number,
932
+ pointerType: Schema.String
933
+ },
934
+ PointerUp: {
935
+ pointerId: Schema.Number,
936
+ pointerType: Schema.String,
937
+ shiftKey: Schema.Boolean
938
+ },
939
+ PointerCancelled: { pointerId: Schema.Number },
940
+ KeyDown: {
941
+ key: Schema.String,
942
+ repeat: Schema.Boolean
943
+ },
944
+ KeyUp: {
945
+ key: Schema.String,
946
+ shiftKey: Schema.Boolean
947
+ },
948
+ /** `detail` is 0 for a click no pointer made: keyboard on a native control, or assistive technology. */
949
+ Clicked: {
950
+ detail: Schema.Number,
951
+ shiftKey: Schema.Boolean
952
+ },
953
+ Unsuppressed: { generation: Schema.Number }
954
+ });
955
+ const Pressed = Schema.TaggedStruct("Pressed", {
956
+ pointerType: PointerType,
957
+ /** Shift was held, for a range selection. */
958
+ shiftKey: Schema.Boolean
959
+ });
960
+ const Args$3 = Schema.Struct({
961
+ /** How long after a pointer press a `click` is taken for its ghost. */
962
+ clickSuppressionMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite())) });
963
+ const idle = (model) => ({
964
+ ...model,
965
+ pressed: false,
966
+ pointerId: null,
967
+ key: null
968
+ });
969
+ const isActivationKey$1 = (key) => key === "Enter" || key === " ";
970
+ const asPointerType = (raw) => raw === "touch" || raw === "pen" ? raw : "mouse";
971
+ const bundle$3 = Bundle.make("Press", {
972
+ Model: Model$3,
973
+ Message: Message$3,
974
+ args: Args$3,
975
+ init: () => ({ model: {
976
+ pressed: false,
977
+ pointerId: null,
978
+ key: null,
979
+ suppressing: null,
980
+ generation: 0
981
+ } }),
982
+ update: (model, message, args) => Message$3.match(message, {
983
+ PointerDown: ({ pointerId, button }) => button !== 0 || model.pointerId !== null ? { model } : { model: {
984
+ ...model,
985
+ pressed: true,
986
+ pointerId
987
+ } },
988
+ PointerUp: ({ pointerId, pointerType, shiftKey }) => {
989
+ if (model.pointerId !== pointerId) return { model };
990
+ const generation = model.generation + 1;
991
+ return {
992
+ model: {
993
+ ...idle(model),
994
+ suppressing: generation,
995
+ generation
996
+ },
997
+ commands: [{
998
+ name: "Press.unsuppress",
999
+ args: { generation },
1000
+ effect: Effect.as(Effect.sleep(args.clickSuppressionMs), Message$3.Unsuppressed({ generation }))
1001
+ }],
1002
+ outMessage: Pressed.make({
1003
+ pointerType: asPointerType(pointerType),
1004
+ shiftKey
1005
+ })
1006
+ };
1007
+ },
1008
+ PointerCancelled: ({ pointerId }) => model.pointerId === pointerId ? { model: idle(model) } : { model },
1009
+ KeyDown: ({ key, repeat }) => !isActivationKey$1(key) || repeat || model.key !== null || model.pointerId !== null ? { model } : { model: {
1010
+ ...model,
1011
+ pressed: true,
1012
+ key
1013
+ } },
1014
+ KeyUp: ({ key, shiftKey }) => model.key === key ? {
1015
+ model: idle(model),
1016
+ outMessage: Pressed.make({
1017
+ pointerType: "keyboard",
1018
+ shiftKey
1019
+ })
1020
+ } : { model },
1021
+ Clicked: ({ detail, shiftKey }) => detail === 0 ? {
1022
+ model,
1023
+ outMessage: Pressed.make({
1024
+ pointerType: "virtual",
1025
+ shiftKey
1026
+ })
1027
+ } : model.suppressing !== null ? { model } : {
1028
+ model,
1029
+ outMessage: Pressed.make({
1030
+ pointerType: "mouse",
1031
+ shiftKey
1032
+ })
1033
+ },
1034
+ Unsuppressed: ({ generation }) => model.suppressing === generation ? { model: {
1035
+ ...model,
1036
+ suppressing: null
1037
+ } } : { model }
1038
+ })
1039
+ });
1040
+ /**
1041
+ * The element's press-relevant events as facts. Enter and Space key downs
1042
+ * are default-prevented so a native control does not also click, and Space
1043
+ * does not scroll; the Bundle owns activation. An element marked
1044
+ * `aria-disabled="true"` (which the Behavior writes) reports nothing, read
1045
+ * at event time so a toggle needs no remount. Nothing else is decided here.
1046
+ */
1047
+ const events = Mount.defineStream("PressEvents", {
1048
+ messages: [
1049
+ Message$3.PointerDown,
1050
+ Message$3.PointerUp,
1051
+ Message$3.PointerCancelled,
1052
+ Message$3.KeyDown,
1053
+ Message$3.KeyUp,
1054
+ Message$3.Clicked
1055
+ ],
1056
+ execute: ({ element }) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
1057
+ const offer = (message) => {
1058
+ if (element.getAttribute("aria-disabled") === "true") return;
1059
+ Queue.offerUnsafe(queue, message);
1060
+ };
1061
+ const listeners = [
1062
+ ["pointerdown", (event) => {
1063
+ const pointer = event;
1064
+ offer(Message$3.PointerDown({
1065
+ pointerId: pointer.pointerId ?? 0,
1066
+ button: pointer.button ?? 0,
1067
+ pointerType: pointer.pointerType ?? "mouse"
1068
+ }));
1069
+ }],
1070
+ ["pointerup", (event) => {
1071
+ const pointer = event;
1072
+ offer(Message$3.PointerUp({
1073
+ pointerId: pointer.pointerId ?? 0,
1074
+ pointerType: pointer.pointerType ?? "mouse",
1075
+ shiftKey: pointer.shiftKey ?? false
1076
+ }));
1077
+ }],
1078
+ ["pointerleave", (event) => offer(Message$3.PointerCancelled({ pointerId: event.pointerId ?? 0 }))],
1079
+ ["pointercancel", (event) => offer(Message$3.PointerCancelled({ pointerId: event.pointerId ?? 0 }))],
1080
+ ["keydown", (event) => {
1081
+ const key = event;
1082
+ if (isActivationKey$1(key.key)) key.preventDefault();
1083
+ offer(Message$3.KeyDown({
1084
+ key: key.key,
1085
+ repeat: key.repeat
1086
+ }));
1087
+ }],
1088
+ ["keyup", (event) => {
1089
+ const key = event;
1090
+ offer(Message$3.KeyUp({
1091
+ key: key.key,
1092
+ shiftKey: key.shiftKey
1093
+ }));
1094
+ }],
1095
+ ["click", (event) => {
1096
+ const click = event;
1097
+ offer(Message$3.Clicked({
1098
+ detail: click.detail ?? 0,
1099
+ shiftKey: click.shiftKey ?? false
1100
+ }));
1101
+ }]
1102
+ ];
1103
+ for (const [type, listener] of listeners) element.addEventListener(type, listener);
1104
+ return listeners;
1105
+ }), (listeners) => Effect.sync(() => {
1106
+ for (const [type, listener] of listeners) element.removeEventListener(type, listener);
1107
+ })))
1108
+ });
1109
+ /**
1110
+ * Attaches the events Mount to the target slot, mapped into the placement's
1111
+ * Messages, and writes `data-pressed` while down for styling. The element
1112
+ * decides nothing; a click, a touch, Enter, Space, or an assistive
1113
+ * technology's click all reach `update` and come out as one `Pressed`.
1114
+ */
1115
+ const behavior$6 = (declared) => (slots) => (options) => Behavior.forSlots(slots)({ [options.target]: Behavior.slot({
1116
+ requires: { capability: Capability.Interactive },
1117
+ attributes: ({ input, h }) => [...input[declared.field].pressed ? [h.DataAttribute("pressed", "true")] : [], ...options.disabled?.(input) === true ? [h.AriaDisabled(true)] : []],
1118
+ mount: () => Mount.mapMessage(events(), (message) => declared.wrapper.make(message))
1119
+ }) }, { name: "Press" });
1120
+ //#endregion
1121
+ //#region src/interaction/long-press.ts
1122
+ /**
1123
+ * LongPress: holding an element down for `thresholdMs` is one fact. It reads
1124
+ * the same facts `Press.events` reports; the threshold is a Command on
1125
+ * Effect's clock carrying a generation, so a release before it fires makes
1126
+ * its `Elapsed` a no-op. `LongPressed` is an OutMessage the placement must
1127
+ * handle. A short press is `Press`'s business, not this Bundle's.
1128
+ */
1129
+ var long_press_exports = /* @__PURE__ */ __exportAll({
1130
+ Args: () => Args$2,
1131
+ LongPressed: () => LongPressed,
1132
+ Message: () => Message$2,
1133
+ Model: () => Model$2,
1134
+ behavior: () => behavior$5,
1135
+ bundle: () => bundle$2
1136
+ });
1137
+ const Model$2 = Schema.Struct({
1138
+ /** Down, and the threshold has not passed. */
1139
+ holding: Schema.Boolean,
1140
+ pointerId: Schema.NullOr(Schema.Number),
1141
+ key: Schema.NullOr(Schema.String),
1142
+ generation: Schema.Number
1143
+ });
1144
+ const Message$2 = defineMessageUnion({
1145
+ PointerDown: {
1146
+ pointerId: Schema.Number,
1147
+ button: Schema.Number,
1148
+ pointerType: Schema.String
1149
+ },
1150
+ PointerUp: {
1151
+ pointerId: Schema.Number,
1152
+ pointerType: Schema.String,
1153
+ shiftKey: Schema.Boolean
1154
+ },
1155
+ PointerCancelled: { pointerId: Schema.Number },
1156
+ KeyDown: {
1157
+ key: Schema.String,
1158
+ repeat: Schema.Boolean
1159
+ },
1160
+ KeyUp: {
1161
+ key: Schema.String,
1162
+ shiftKey: Schema.Boolean
1163
+ },
1164
+ Clicked: {
1165
+ detail: Schema.Number,
1166
+ shiftKey: Schema.Boolean
1167
+ },
1168
+ Elapsed: {
1169
+ generation: Schema.Number,
1170
+ pointerType: PointerType
1171
+ }
1172
+ });
1173
+ const LongPressed = Schema.TaggedStruct("LongPressed", { pointerType: PointerType });
1174
+ const Args$2 = Schema.Struct({ thresholdMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) });
1175
+ const isActivationKey = (key) => key === "Enter" || key === " ";
1176
+ const released = (model) => ({
1177
+ ...model,
1178
+ holding: false,
1179
+ pointerId: null,
1180
+ key: null
1181
+ });
1182
+ const start = (model, args, by, pointerType) => {
1183
+ const generation = model.generation + 1;
1184
+ return {
1185
+ model: {
1186
+ holding: true,
1187
+ pointerId: "pointerId" in by ? by.pointerId : null,
1188
+ key: "key" in by ? by.key : null,
1189
+ generation
1190
+ },
1191
+ commands: [{
1192
+ name: "LongPress.elapse",
1193
+ args: { generation },
1194
+ effect: Effect.as(Effect.sleep(args.thresholdMs), Message$2.Elapsed({
1195
+ generation,
1196
+ pointerType
1197
+ }))
1198
+ }]
1199
+ };
1200
+ };
1201
+ const bundle$2 = Bundle.make("LongPress", {
1202
+ Model: Model$2,
1203
+ Message: Message$2,
1204
+ args: Args$2,
1205
+ init: () => ({ model: {
1206
+ holding: false,
1207
+ pointerId: null,
1208
+ key: null,
1209
+ generation: 0
1210
+ } }),
1211
+ update: (model, message, args) => Message$2.match(message, {
1212
+ PointerDown: ({ pointerId, button, pointerType }) => button !== 0 || model.holding ? { model } : start(model, args, { pointerId }, pointerType === "touch" || pointerType === "pen" ? pointerType : "mouse"),
1213
+ PointerUp: ({ pointerId }) => model.pointerId === pointerId ? { model: released(model) } : { model },
1214
+ PointerCancelled: ({ pointerId }) => model.pointerId === pointerId ? { model: released(model) } : { model },
1215
+ KeyDown: ({ key, repeat }) => !isActivationKey(key) || repeat || model.holding ? { model } : start(model, args, { key }, "keyboard"),
1216
+ KeyUp: ({ key }) => model.key === key ? { model: released(model) } : { model },
1217
+ Clicked: () => ({ model }),
1218
+ Elapsed: ({ generation, pointerType }) => model.holding && model.generation === generation ? {
1219
+ model: released(model),
1220
+ outMessage: LongPressed.make({ pointerType })
1221
+ } : { model }
1222
+ })
1223
+ });
1224
+ /**
1225
+ * Attaches `Press.events` to the target, mapped into this placement's
1226
+ * Messages, and writes `data-holding` while the element is down. `Press` and
1227
+ * `LongPress` on one slot are refused by the resolver (two Mounts of one
1228
+ * name), so today a slot takes one of them.
1229
+ */
1230
+ const behavior$5 = (declared) => (slots) => (options) => Behavior.forSlots(slots)({ [options.target]: Behavior.slot({
1231
+ requires: { capability: Capability.Interactive },
1232
+ attributes: ({ input, h }) => input[declared.field].holding ? [h.DataAttribute("holding", "true")] : [],
1233
+ mount: () => Mount.mapMessage(events(), (fact) => declared.wrapper.make(fact))
1234
+ }) }, { name: "LongPress" });
1235
+ //#endregion
1236
+ //#region src/interaction/move.ts
1237
+ /**
1238
+ * `Move` as a Behavior: attaches the `foldkit-primitives/dom` Mount to a
1239
+ * draggable slot and maps its facts into the view's Messages. No Bundle: a
1240
+ * drag's meaning (a threshold, a snap, a reorder) is the parent's `update`,
1241
+ * and the deltas are already facts.
1242
+ */
1243
+ var move_exports = /* @__PURE__ */ __exportAll({
1244
+ MoveEnded: () => MoveEnded,
1245
+ MoveStarted: () => MoveStarted,
1246
+ Moved: () => Moved,
1247
+ behavior: () => behavior$4,
1248
+ mount: () => Move
1249
+ });
1250
+ const behavior$4 = (slots) => (options) => Behavior.forSlots(slots)({ [options.handle]: Behavior.slot({
1251
+ requires: { capability: Capability.Draggable },
1252
+ mount: () => Mount.mapMessage(Move(), options.toMessage)
1253
+ }) }, { name: "Move" });
1254
+ //#endregion
1255
+ //#region src/interaction/selection.ts
1256
+ /**
1257
+ * Selection over a set of items: which ids are selected, and the anchor a
1258
+ * range extends from. `mode` is `'single'` (a click replaces), `'multiple'`
1259
+ * (a click toggles), or `'none'`. Single mode keeps one item selected unless
1260
+ * `allowEmpty`. A range needs the items' order, which the view knows and the
1261
+ * Bundle does not, so `Ranged` carries it. The Behavior says the state in
1262
+ * ARIA and wires a plain click; a Shift range comes from `Press`, whose
1263
+ * `Pressed` carries `shiftKey`.
1264
+ */
1265
+ var selection_exports = /* @__PURE__ */ __exportAll({
1266
+ Args: () => Args$1,
1267
+ Message: () => Message$1,
1268
+ Mode: () => Mode,
1269
+ Model: () => Model$1,
1270
+ behavior: () => behavior$3,
1271
+ between: () => between,
1272
+ bundle: () => bundle$1
1273
+ });
1274
+ const Mode = Schema.Literals([
1275
+ "single",
1276
+ "multiple",
1277
+ "none"
1278
+ ]);
1279
+ const Model$1 = Schema.Struct({
1280
+ /** In selection order. */
1281
+ selected: Schema.Array(Schema.String),
1282
+ /** Where the last plain selection happened; a range extends from it. */
1283
+ anchor: Schema.NullOr(Schema.String)
1284
+ });
1285
+ const Message$1 = defineMessageUnion({
1286
+ /** A plain activation: replaces in single mode, toggles in multiple mode. */
1287
+ Activated: { id: Schema.String },
1288
+ /** A Shift activation: selects everything between the anchor and `id` in `order`. */
1289
+ Ranged: {
1290
+ id: Schema.String,
1291
+ order: Schema.Array(Schema.String)
1292
+ },
1293
+ Replaced: { ids: Schema.Array(Schema.String) },
1294
+ Cleared: {}
1295
+ });
1296
+ const Args$1 = Schema.Struct({
1297
+ mode: Mode,
1298
+ /** In single mode, whether activating the selected item deselects it. */
1299
+ allowEmpty: Schema.Boolean
1300
+ });
1301
+ const without = (ids, id) => ids.filter((other) => other !== id);
1302
+ /** The ids between `from` and `to` in `order`, inclusive, in `order`'s order. */
1303
+ const between = (order, from, to) => {
1304
+ const end = order.indexOf(to);
1305
+ if (end === -1) return [];
1306
+ const start = from === null ? -1 : order.indexOf(from);
1307
+ if (start === -1) return [to];
1308
+ return order.slice(Math.min(start, end), Math.max(start, end) + 1);
1309
+ };
1310
+ const bundle$1 = Bundle.make("Selection", {
1311
+ Model: Model$1,
1312
+ Message: Message$1,
1313
+ args: Args$1,
1314
+ init: () => ({ model: {
1315
+ selected: [],
1316
+ anchor: null
1317
+ } }),
1318
+ update: (model, message, args) => Message$1.match(message, {
1319
+ Activated: ({ id }) => {
1320
+ if (args.mode === "none") return { model };
1321
+ const isSelected = model.selected.includes(id);
1322
+ if (args.mode === "single") return isSelected ? args.allowEmpty ? { model: {
1323
+ selected: [],
1324
+ anchor: id
1325
+ } } : { model } : { model: {
1326
+ selected: [id],
1327
+ anchor: id
1328
+ } };
1329
+ return { model: {
1330
+ selected: isSelected ? without(model.selected, id) : [...model.selected, id],
1331
+ anchor: id
1332
+ } };
1333
+ },
1334
+ Ranged: ({ id, order }) => {
1335
+ if (args.mode !== "multiple") return { model };
1336
+ const range = between(order, model.anchor, id);
1337
+ const inRange = new Set(range);
1338
+ const selected = [...model.selected.filter((other) => !inRange.has(other)), ...range];
1339
+ const anchor = model.anchor ?? id;
1340
+ return sameIds(selected, model.selected) && anchor === model.anchor ? { model } : { model: {
1341
+ selected,
1342
+ anchor
1343
+ } };
1344
+ },
1345
+ Replaced: ({ ids }) => {
1346
+ const selected = args.mode === "none" ? [] : [...new Set(ids)];
1347
+ return sameIds(selected, model.selected) ? { model } : { model: {
1348
+ selected,
1349
+ anchor: model.anchor
1350
+ } };
1351
+ },
1352
+ Cleared: () => model.selected.length === 0 ? { model } : { model: {
1353
+ selected: [],
1354
+ anchor: model.anchor
1355
+ } }
1356
+ })
1357
+ });
1358
+ /**
1359
+ * Writes `aria-selected` on each item from the placed slice, and
1360
+ * `aria-multiselectable` on the container in multiple mode; wires `OnClick`
1361
+ * to `Activated` unless told not to. A disabled item gets no click.
1362
+ */
1363
+ const behavior$3 = (declared, args) => (slots) => (options) => {
1364
+ const wrap = (message) => declared.wrapper.make(message);
1365
+ const itemsOf = perInput(options.items);
1366
+ const selectedOf = perInput((input) => new Set(input[declared.field].selected));
1367
+ return Behavior.forSlots(slots)({
1368
+ ...options.container === void 0 ? {} : { [options.container]: Behavior.slot({
1369
+ requires: { capability: Capability.Container },
1370
+ attributes: ({ h }) => [h.AriaMultiSelectable(args.mode === "multiple")]
1371
+ }) },
1372
+ [options.item]: Behavior.slot({
1373
+ requires: { capability: Capability.Interactive },
1374
+ attributes: ({ input, h, item }) => {
1375
+ if (item === void 0) return [];
1376
+ const items = itemsOf(input);
1377
+ const id = item.id ?? items.ids[item.index];
1378
+ if (id === void 0) return [];
1379
+ const clickable = options.click !== false && args.mode !== "none" && !items.isDisabled(item.index);
1380
+ return [h.AriaSelected(selectedOf(input).has(id)), ...clickable ? [h.OnClick(wrap(Message$1.Activated({ id })))] : []];
1381
+ }
1382
+ })
1383
+ }, { name: "Selection" });
1384
+ };
1385
+ //#endregion
1386
+ //#region src/interaction/targets.ts
1387
+ /**
1388
+ * `Targets` as a Behavior: attaches the `foldkit-primitives/dom` Mount to a
1389
+ * container slot and maps what it reports into the view's Messages. No Bundle:
1390
+ * which descendant is under the pointer, or was pressed, is already a fact, and
1391
+ * what it means is the parent's `update`.
1392
+ */
1393
+ var targets_exports = /* @__PURE__ */ __exportAll({
1394
+ TargetHovered: () => TargetHovered,
1395
+ TargetPressed: () => TargetPressed,
1396
+ behavior: () => behavior$2,
1397
+ mount: () => Targets,
1398
+ targetOf: () => targetOf
1399
+ });
1400
+ const behavior$2 = (slots) => (options) => Behavior.forSlots(slots)({ [options.container]: Behavior.slot({
1401
+ requires: { capability: Capability.Container },
1402
+ mount: () => Mount.mapMessage(Targets({
1403
+ attribute: options.attribute,
1404
+ preventDefault: options.preventDefault ?? false
1405
+ }), options.toMessage)
1406
+ }) }, { name: "Targets" });
1407
+ //#endregion
1408
+ //#region src/interaction/pointer-drag.ts
1409
+ /**
1410
+ * `PointerDrag` as a Behavior: attaches the `foldkit-primitives/dom` Mount to
1411
+ * a container slot and maps what it reports into the view's Messages. No
1412
+ * Bundle: what is dragged and where the pointer is are facts, and what a drop
1413
+ * means, and whether it is allowed, is the parent's `update`.
1414
+ */
1415
+ var pointer_drag_exports = /* @__PURE__ */ __exportAll({
1416
+ DRAG_THRESHOLD: () => 4,
1417
+ DragCancelled: () => DragCancelled,
1418
+ DragDropped: () => DragDropped,
1419
+ DragPlace: () => DragPlace,
1420
+ DragStarted: () => DragStarted,
1421
+ DragZone: () => DragZone,
1422
+ DraggedOver: () => DraggedOver,
1423
+ behavior: () => behavior$1,
1424
+ boxOf: () => boxOf,
1425
+ mount: () => PointerDrag,
1426
+ zoneOf: () => zoneOf
1427
+ });
1428
+ const behavior$1 = (slots) => (options) => Behavior.forSlots(slots)({ [options.container]: Behavior.slot({
1429
+ requires: { capability: Capability.Container },
1430
+ mount: () => Mount.mapMessage(PointerDrag(options.targets === void 0 ? { attribute: options.attribute } : {
1431
+ attribute: options.attribute,
1432
+ targets: options.targets
1433
+ }), options.toMessage)
1434
+ }) }, { name: "PointerDrag" });
1435
+ //#endregion
1436
+ //#region src/interaction/tree-navigation.ts
1437
+ /**
1438
+ * Keyboard navigation of a tree (a file explorer, a page's layers, a nested
1439
+ * menu), after the WAI-ARIA tree pattern: one tab stop, Up and Down through the
1440
+ * rows that are showing, Right to open a row or step into it, Left to close it
1441
+ * or step out to its parent, Home and End to the first and last row.
1442
+ *
1443
+ * The Model slice is the current row's id and which rows are open. Openness is
1444
+ * stored as the rows `toggled` away from a default, so a layers panel that
1445
+ * starts with everything open and a file tree that starts closed are the same
1446
+ * bundle with a different `openByDefault`, and neither stores every row.
1447
+ *
1448
+ * Which rows exist is the view's to say, each with its parent, in tree order.
1449
+ * What a key does is a pure function of those rows and the Model; the Behavior
1450
+ * wires it to the slots and writes the tree's ARIA attributes.
1451
+ */
1452
+ var tree_navigation_exports = /* @__PURE__ */ __exportAll({
1453
+ Args: () => Args,
1454
+ Message: () => Message,
1455
+ Model: () => Model,
1456
+ behavior: () => behavior,
1457
+ bundle: () => bundle,
1458
+ isOpen: () => isOpen,
1459
+ move: () => move,
1460
+ shown: () => shown,
1461
+ tabStop: () => tabStop
1462
+ });
1463
+ const Model = Schema.Struct({
1464
+ /** The current row's id, or `null` before any row has been focused. */
1465
+ current: Schema.NullOr(Schema.String),
1466
+ /** The rows whose open state differs from `openByDefault`. */
1467
+ toggled: Schema.Array(Schema.String)
1468
+ });
1469
+ const Message = defineMessageUnion({
1470
+ /** A row became current: the user focused it, or a key moved there. */
1471
+ Focused: { id: Schema.String },
1472
+ Opened: { id: Schema.String },
1473
+ Closed: { id: Schema.String }
1474
+ });
1475
+ const Args = Schema.Struct({
1476
+ /** Whether a row with children starts open. */
1477
+ openByDefault: Schema.Boolean });
1478
+ /** Whether a row is open, given the default and what was toggled. */
1479
+ const isOpen = (model, args, id) => args.openByDefault !== model.toggled.includes(id);
1480
+ const setOpen = (model, args, id, open) => isOpen(model, args, id) === open ? model : {
1481
+ ...model,
1482
+ toggled: model.toggled.includes(id) ? model.toggled.filter((toggled) => toggled !== id) : [...model.toggled, id]
1483
+ };
1484
+ const bundle = Bundle.make("TreeNavigation", {
1485
+ Model,
1486
+ Message,
1487
+ args: Args,
1488
+ init: () => ({ model: {
1489
+ current: null,
1490
+ toggled: []
1491
+ } }),
1492
+ update: (model, message, args) => {
1493
+ switch (message._tag) {
1494
+ case "Focused": return model.current === message.id ? { model } : { model: {
1495
+ ...model,
1496
+ current: message.id
1497
+ } };
1498
+ case "Opened": return { model: setOpen(model, args, message.id, true) };
1499
+ case "Closed": return { model: setOpen(model, args, message.id, false) };
1500
+ }
1501
+ }
1502
+ });
1503
+ /**
1504
+ * The rows that are showing: every row whose ancestors are all open, with its
1505
+ * level and its place among its siblings. A row whose parent is not a row is
1506
+ * treated as top-level.
1507
+ */
1508
+ const shown = (rows, model, args) => {
1509
+ const byId = new Map(rows.map((row) => [row.id, row]));
1510
+ const siblingsOf = /* @__PURE__ */ new Map();
1511
+ const positionOf = /* @__PURE__ */ new Map();
1512
+ for (const row of rows) {
1513
+ const parent = row.parent !== null && byId.has(row.parent) ? row.parent : null;
1514
+ const count = (siblingsOf.get(parent) ?? 0) + 1;
1515
+ siblingsOf.set(parent, count);
1516
+ positionOf.set(row.id, count);
1517
+ }
1518
+ const levels = /* @__PURE__ */ new Map();
1519
+ const showing = /* @__PURE__ */ new Map();
1520
+ const result = [];
1521
+ for (const row of rows) {
1522
+ const parent = row.parent !== null && byId.has(row.parent) ? row.parent : null;
1523
+ const level = parent === null ? 1 : (levels.get(parent) ?? 0) + 1;
1524
+ levels.set(row.id, level);
1525
+ const visible = parent === null || showing.get(parent) === true && isOpen(model, args, parent);
1526
+ showing.set(row.id, visible);
1527
+ if (!visible) continue;
1528
+ result.push({
1529
+ ...row,
1530
+ level,
1531
+ position: positionOf.get(row.id) ?? 1,
1532
+ siblings: siblingsOf.get(parent) ?? 1
1533
+ });
1534
+ }
1535
+ return result;
1536
+ };
1537
+ const isPlain = (modifiers) => !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey;
1538
+ /**
1539
+ * What a key does from the current row, over the rows that are showing, or
1540
+ * `undefined` when it is not a key the tree handles. Disabled rows are stepped
1541
+ * over. With nothing current, Down and Home land on the first row, Up and End
1542
+ * on the last. Right and Left swap under `rtl`.
1543
+ */
1544
+ const move = (rows, current, key, modifiers, model, args, direction = "ltr") => {
1545
+ const enabled = rows.filter((row) => row.disabled !== true);
1546
+ if (enabled.length === 0 || !isPlain(modifiers)) return void 0;
1547
+ const focus = (row) => row === void 0 ? void 0 : {
1548
+ _tag: "Focus",
1549
+ id: row.id
1550
+ };
1551
+ const at = enabled.findIndex((row) => row.id === current);
1552
+ const inward = direction === "rtl" ? "ArrowLeft" : "ArrowRight";
1553
+ const outward = direction === "rtl" ? "ArrowRight" : "ArrowLeft";
1554
+ if (key === "Home") return focus(enabled[0]);
1555
+ if (key === "End") return focus(enabled[enabled.length - 1]);
1556
+ if (key === "ArrowDown") return focus(at === -1 ? enabled[0] : enabled[Math.min(at + 1, enabled.length - 1)]);
1557
+ if (key === "ArrowUp") return focus(at === -1 ? enabled[enabled.length - 1] : enabled[Math.max(at - 1, 0)]);
1558
+ const row = enabled[at];
1559
+ if (row === void 0) return key === inward || key === outward ? focus(enabled[0]) : void 0;
1560
+ if (key === inward) {
1561
+ if (!row.branch) return void 0;
1562
+ if (!isOpen(model, args, row.id)) return {
1563
+ _tag: "Open",
1564
+ id: row.id
1565
+ };
1566
+ return focus(enabled.find((child) => child.parent === row.id));
1567
+ }
1568
+ if (key === outward) {
1569
+ if (row.branch && isOpen(model, args, row.id)) return {
1570
+ _tag: "Close",
1571
+ id: row.id
1572
+ };
1573
+ return focus(enabled.find((parent) => parent.id === row.parent));
1574
+ }
1575
+ };
1576
+ /** The row that holds the tab stop: the current one while it is showing, else the first. */
1577
+ const tabStop = (rows, current) => {
1578
+ return (rows.find((row) => row.id === current && row.disabled !== true) ?? rows.find((row) => row.disabled !== true))?.id;
1579
+ };
1580
+ /**
1581
+ * Wires a placed `TreeNavigation` to the slots. The container's keys move focus
1582
+ * to the row they pick, or open or close the current row with focus left where
1583
+ * it is. Each showing row gets its id, `role="treeitem"`, `aria-level` (and
1584
+ * the same level as `--fk-tree-level`, so one `calc` indents any depth),
1585
+ * `aria-posinset`, `aria-setsize`, `aria-expanded` when it has children,
1586
+ * `aria-disabled` when disabled, a roving `tabindex`, and `OnFocus` reporting
1587
+ * it current. Every handled key is default-prevented.
1588
+ */
1589
+ const behavior = (declared, args) => (slots) => (options) => {
1590
+ const wrap = (message) => declared.wrapper.make(message);
1591
+ const slice = (input) => input[declared.field];
1592
+ const domId = options.domId ?? ((id) => id);
1593
+ const prepare = perInput((input) => {
1594
+ const model = slice(input);
1595
+ const rows = shown(options.rows(input), model, args);
1596
+ return {
1597
+ model,
1598
+ rows,
1599
+ byId: new Map(rows.map((row) => [row.id, row])),
1600
+ stop: tabStop(rows, model.current)
1601
+ };
1602
+ });
1603
+ return Behavior.forSlots(slots)({
1604
+ [options.container]: Behavior.slot({
1605
+ requires: { capability: Capability.Interactive },
1606
+ mount: () => FollowTabStop(),
1607
+ attributes: ({ input, h }) => {
1608
+ const { model, rows } = prepare(input);
1609
+ const direction = options.direction?.(input) ?? "ltr";
1610
+ return [h.OnKeyDownFocus((key, modifiers) => {
1611
+ const action = move(rows, model.current, key, modifiers, model, args, direction);
1612
+ if (action === void 0) return Option.none();
1613
+ const message = action._tag === "Focus" ? Message.Focused({ id: action.id }) : action._tag === "Open" ? Message.Opened({ id: action.id }) : Message.Closed({ id: action.id });
1614
+ return Option.some({
1615
+ focusSelector: idSelector(domId(action.id)),
1616
+ message: wrap(message)
1617
+ });
1618
+ })];
1619
+ }
1620
+ }),
1621
+ [options.item]: Behavior.slot({
1622
+ requires: { capability: Capability.Focusable },
1623
+ attributes: ({ input, h, item }) => {
1624
+ if (item === void 0) return [];
1625
+ const { model, rows, byId, stop } = prepare(input);
1626
+ const row = item.id === void 0 ? rows[item.index] : byId.get(item.id);
1627
+ if (row === void 0) return [];
1628
+ return [
1629
+ h.Id(domId(row.id)),
1630
+ h.Role("treeitem"),
1631
+ h.AriaLevel(row.level),
1632
+ h.Style({ "--fk-tree-level": String(row.level) }),
1633
+ h.AriaPosinset(row.position),
1634
+ h.AriaSetsize(row.siblings),
1635
+ ...row.branch ? [h.AriaExpanded(isOpen(model, args, row.id))] : [],
1636
+ ...row.disabled === true ? [h.AriaDisabled(true)] : [],
1637
+ h.Tabindex(stop === row.id ? 0 : -1),
1638
+ h.OnFocus(wrap(Message.Focused({ id: row.id })))
1639
+ ];
1640
+ }
1641
+ })
1642
+ }, { name: "TreeNavigation" });
1643
+ };
1644
+ //#endregion
1645
+ export { dismiss_layer_exports as DismissLayer, editable_text_exports as EditableText, focus_scope_exports as FocusScope, focus_visible_exports as FocusVisible, grid_navigation_exports as GridNavigation, layers_exports as Layers, list_navigation_exports as ListNavigation, live_announce_exports as LiveAnnounce, long_press_exports as LongPress, move_exports as Move, pointer_drag_exports as PointerDrag, press_exports as Press, roving_tabindex_exports as RovingTabindex, selection_exports as Selection, targets_exports as Targets, tree_navigation_exports as TreeNavigation, typeahead_exports as Typeahead };
1646
+
1647
+ //# sourceMappingURL=index.mjs.map