foldkit-primitives 0.9.0 → 0.10.1

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 (59) hide show
  1. package/README.md +31 -1
  2. package/dist/device/index.d.mts +69 -36
  3. package/dist/device/index.d.mts.map +1 -1
  4. package/dist/device/index.mjs +34 -34
  5. package/dist/device/index.mjs.map +1 -1
  6. package/dist/dom/index.d.mts +68 -19
  7. package/dist/dom/index.d.mts.map +1 -1
  8. package/dist/dom/index.mjs +32 -25
  9. package/dist/dom/index.mjs.map +1 -1
  10. package/dist/{editable-text-BbsPOATY.d.mts → editable-text-Dvm-Fuqs.d.mts} +65 -3
  11. package/dist/editable-text-Dvm-Fuqs.d.mts.map +1 -0
  12. package/dist/events/index.d.mts +52 -31
  13. package/dist/events/index.d.mts.map +1 -1
  14. package/dist/events/index.mjs +40 -28
  15. package/dist/events/index.mjs.map +1 -1
  16. package/dist/{follow-tab-stop-BDxyoUPQ.mjs → follow-tab-stop-Ci1tqAIW.mjs} +63 -61
  17. package/dist/follow-tab-stop-Ci1tqAIW.mjs.map +1 -0
  18. package/dist/{index-Cppf-lBx.d.mts → index-CmmxqSEi.d.mts} +34 -15
  19. package/dist/index-CmmxqSEi.d.mts.map +1 -0
  20. package/dist/index.d.mts +3 -3
  21. package/dist/index.mjs +3 -3
  22. package/dist/{input-modality-m6v7__Xs.mjs → input-modality-BXD383jg.mjs} +9 -9
  23. package/dist/input-modality-BXD383jg.mjs.map +1 -0
  24. package/dist/{input-modality-DGOhAknO.d.mts → input-modality-BY-bSrEc.d.mts} +9 -2
  25. package/dist/input-modality-BY-bSrEc.d.mts.map +1 -0
  26. package/dist/interaction/index.d.mts +118 -118
  27. package/dist/interaction/index.d.mts.map +1 -1
  28. package/dist/interaction/index.mjs +3 -3
  29. package/dist/interaction/index.mjs.map +1 -1
  30. package/dist/media/index.d.mts +24 -3
  31. package/dist/media/index.d.mts.map +1 -1
  32. package/dist/media/index.mjs +23 -13
  33. package/dist/media/index.mjs.map +1 -1
  34. package/dist/motion/index.d.mts +1 -1
  35. package/dist/motion/index.mjs +1 -2
  36. package/dist/{motion-DGGBZ0wt.mjs → motion-DmFDppRn.mjs} +20 -20
  37. package/dist/{motion-DGGBZ0wt.mjs.map → motion-DmFDppRn.mjs.map} +1 -1
  38. package/dist/net/index.d.mts +63 -25
  39. package/dist/net/index.d.mts.map +1 -1
  40. package/dist/net/index.mjs +31 -31
  41. package/dist/net/index.mjs.map +1 -1
  42. package/dist/observers/index.d.mts +25 -0
  43. package/dist/observers/index.d.mts.map +1 -1
  44. package/dist/observers/index.mjs +22 -22
  45. package/dist/observers/index.mjs.map +1 -1
  46. package/dist/state/index.d.mts +98 -44
  47. package/dist/state/index.d.mts.map +1 -1
  48. package/dist/state/index.mjs +39 -39
  49. package/dist/state/index.mjs.map +1 -1
  50. package/dist/time/index.d.mts +35 -5
  51. package/dist/time/index.d.mts.map +1 -1
  52. package/dist/time/index.mjs +20 -20
  53. package/dist/time/index.mjs.map +1 -1
  54. package/package.json +14 -14
  55. package/dist/editable-text-BbsPOATY.d.mts.map +0 -1
  56. package/dist/follow-tab-stop-BDxyoUPQ.mjs.map +0 -1
  57. package/dist/index-Cppf-lBx.d.mts.map +0 -1
  58. package/dist/input-modality-DGOhAknO.d.mts.map +0 -1
  59. package/dist/input-modality-m6v7__Xs.mjs.map +0 -1
@@ -3,6 +3,13 @@ import * as Mount from "foldkit/mount";
3
3
  //#region src/dom/focus-scope.d.ts
4
4
  /** Tabbable descendants in DOM order: focusable, visible, and not `tabindex="-1"`. */
5
5
  declare const tabbableWithin: (root: Element) => ReadonlyArray<HTMLElement>;
6
+ /**
7
+ * A focus scope as a Mount: on insert it remembers what was focused, focuses
8
+ * an initial element inside, and with `contain` keeps Tab and Shift+Tab
9
+ * cycling inside; on unmount it gives focus back. Nothing crosses to the
10
+ * Model: which element had focus is a DOM fact, so this is element lifecycle,
11
+ * not state. It emits no Messages.
12
+ */
6
13
  declare const FocusScope: Mount.MountDefinitionWithArgs<"FocusScope", {
7
14
  /** Keep Tab, Shift+Tab, and a stray focus inside. */
8
15
  contain: Schema.Boolean;
@@ -14,7 +21,9 @@ declare const FocusScope: Mount.MountDefinitionWithArgs<"FocusScope", {
14
21
  }, never>;
15
22
  //#endregion
16
23
  //#region src/dom/layers.d.ts
24
+ /** Locks the document's scroll while the element is mounted; nested locks release with the last. */
17
25
  declare const ScrollLock: Mount.MountDefinitionNoArgs<"ScrollLock", never>;
26
+ /** Makes everything outside the element inert while it is mounted, and restores it after. */
18
27
  declare const HideOutside: Mount.MountDefinitionNoArgs<"HideOutside", never>;
19
28
  //#endregion
20
29
  //#region src/dom/move.d.ts
@@ -38,6 +47,13 @@ declare const MoveEnded: Schema.TaggedStruct<"MoveEnded", {
38
47
  }>;
39
48
  type MoveEnded = typeof MoveEnded.Type;
40
49
  type Fact = MoveStarted | Moved | MoveEnded;
50
+ /**
51
+ * Pointer movement on an element as a Mount: a primary-button pointer down
52
+ * captures the pointer, each move reports the delta from where it started,
53
+ * and up, cancel, or lost capture ends it. Nothing is decided here: a drag
54
+ * threshold, a snap, or a constraint is the parent's `update`. Capture is
55
+ * released with the Mount.
56
+ */
41
57
  declare const Move: Mount.MountDefinitionNoArgs<"Move", {
42
58
  readonly _tag: "MoveStarted";
43
59
  readonly pointerId: number;
@@ -71,6 +87,16 @@ type TargetPressed = typeof TargetPressed.Type;
71
87
  type TargetFact = TargetHovered | TargetPressed;
72
88
  /** The id of the nearest marked element at or above `from`, inside `container`. */
73
89
  declare const targetOf: (container: Element, from: EventTarget | null, attribute: string) => string | null;
90
+ /**
91
+ * Which marked descendant of an element the pointer is over, and which one was
92
+ * pressed, as a Mount: one set of listeners on the container, not one per item.
93
+ * A descendant is marked by an attribute whose value is its id, such as a
94
+ * canvas's `data-composition-node` or a table's `data-row`. The nearest marked
95
+ * ancestor of the event's target, inside the container, is the one reported.
96
+ *
97
+ * What hovering or pressing means (a selection, an outline, a menu) is the
98
+ * parent's `update`.
99
+ */
74
100
  declare const Targets: Mount.MountDefinitionWithArgs<"Targets", {
75
101
  /** The attribute that marks a descendant, holding its id. */
76
102
  attribute: Schema.String;
@@ -151,6 +177,20 @@ declare const zoneOf: (box: Box, y: number) => DragZone;
151
177
  * first child's stands for it, as an editor's node wrapper's does.
152
178
  */
153
179
  declare const boxOf: (element: Element) => Box;
180
+ /**
181
+ * Dragging one marked descendant of an element onto another, as a Mount. A
182
+ * descendant is marked by an attribute whose value is its id, as `Targets`
183
+ * marks them; a press on one that then moves past a threshold starts a drag,
184
+ * and the pointer's place over another marked descendant is reported with the
185
+ * zone of it the pointer is in: its first third (`before`), its middle
186
+ * (`inside`), or its last third (`after`). Releasing drops, and Escape or a
187
+ * cancelled pointer ends the drag with nothing dropped.
188
+ *
189
+ * It writes no roles, no `tabindex` and no key handling, so the marked
190
+ * elements can be anything: a tree's rows, a canvas's nodes. The keyboard's way
191
+ * to do what a drag does is the parent's to give. Geometry is measured when the
192
+ * pointer moves and never kept; what a drop means is the parent's `update`.
193
+ */
154
194
  declare const PointerDrag: Mount.MountDefinitionWithArgs<"PointerDrag", {
155
195
  /** The attribute that marks a descendant, holding its id. */
156
196
  attribute: Schema.String;
@@ -178,7 +218,7 @@ declare const PointerDrag: Mount.MountDefinitionWithArgs<"PointerDrag", {
178
218
  readonly _tag: "DraggedOver";
179
219
  readonly over: {
180
220
  readonly id: string;
181
- readonly zone: "inside" | "before" | "after";
221
+ readonly zone: "after" | "before" | "inside";
182
222
  readonly part?: string;
183
223
  } | null;
184
224
  readonly region?: boolean;
@@ -187,7 +227,7 @@ declare const PointerDrag: Mount.MountDefinitionWithArgs<"PointerDrag", {
187
227
  readonly id: string;
188
228
  readonly over: {
189
229
  readonly id: string;
190
- readonly zone: "inside" | "before" | "after";
230
+ readonly zone: "after" | "before" | "inside";
191
231
  readonly part?: string;
192
232
  } | null;
193
233
  readonly region?: boolean;
@@ -225,6 +265,28 @@ type TextFact = TextEdited | TextCommitted | TextCancelled | EditAsked;
225
265
  * itself, which is not the author's; in one line, the author's breaks are spaces.
226
266
  */
227
267
  declare const textOf: (element: HTMLElement) => string;
268
+ /**
269
+ * Text typed into an editable descendant of an element, as a Mount: one set of
270
+ * listeners on the container, like `Targets`. A field is a descendant marked
271
+ * by an attribute whose value names it, while it is `contenteditable`. The
272
+ * view decides which one is; the Mount focuses it as it becomes so (made
273
+ * editable, or drawn editable), with the caret at its end, and at no other
274
+ * time. When Enter or Escape ends an edit and the view then removes the field
275
+ * or makes it no longer editable, focus, left on nothing, comes back to the
276
+ * container, so the next key reaches it; give the container a `tabindex`. A double-click on a marked field that is not editable yet asks for it
277
+ * to be (`EditAsked`).
278
+ *
279
+ * It reads `innerText`, never `innerHTML`, so what arrives is text. A field is
280
+ * one line unless it carries `aria-multiline="true"`: in one line, a line
281
+ * break becomes a space. Nothing is reported while an input method composes;
282
+ * the composed text arrives once, when it is done.
283
+ *
284
+ * Enter commits (Shift+Enter breaks a line in a multiline field), and so does
285
+ * leaving the field. Escape cancels, and puts the text the field had when
286
+ * editing began back in the DOM, since a view that stopped redrawing the field
287
+ * while it was edited will not. Where the browser lacks `plaintext-only`, a
288
+ * paste is inserted as text. What an edit means is the parent's `update`.
289
+ */
228
290
  declare const EditableText: Mount.MountDefinitionWithArgs<"EditableText", {
229
291
  /** The attribute that marks a field, holding its name. */
230
292
  attribute: Schema.String;
@@ -246,4 +308,4 @@ declare const EditableText: Mount.MountDefinitionWithArgs<"EditableText", {
246
308
  }>;
247
309
  //#endregion
248
310
  export { ScrollLock as A, targetOf as C, MoveStarted as D, MoveEnded as E, tabbableWithin as M, Moved as O, Targets as S, Move as T, boxOf as _, TextEdited as a, TargetHovered as b, DRAG_THRESHOLD as c, DragFact as d, DragPlace as f, PointerDrag as g, DraggedOver as h, TextCommitted as i, FocusScope as j, HideOutside as k, DragCancelled as l, DragZone as m, EditableText as n, TextFact as o, DragStarted as p, TextCancelled as r, textOf as s, EditAsked as t, DragDropped as u, zoneOf as v, Fact as w, TargetPressed as x, TargetFact as y };
249
- //# sourceMappingURL=editable-text-BbsPOATY.d.mts.map
311
+ //# sourceMappingURL=editable-text-Dvm-Fuqs.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editable-text-Dvm-Fuqs.d.mts","names":[],"sources":["../src/dom/focus-scope.ts","../src/dom/layers.ts","../src/dom/move.ts","../src/dom/targets.ts","../src/dom/pointer-drag.ts","../src/dom/editable-text.ts"],"mappings":";;;;cAmBa,iBAAc,MAAU,YAAU,cAAc;;;;;;;;cAmBhD,YAAU,MAAA;;;;;;;;;;;;cCtBV,YAAU,MAAA;;cAWV,aAAW,MAAA;;;cCxBX,aAAW,OAAA;;;;KAIZ,qBAAqB,YAAY;cAEhC,OAAK,OAAA;;;;;;;KAON,eAAe,MAAM;cAEpB,WAAS,OAAA;;;;;KAKV,mBAAmB,UAAU;KAE7B,OAAO,cAAc,QAAQ;;;;;;;;cAsB5B,MAAI,MAAA;;;;;;;;;;;;;;;;;cC5CJ,eAAa,OAAA;;;;KAId,uBAAuB,cAAc;cAEpC,eAAa,OAAA;;;;;;;KAOd,uBAAuB,cAAc;KAErC,aAAa,gBAAgB;;cAU5B,WAAQ,WAAe,SAAO,MAAQ,oBAAkB;;;;;;;;;;;cAgBxD,SAAO,MAAA;;;;;;;;;;;;;;;;;;cCxCP,UAAQ,OAAA;KACT,kBAAkB,SAAS;;;;;;cAO1B,WAAS,OAAA;;;;;KAKV,mBAAmB,UAAU;cAE5B,aAAW,OAAA;;eAAX,OAAA;;KAID,qBAAqB,YAAY;cAEhC,aAAW,OAAA;;;;;;;;;;;;;;KAUZ,qBAAqB,YAAY;cAEhC,aAAW,OAAA;;;;;;;;;;KAMZ,qBAAqB,YAAY;cAEhC,eAAa,OAAA;eAAb,OAAA;;KACD,uBAAuB,cAAc;KAErC,WAAW,cAAc,cAAc,cAAc;;cAGpD;UAEH;WACC;WACA;;;cAIE,SAAM,KAAS,KAAG,cAAc;;;;;cAUhC,QAAK,SAAa,YAAU;;;;;;;;;;;;;;;cA8B5B,aAAW,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cChGX,YAAU,OAAA;;;;;KAKX,oBAAoB,WAAW;cAE9B,eAAa,OAAA;;;;KAId,uBAAuB,cAAc;cAEpC,eAAa,OAAA;;;;;KAKd,uBAAuB,cAAc;cAEpC,WAAS,OAAA;;kBAAT,OAAA;;KAID,mBAAmB,UAAU;KAE7B,WAAW,aAAa,gBAAgB,gBAAgB;;;;;cASvD,SAAM,SAAa;;;;;;;;;;;;;;;;;;;;;;;cA0CnB,cAAY,MAAA;;aAAZ,OAAA"}
@@ -1,22 +1,30 @@
1
- import { a as modalityOfKey, i as Modality, n as InputModalityMessage, r as InputModalityModel, t as InputModality } from "../input-modality-DGOhAknO.mjs";
1
+ import { a as modalityOfKey, i as Modality, n as InputModalityMessage, r as InputModalityModel, t as InputModality } from "../input-modality-BY-bSrEc.mjs";
2
2
  import { Schema, Stream } from "effect";
3
3
  import { Bundle } from "foldkit-bundle";
4
4
  //#region src/events/active-element.d.ts
5
5
  export declare const ActiveElementMessage: import("foldkit/message").MessageUnion<{
6
6
  readonly Changed: {
7
- readonly tag: Schema.String;
8
- readonly id: Schema.NullOr<Schema.String>;
7
+ tag: Schema.String;
8
+ id: Schema.NullOr<Schema.String>;
9
9
  };
10
10
  }>;
11
11
  export type ActiveElementMessage = typeof ActiveElementMessage.Type;
12
12
  /**
13
13
  * Focus changes on the document. Without a document the stream is empty
14
- * instead of throwing. Lift with `Subscription.persistent`, mapping into
14
+ * instead of throwing. Lift with `Subscription.persistentEntry`, mapping into
15
15
  * the parent's Message.
16
16
  */
17
17
  export declare const activeElementEvents: () => Stream.Stream<ActiveElementMessage>;
18
18
  //#endregion
19
19
  //#region src/events/hotkeys.d.ts
20
+ export interface KeyPress {
21
+ readonly key: string;
22
+ readonly repeat: boolean;
23
+ readonly ctrl: boolean;
24
+ readonly shift: boolean;
25
+ readonly alt: boolean;
26
+ readonly meta: boolean;
27
+ }
20
28
  /**
21
29
  * Hotkey matching as a pure function over a `Pressed` payload: `matchHotkey`
22
30
  * answers whether a press is the shortcut, so `update` stays a table of
@@ -29,14 +37,6 @@ export declare const activeElementEvents: () => Stream.Stream<ActiveElementMessa
29
37
  * no key, or two keys, matches nothing: predicates return false, they do
30
38
  * not throw on the hot path.
31
39
  */
32
- export interface KeyPress {
33
- readonly key: string;
34
- readonly repeat: boolean;
35
- readonly ctrl: boolean;
36
- readonly shift: boolean;
37
- readonly alt: boolean;
38
- readonly meta: boolean;
39
- }
40
40
  export declare const matchHotkey: (pattern: string, press: KeyPress) => boolean;
41
41
  //#endregion
42
42
  //#region src/events/idle.d.ts
@@ -49,34 +49,42 @@ export declare const IdleMessage: import("foldkit/message").MessageUnion<{
49
49
  readonly BecameActive: {};
50
50
  }>;
51
51
  export type IdleMessage = typeof IdleMessage.Type;
52
+ /**
53
+ * Idleness as a bundle: `idle` in the Model, flipped by activity and
54
+ * silence. While active, activity debounced past `timeoutMs` settles to
55
+ * `BecameIdle`; while idle, the first activity wakes to `BecameActive` and
56
+ * the dependency flip restarts the watch. Starts active — a lurker idles
57
+ * when the timeout elapses. The clock is Effect's, so tests drive it with
58
+ * TestClock.
59
+ */
52
60
  export declare const Idle: Bundle.Bundle<"Idle", {
53
61
  readonly timeoutMs: number;
54
62
  }, {
55
63
  readonly idle: boolean;
56
64
  }, {
57
- readonly _tag: "BecameIdle";
58
- } | {
59
65
  readonly _tag: "BecameActive";
66
+ } | {
67
+ readonly _tag: "BecameIdle";
60
68
  }, never, never, never, void, {}, {}>;
61
69
  //#endregion
62
70
  //#region src/events/keyboard.d.ts
63
71
  export declare const KeyboardMessage: import("foldkit/message").MessageUnion<{
64
72
  readonly Pressed: {
65
- readonly key: Schema.String;
66
- readonly repeat: Schema.Boolean;
67
- readonly ctrl: Schema.Boolean;
68
- readonly shift: Schema.Boolean;
69
- readonly alt: Schema.Boolean;
70
- readonly meta: Schema.Boolean;
73
+ key: Schema.String;
74
+ repeat: Schema.Boolean;
75
+ ctrl: Schema.Boolean;
76
+ shift: Schema.Boolean;
77
+ alt: Schema.Boolean;
78
+ meta: Schema.Boolean;
71
79
  };
72
80
  readonly Released: {
73
- readonly key: Schema.String;
81
+ key: Schema.String;
74
82
  };
75
83
  }>;
76
84
  export type KeyboardMessage = typeof KeyboardMessage.Type;
77
85
  /**
78
86
  * Key presses and releases on the window. Without a window the stream is
79
- * empty instead of throwing. Lift with `Subscription.persistent`, mapping
87
+ * empty instead of throwing. Lift with `Subscription.persistentEntry`, mapping
80
88
  * into the parent's Message.
81
89
  */
82
90
  export declare const keyboardEvents: (options?: {
@@ -91,14 +99,14 @@ export declare const keyboardEvents: (options?: {
91
99
  //#region src/events/pointer.d.ts
92
100
  export declare const PointerMessage: import("foldkit/message").MessageUnion<{
93
101
  readonly Moved: {
94
- readonly x: Schema.Number;
95
- readonly y: Schema.Number;
102
+ x: Schema.Number;
103
+ y: Schema.Number;
96
104
  };
97
105
  }>;
98
106
  export type PointerMessage = typeof PointerMessage.Type;
99
107
  /**
100
108
  * Pointer moves on the window. Without a window the stream is empty instead
101
- * of throwing. Lift with `Subscription.persistent`, mapping into the
109
+ * of throwing. Lift with `Subscription.persistentEntry`, mapping into the
102
110
  * parent's Message.
103
111
  */
104
112
  export declare const pointerEvents: () => Stream.Stream<PointerMessage>;
@@ -106,15 +114,15 @@ export declare const pointerEvents: () => Stream.Stream<PointerMessage>;
106
114
  //#region src/events/scroll.d.ts
107
115
  export declare const ScrollMessage: import("foldkit/message").MessageUnion<{
108
116
  readonly Scrolled: {
109
- readonly x: Schema.Number;
110
- readonly y: Schema.Number;
117
+ x: Schema.Number;
118
+ y: Schema.Number;
111
119
  };
112
120
  }>;
113
121
  export type ScrollMessage = typeof ScrollMessage.Type;
114
122
  /**
115
123
  * Scrolls on the window, in the capture phase so container scrolls (which
116
124
  * do not bubble) report too. Without a window the stream is empty instead
117
- * of throwing. Lift with `Subscription.persistent`, mapping into the
125
+ * of throwing. Lift with `Subscription.persistentEntry`, mapping into the
118
126
  * parent's Message.
119
127
  */
120
128
  export declare const scrollEvents: () => Stream.Stream<ScrollMessage>;
@@ -126,10 +134,16 @@ export declare const VisibilityModel: Schema.Struct<{
126
134
  export type VisibilityModel = typeof VisibilityModel.Type;
127
135
  export declare const VisibilityMessage: import("foldkit/message").MessageUnion<{
128
136
  readonly Changed: {
129
- readonly visible: Schema.Boolean;
137
+ visible: Schema.Boolean;
130
138
  };
131
139
  }>;
132
140
  export type VisibilityMessage = typeof VisibilityMessage.Type;
141
+ /**
142
+ * Page visibility as a bundle: `visible` in the Model, read from the
143
+ * document at startup and kept current by `visibilitychange`. A hidden page
144
+ * is the application's cue to pause polling, animations, and realtime
145
+ * streams; the decision stays in application `update`, not here.
146
+ */
133
147
  export declare const Visibility: Bundle.Bundle<"Visibility", void, {
134
148
  readonly visible: boolean;
135
149
  }, {
@@ -145,11 +159,18 @@ export declare const WindowSizeModel: Schema.Struct<{
145
159
  export type WindowSizeModel = typeof WindowSizeModel.Type;
146
160
  export declare const WindowSizeMessage: import("foldkit/message").MessageUnion<{
147
161
  readonly Changed: {
148
- readonly width: Schema.Number;
149
- readonly height: Schema.Number;
162
+ width: Schema.Number;
163
+ height: Schema.Number;
150
164
  };
151
165
  }>;
152
166
  export type WindowSizeMessage = typeof WindowSizeMessage.Type;
167
+ /**
168
+ * Window size as a bundle: `{ width, height }` in the Model, read from the
169
+ * window at startup and kept current by one `resize` listener. Breakpoints
170
+ * owns its own listener and derives names; place this one when views need
171
+ * the raw pixels. Placing both doubles resize listeners — pick the one the
172
+ * view reads.
173
+ */
153
174
  export declare const WindowSize: Bundle.Bundle<"WindowSize", void, {
154
175
  readonly width: number;
155
176
  readonly height: number;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/events/active-element.ts","../../src/events/hotkeys.ts","../../src/events/idle.ts","../../src/events/keyboard.ts","../../src/events/pointer.ts","../../src/events/scroll.ts","../../src/events/visibility.ts","../../src/events/window-size.ts"],"mappings":";;;;qBASa,gDAAoB;;;;;;YAGrB,8BAA8B,qBAAqB;;;;;;qBAOlD,2BAA0B,OAAO,OAAO;;;;;;;;;;;;;;;iBCNpC;WACN;WACA;WACA;WACA;WACA;WACA;;qBAkCE,cAAe,iBAAiB,OAAO;;;qBCvCvC,WAAS,OAAA;;;YACV,mBAAmB,UAAU;qBAE5B,uCAAW;;;;YACZ,qBAAqB,YAAY;qBAchC,MAAI,OAAA;;;;;;;;;;;qBCtBJ,2CAAe;;;;;;;;;;;;;YAWhB,yBAAyB,gBAAgB;;;;;;qBAOxC,iBACX;;;;;;WAMW,kBAAkB,OAAO;MAEnC,OAAO,OAAO;;;qBC5BJ,0CAAc;;;;;;YAGf,wBAAwB,eAAe;;;;;;qBAOtC,qBAAoB,OAAO,OAAO;;;qBCVlC,yCAAa;;;;;;YAGd,uBAAuB,cAAc;;;;;;;qBAQpC,oBAAmB,OAAO,OAAO;;;qBCTjC,iBAAe,OAAA;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;sBAAjB,OAAA;;;YACD,2BAA2B,kBAAkB;qBAiB5C,YAAU,OAAA;;;;;;;;qBCnBV,iBAAe,OAAA;;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;;;;;YAGlB,2BAA2B,kBAAkB;qBAmC5C,YAAU,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/events/active-element.ts","../../src/events/hotkeys.ts","../../src/events/idle.ts","../../src/events/keyboard.ts","../../src/events/pointer.ts","../../src/events/scroll.ts","../../src/events/visibility.ts","../../src/events/window-size.ts"],"mappings":";;;;qBASa,gDAAoB;;IACpB,KAAG,OAAA;IAAiB,IAAE,OAAA,OAAA,OAAA;;;YAEvB,8BAA8B,qBAAqB;;;;;;qBAOlD,2BAA0B,OAAO,OAAO;;;iBCnBpC;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;;;;;qBA8CE,cAAW,iBAAmB,OAAS;;;qBC9CvC,WAAS,OAAA;iBAAT,OAAA;;YACD,mBAAmB,UAAU;qBAE5B,uCAAW;;;;YACZ,qBAAqB,YAAY;;;;;;;;;qBAsBhC,MAAI,OAAA;;;;;;;;;;;qBCtBJ,2CAAe;;IAExB,KAAG,OAAA;IACH,QAAM,OAAA;IACN,MAAI,OAAA;IACJ,OAAK,OAAA;IACL,KAAG,OAAA;IACH,MAAI,OAAA;;;IAEM,KAAG,OAAA;;;YAEL,yBAAyB,gBAAgB;;;;;;qBAOxC,iBAAc;;;;;;WAOd,kBAAkB,OAAO;MAEnC,OAAO,OAAO;;;qBC5BJ,0CAAc;;IAChB,GAAC,OAAA;IAAiB,GAAC,OAAA;;;YAElB,wBAAwB,eAAe;;;;;;qBAOtC,qBAAoB,OAAO,OAAO;;;qBCVlC,yCAAa;;IACZ,GAAC,OAAA;IAAiB,GAAC,OAAA;;;YAErB,uBAAuB,cAAc;;;;;;;qBAQpC,oBAAmB,OAAO,OAAO;;;qBCfjC,iBAAe,OAAA;oBAAf,OAAA;;YACD,yBAAyB,gBAAgB;qBAExC,6CAAiB;;IAAmC,SAAO,OAAA;;;YAC5D,2BAA2B,kBAAkB;;;;;;;qBAuB5C,YAAU,OAAA;;;;;;;;qBC1BV,iBAAe,OAAA;;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;IACjB,OAAK,OAAA;IAAiB,QAAM,OAAA;;;YAE7B,2BAA2B,kBAAkB;;;;;;;;qBA0C5C,YAAU,OAAA"}
@@ -1,5 +1,5 @@
1
1
  import { i as unlessSame } from "../internal-DP-xLKbT.mjs";
2
- import { a as modalityOfKey, i as Modality, n as InputModalityMessage, r as InputModalityModel, t as InputModality } from "../input-modality-m6v7__Xs.mjs";
2
+ import { a as modalityOfKey, i as Modality, n as InputModalityMessage, r as InputModalityModel, t as InputModality } from "../input-modality-BXD383jg.mjs";
3
3
  import { Effect, Queue, Schema, Stream } from "effect";
4
4
  import { defineMessageUnion } from "foldkit/message";
5
5
  import * as Subscription from "foldkit/subscription";
@@ -17,7 +17,7 @@ const ActiveElementMessage = defineMessageUnion({ Changed: {
17
17
  } });
18
18
  /**
19
19
  * Focus changes on the document. Without a document the stream is empty
20
- * instead of throwing. Lift with `Subscription.persistent`, mapping into
20
+ * instead of throwing. Lift with `Subscription.persistentEntry`, mapping into
21
21
  * the parent's Message.
22
22
  */
23
23
  const activeElementEvents = () => {
@@ -55,6 +55,18 @@ const keyNameOf = (part) => {
55
55
  default: return part;
56
56
  }
57
57
  };
58
+ /**
59
+ * Hotkey matching as a pure function over a `Pressed` payload: `matchHotkey`
60
+ * answers whether a press is the shortcut, so `update` stays a table of
61
+ * chords instead of a nest of modifier checks. Patterns read like
62
+ * `"ctrl+shift+k"`: modifiers first (any order), the key last,
63
+ * case-insensitive, with `cmd`/`command` and `option` as the Mac names for
64
+ * meta and alt. Matching is exact — unlisted modifiers must be up — and
65
+ * auto-repeat never matches, so holding a chord fires once. `space` (or a
66
+ * bare space) means the space key; `esc` means escape. A pattern with
67
+ * no key, or two keys, matches nothing: predicates return false, they do
68
+ * not throw on the hot path.
69
+ */
58
70
  const matchHotkey = (pattern, press) => {
59
71
  if (press.repeat) return false;
60
72
  const trimmed = pattern.split("+").map((part) => part.trim().toLowerCase()).filter((part) => part !== "");
@@ -76,14 +88,6 @@ const matchHotkey = (pattern, press) => {
76
88
  };
77
89
  //#endregion
78
90
  //#region src/events/idle.ts
79
- /**
80
- * Idleness as a bundle: `idle` in the Model, flipped by activity and
81
- * silence. While active, activity debounced past `timeoutMs` settles to
82
- * `BecameIdle`; while idle, the first activity wakes to `BecameActive` and
83
- * the dependency flip restarts the watch. Starts active — a lurker idles
84
- * when the timeout elapses. The clock is Effect's, so tests drive it with
85
- * TestClock.
86
- */
87
91
  const IdleModel = Schema.Struct({ idle: Schema.Boolean });
88
92
  const IdleMessage = defineMessageUnion({
89
93
  BecameIdle: {},
@@ -99,6 +103,14 @@ const activity = () => {
99
103
  ];
100
104
  return Stream.mergeAll(senses, { concurrency: "unbounded" });
101
105
  };
106
+ /**
107
+ * Idleness as a bundle: `idle` in the Model, flipped by activity and
108
+ * silence. While active, activity debounced past `timeoutMs` settles to
109
+ * `BecameIdle`; while idle, the first activity wakes to `BecameActive` and
110
+ * the dependency flip restarts the watch. Starts active — a lurker idles
111
+ * when the timeout elapses. The clock is Effect's, so tests drive it with
112
+ * TestClock.
113
+ */
102
114
  const Idle = Bundle.make("Idle", {
103
115
  Model: IdleModel,
104
116
  Message: IdleMessage,
@@ -140,7 +152,7 @@ const KeyboardMessage = defineMessageUnion({
140
152
  });
141
153
  /**
142
154
  * Key presses and releases on the window. Without a window the stream is
143
- * empty instead of throwing. Lift with `Subscription.persistent`, mapping
155
+ * empty instead of throwing. Lift with `Subscription.persistentEntry`, mapping
144
156
  * into the parent's Message.
145
157
  */
146
158
  const keyboardEvents = (options = {}) => {
@@ -184,7 +196,7 @@ const PointerMessage = defineMessageUnion({ Moved: {
184
196
  } });
185
197
  /**
186
198
  * Pointer moves on the window. Without a window the stream is empty instead
187
- * of throwing. Lift with `Subscription.persistent`, mapping into the
199
+ * of throwing. Lift with `Subscription.persistentEntry`, mapping into the
188
200
  * parent's Message.
189
201
  */
190
202
  const pointerEvents = () => {
@@ -209,7 +221,7 @@ const ScrollMessage = defineMessageUnion({ Scrolled: {
209
221
  /**
210
222
  * Scrolls on the window, in the capture phase so container scrolls (which
211
223
  * do not bubble) report too. Without a window the stream is empty instead
212
- * of throwing. Lift with `Subscription.persistent`, mapping into the
224
+ * of throwing. Lift with `Subscription.persistentEntry`, mapping into the
213
225
  * parent's Message.
214
226
  */
215
227
  const scrollEvents = () => {
@@ -221,12 +233,6 @@ const scrollEvents = () => {
221
233
  };
222
234
  //#endregion
223
235
  //#region src/events/visibility.ts
224
- /**
225
- * Page visibility as a bundle: `visible` in the Model, read from the
226
- * document at startup and kept current by `visibilitychange`. A hidden page
227
- * is the application's cue to pause polling, animations, and realtime
228
- * streams; the decision stays in application `update`, not here.
229
- */
230
236
  const VisibilityModel = Schema.Struct({ visible: Schema.Boolean });
231
237
  const VisibilityMessage = defineMessageUnion({ Changed: { visible: Schema.Boolean } });
232
238
  const isVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
@@ -234,22 +240,21 @@ const visibilityStream = () => {
234
240
  if (typeof document === "undefined" || typeof document.addEventListener !== "function") return Stream.empty;
235
241
  return Stream.concat(Stream.make(VisibilityMessage.Changed({ visible: isVisible() })), Stream.fromEventListener(document, "visibilitychange").pipe(Stream.map(() => VisibilityMessage.Changed({ visible: isVisible() }))));
236
242
  };
243
+ /**
244
+ * Page visibility as a bundle: `visible` in the Model, read from the
245
+ * document at startup and kept current by `visibilitychange`. A hidden page
246
+ * is the application's cue to pause polling, animations, and realtime
247
+ * streams; the decision stays in application `update`, not here.
248
+ */
237
249
  const Visibility = Bundle.make("Visibility", {
238
250
  Model: VisibilityModel,
239
251
  Message: VisibilityMessage,
240
252
  init: () => ({ model: { visible: isVisible() } }),
241
253
  update: (model, message) => message.visible === model.visible ? { model } : { model: { visible: message.visible } },
242
- subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(visibilityStream()) }))
254
+ subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistentEntry(visibilityStream()) }))
243
255
  });
244
256
  //#endregion
245
257
  //#region src/events/window-size.ts
246
- /**
247
- * Window size as a bundle: `{ width, height }` in the Model, read from the
248
- * window at startup and kept current by one `resize` listener. Breakpoints
249
- * owns its own listener and derives names; place this one when views need
250
- * the raw pixels. Placing both doubles resize listeners — pick the one the
251
- * view reads.
252
- */
253
258
  const WindowSizeModel = Schema.Struct({
254
259
  width: Schema.Number,
255
260
  height: Schema.Number
@@ -284,6 +289,13 @@ const sizeStream = () => {
284
289
  })), Stream.fromQueue(queue));
285
290
  }));
286
291
  };
292
+ /**
293
+ * Window size as a bundle: `{ width, height }` in the Model, read from the
294
+ * window at startup and kept current by one `resize` listener. Breakpoints
295
+ * owns its own listener and derives names; place this one when views need
296
+ * the raw pixels. Placing both doubles resize listeners — pick the one the
297
+ * view reads.
298
+ */
287
299
  const WindowSize = Bundle.make("WindowSize", {
288
300
  Model: WindowSizeModel,
289
301
  Message: WindowSizeMessage,
@@ -295,7 +307,7 @@ const WindowSize = Bundle.make("WindowSize", {
295
307
  width: message.width,
296
308
  height: message.height
297
309
  }) }),
298
- subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(sizeStream()) }))
310
+ subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistentEntry(sizeStream()) }))
299
311
  });
300
312
  //#endregion
301
313
  export { ActiveElementMessage, Idle, IdleMessage, IdleModel, InputModality, InputModalityMessage, InputModalityModel, KeyboardMessage, Modality, PointerMessage, ScrollMessage, Visibility, VisibilityMessage, VisibilityModel, WindowSize, WindowSizeMessage, WindowSizeModel, activeElementEvents, keyboardEvents, matchHotkey, modalityOfKey, pointerEvents, scrollEvents };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/events/active-element.ts","../../src/events/hotkeys.ts","../../src/events/idle.ts","../../src/events/keyboard.ts","../../src/events/pointer.ts","../../src/events/scroll.ts","../../src/events/visibility.ts","../../src/events/window-size.ts"],"sourcesContent":["/**\n * Focused element as an entry — not a bundle, because elements are not\n * Model facts: this reports the active element's tag and id (both\n * serializable), and the parent stores what matters. Starts with the\n * current answer, then follows `focusin`/`focusout`.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ActiveElementMessage = defineMessageUnion({\n Changed: { tag: Schema.String, id: Schema.NullOr(Schema.String) },\n})\nexport type ActiveElementMessage = typeof ActiveElementMessage.Type\n\n/**\n * Focus changes on the document. Without a document the stream is empty\n * instead of throwing. Lift with `Subscription.persistent`, mapping into\n * the parent's Message.\n */\nexport const activeElementEvents = (): Stream.Stream<ActiveElementMessage> => {\n if (typeof document === 'undefined') return Stream.empty\n const read = (): ActiveElementMessage => {\n const element = document.activeElement as Element | null | undefined\n return ActiveElementMessage.Changed({\n tag: element?.tagName ?? '',\n id: element?.id !== undefined && element.id !== '' ? element.id : null,\n })\n }\n const changes: Stream.Stream<ActiveElementMessage> = Stream.mergeAll(\n [\n Stream.fromEventListener(document, 'focusin').pipe(Stream.map(read)),\n Stream.fromEventListener(document, 'focusout').pipe(Stream.map(read)),\n ],\n { concurrency: 'unbounded' },\n )\n return Stream.concat(Stream.make(read()), changes)\n}\n","/**\n * Hotkey matching as a pure function over a `Pressed` payload: `matchHotkey`\n * answers whether a press is the shortcut, so `update` stays a table of\n * chords instead of a nest of modifier checks. Patterns read like\n * `\"ctrl+shift+k\"`: modifiers first (any order), the key last,\n * case-insensitive, with `cmd`/`command` and `option` as the Mac names for\n * meta and alt. Matching is exact — unlisted modifiers must be up — and\n * auto-repeat never matches, so holding a chord fires once. `space` (or a\n * bare space) means the space key; `esc` means escape. A pattern with\n * no key, or two keys, matches nothing: predicates return false, they do\n * not throw on the hot path.\n */\n\nexport interface KeyPress {\n readonly key: string\n readonly repeat: boolean\n readonly ctrl: boolean\n readonly shift: boolean\n readonly alt: boolean\n readonly meta: boolean\n}\n\nconst modifierOf = (part: string): 'ctrl' | 'shift' | 'alt' | 'meta' | null => {\n switch (part) {\n case 'ctrl':\n case 'control':\n return 'ctrl'\n case 'shift':\n return 'shift'\n case 'alt':\n case 'option':\n return 'alt'\n case 'meta':\n case 'cmd':\n case 'command':\n return 'meta'\n default:\n return null\n }\n}\n\nconst keyNameOf = (part: string): string => {\n switch (part) {\n case 'spacebar':\n case ' ':\n return 'space'\n case 'esc':\n return 'escape'\n default:\n return part\n }\n}\n\nexport const matchHotkey = (pattern: string, press: KeyPress): boolean => {\n if (press.repeat) return false\n const trimmed = pattern\n .split('+')\n .map(part => part.trim().toLowerCase())\n .filter(part => part !== '')\n // A bare whitespace pattern means the space key; an empty pattern is malformed.\n const parts = trimmed.length === 0 && pattern.length > 0 ? ['space'] : trimmed\n const keys = parts.filter(part => modifierOf(part) === null)\n if (keys.length !== 1) return false\n if (keyNameOf(keys[0]!) !== keyNameOf(press.key.toLowerCase())) return false\n const required: Record<'ctrl' | 'shift' | 'alt' | 'meta', boolean> = {\n ctrl: false,\n shift: false,\n alt: false,\n meta: false,\n }\n for (const part of parts) {\n const modifier = modifierOf(part)\n if (modifier !== null) required[modifier] = true\n }\n return (\n required.ctrl === press.ctrl &&\n required.shift === press.shift &&\n required.alt === press.alt &&\n required.meta === press.meta\n )\n}\n","/**\n * Idleness as a bundle: `idle` in the Model, flipped by activity and\n * silence. While active, activity debounced past `timeoutMs` settles to\n * `BecameIdle`; while idle, the first activity wakes to `BecameActive` and\n * the dependency flip restarts the watch. Starts active — a lurker idles\n * when the timeout elapses. The clock is Effect's, so tests drive it with\n * TestClock.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const IdleModel = Schema.Struct({ idle: Schema.Boolean })\nexport type IdleModel = typeof IdleModel.Type\n\nexport const IdleMessage = defineMessageUnion({ BecameIdle: {}, BecameActive: {} })\nexport type IdleMessage = typeof IdleMessage.Type\n\nconst activity = (): Stream.Stream<unknown> => {\n if (typeof window === 'undefined') return Stream.empty\n // One stream per sense; the merge fans into the debounce below.\n const senses: Array<Stream.Stream<unknown>> = [\n Stream.fromEventListener(window, 'mousemove'),\n Stream.fromEventListener(window, 'keydown'),\n Stream.fromEventListener(window, 'pointerdown'),\n Stream.fromEventListener(window, 'scroll'),\n ]\n return Stream.mergeAll(senses, { concurrency: 'unbounded' })\n}\n\nexport const Idle = Bundle.make('Idle', {\n Model: IdleModel,\n Message: IdleMessage,\n args: Schema.Struct({\n timeoutMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { idle: false } }),\n update: (model, message) =>\n IdleMessage.match<Update.ReturnWithOutMessage<IdleModel, IdleMessage, never>>(message, {\n // One already in flight when the watch restarts can repeat the state.\n BecameIdle: () => (model.idle ? { model } : { model: { ...model, idle: true } }),\n BecameActive: () => (model.idle ? { model: { ...model, idle: false } } : { model }),\n }),\n subscriptions: ({ timeoutMs }): Subscription.Subscriptions<IdleModel, IdleMessage> =>\n Subscription.make<IdleModel, IdleMessage>()(entry => ({\n watch: entry(\n { idle: Schema.Boolean },\n {\n modelToDependencies: model => ({ idle: model.idle }),\n dependenciesToStream: ({ idle }) =>\n idle\n ? activity().pipe(\n Stream.take(1),\n Stream.map(() => IdleMessage.BecameActive()),\n )\n : // Seeded with subscribe time as last-known-alive: silence from\n // here still settles, so a lurker idles without moving first.\n Stream.concat(Stream.make(null), activity()).pipe(\n Stream.debounce(timeoutMs),\n Stream.map(() => IdleMessage.BecameIdle()),\n ),\n },\n ),\n })),\n})\n","/**\n * Keyboard as an entry — not a bundle, because the parent owns whatever key\n * state it keeps (a held-keys set, a shortcut map, a focused field): this\n * reports presses and releases, including auto-repeat, and the parent\n * stores what matters. Hotkey matching stays application policy.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { KeyPress } from './hotkeys.js'\n\nexport const KeyboardMessage = defineMessageUnion({\n Pressed: {\n key: Schema.String,\n repeat: Schema.Boolean,\n ctrl: Schema.Boolean,\n shift: Schema.Boolean,\n alt: Schema.Boolean,\n meta: Schema.Boolean,\n },\n Released: { key: Schema.String },\n})\nexport type KeyboardMessage = typeof KeyboardMessage.Type\n\n/**\n * Key presses and releases on the window. Without a window the stream is\n * empty instead of throwing. Lift with `Subscription.persistent`, mapping\n * into the parent's Message.\n */\nexport const keyboardEvents = (\n options: {\n /**\n * Whether to cancel a press's default (the page scrolling under an arrow\n * key, the browser's own shortcut), decided in the listener, since a\n * default can only be cancelled before the event finishes dispatching.\n */\n readonly preventDefault?: (press: KeyPress) => boolean\n } = {},\n): Stream.Stream<KeyboardMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n return Stream.callback<KeyboardMessage>(queue =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const down = (event: KeyboardEvent) => {\n const press = KeyboardMessage.Pressed({\n key: event.key,\n repeat: event.repeat,\n ctrl: event.ctrlKey,\n shift: event.shiftKey,\n alt: event.altKey,\n meta: event.metaKey,\n })\n if (options.preventDefault?.(press) === true) event.preventDefault()\n Queue.offerUnsafe(queue, press)\n }\n const up = (event: KeyboardEvent) =>\n Queue.offerUnsafe(queue, KeyboardMessage.Released({ key: event.key }))\n window.addEventListener('keydown', down)\n window.addEventListener('keyup', up)\n return { down, up }\n }),\n ({ down, up }) =>\n Effect.sync(() => {\n window.removeEventListener('keydown', down)\n window.removeEventListener('keyup', up)\n }),\n ),\n )\n}\n","/**\n * Pointer position as an entry — not a bundle, because the parent owns what\n * it keeps (a cursor, a drag anchor, hover state): this reports moves, and\n * the parent stores what matters. Element-scoped pointer needs are Mounts,\n * not this window-level entry.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const PointerMessage = defineMessageUnion({\n Moved: { x: Schema.Number, y: Schema.Number },\n})\nexport type PointerMessage = typeof PointerMessage.Type\n\n/**\n * Pointer moves on the window. Without a window the stream is empty instead\n * of throwing. Lift with `Subscription.persistent`, mapping into the\n * parent's Message.\n */\nexport const pointerEvents = (): Stream.Stream<PointerMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n return Stream.fromEventListener<PointerEvent>(window, 'pointermove').pipe(\n Stream.map(event => PointerMessage.Moved({ x: event.clientX, y: event.clientY })),\n )\n}\n","/**\n * Window scroll position as an entry — not a bundle, because the parent\n * owns what it keeps (a stuck header, a back-to-top gate, restore-on-back):\n * this reports positions, and the parent stores what matters.\n * Element-scoped scrolling needs a Mount, not this window-level entry.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ScrollMessage = defineMessageUnion({\n Scrolled: { x: Schema.Number, y: Schema.Number },\n})\nexport type ScrollMessage = typeof ScrollMessage.Type\n\n/**\n * Scrolls on the window, in the capture phase so container scrolls (which\n * do not bubble) report too. Without a window the stream is empty instead\n * of throwing. Lift with `Subscription.persistent`, mapping into the\n * parent's Message.\n */\nexport const scrollEvents = (): Stream.Stream<ScrollMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n return Stream.fromEventListener(window, 'scroll', { capture: true }).pipe(\n Stream.map(() => ScrollMessage.Scrolled({ x: window.scrollX, y: window.scrollY })),\n )\n}\n","/**\n * Page visibility as a bundle: `visible` in the Model, read from the\n * document at startup and kept current by `visibilitychange`. A hidden page\n * is the application's cue to pause polling, animations, and realtime\n * streams; the decision stays in application `update`, not here.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const VisibilityModel = Schema.Struct({ visible: Schema.Boolean })\nexport type VisibilityModel = typeof VisibilityModel.Type\n\nexport const VisibilityMessage = defineMessageUnion({ Changed: { visible: Schema.Boolean } })\nexport type VisibilityMessage = typeof VisibilityMessage.Type\n\nconst isVisible = (): boolean =>\n typeof document === 'undefined' || document.visibilityState !== 'hidden'\n\nconst visibilityStream = (): Stream.Stream<VisibilityMessage> => {\n if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') {\n return Stream.empty\n }\n return Stream.concat(\n Stream.make(VisibilityMessage.Changed({ visible: isVisible() })),\n Stream.fromEventListener(document, 'visibilitychange').pipe(\n Stream.map(() => VisibilityMessage.Changed({ visible: isVisible() })),\n ),\n )\n}\n\nexport const Visibility = Bundle.make('Visibility', {\n Model: VisibilityModel,\n Message: VisibilityMessage,\n // No document (SSR): assume visible; the stream corrects on subscribe.\n init: () => ({ model: { visible: isVisible() } }),\n // The stream opens with the state `init` already read.\n update: (model, message) =>\n message.visible === model.visible ? { model } : { model: { visible: message.visible } },\n subscriptions: (): Subscription.Subscriptions<VisibilityModel, VisibilityMessage> =>\n Subscription.make<VisibilityModel, VisibilityMessage>()(() => ({\n changes: Subscription.persistent(visibilityStream()),\n })),\n})\n","/**\n * Window size as a bundle: `{ width, height }` in the Model, read from the\n * window at startup and kept current by one `resize` listener. Breakpoints\n * owns its own listener and derives names; place this one when views need\n * the raw pixels. Placing both doubles resize listeners — pick the one the\n * view reads.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const WindowSizeModel = Schema.Struct({ width: Schema.Number, height: Schema.Number })\nexport type WindowSizeModel = typeof WindowSizeModel.Type\n\nexport const WindowSizeMessage = defineMessageUnion({\n Changed: { width: Schema.Number, height: Schema.Number },\n})\nexport type WindowSizeMessage = typeof WindowSizeMessage.Type\n\nconst readSize = (): { readonly width: number; readonly height: number } => ({\n width: window.innerWidth,\n height: window.innerHeight,\n})\n\nconst sizeStream = (): Stream.Stream<WindowSizeMessage> => {\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {\n return Stream.empty\n }\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<WindowSizeMessage>()\n const onResize = () => {\n const size = readSize()\n Effect.runFork(\n Queue.offer(queue, WindowSizeMessage.Changed({ width: size.width, height: size.height })),\n )\n }\n window.addEventListener('resize', onResize)\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n window.removeEventListener('resize', onResize)\n }),\n )\n const size = readSize()\n return Stream.concat(\n Stream.make(WindowSizeMessage.Changed({ width: size.width, height: size.height })),\n Stream.fromQueue(queue),\n )\n }),\n )\n}\n\nexport const WindowSize = Bundle.make('WindowSize', {\n Model: WindowSizeModel,\n Message: WindowSizeMessage,\n init: () => ({ model: { width: 0, height: 0 } }),\n update: (model, message) => ({\n model: unlessSame(model, { width: message.width, height: message.height }),\n }),\n subscriptions: (): Subscription.Subscriptions<WindowSizeModel, WindowSizeMessage> =>\n Subscription.make<WindowSizeModel, WindowSizeMessage>()(() => ({\n changes: Subscription.persistent(sizeStream()),\n })),\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/events/active-element.ts","../../src/events/hotkeys.ts","../../src/events/idle.ts","../../src/events/keyboard.ts","../../src/events/pointer.ts","../../src/events/scroll.ts","../../src/events/visibility.ts","../../src/events/window-size.ts"],"sourcesContent":["/**\n * Focused element as an entry — not a bundle, because elements are not\n * Model facts: this reports the active element's tag and id (both\n * serializable), and the parent stores what matters. Starts with the\n * current answer, then follows `focusin`/`focusout`.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ActiveElementMessage = defineMessageUnion({\n Changed: { tag: Schema.String, id: Schema.NullOr(Schema.String) },\n})\nexport type ActiveElementMessage = typeof ActiveElementMessage.Type\n\n/**\n * Focus changes on the document. Without a document the stream is empty\n * instead of throwing. Lift with `Subscription.persistentEntry`, mapping into\n * the parent's Message.\n */\nexport const activeElementEvents = (): Stream.Stream<ActiveElementMessage> => {\n if (typeof document === 'undefined') return Stream.empty\n const read = (): ActiveElementMessage => {\n const element = document.activeElement as Element | null | undefined\n return ActiveElementMessage.Changed({\n tag: element?.tagName ?? '',\n id: element?.id !== undefined && element.id !== '' ? element.id : null,\n })\n }\n const changes: Stream.Stream<ActiveElementMessage> = Stream.mergeAll(\n [\n Stream.fromEventListener(document, 'focusin').pipe(Stream.map(read)),\n Stream.fromEventListener(document, 'focusout').pipe(Stream.map(read)),\n ],\n { concurrency: 'unbounded' },\n )\n return Stream.concat(Stream.make(read()), changes)\n}\n","export interface KeyPress {\n readonly key: string\n readonly repeat: boolean\n readonly ctrl: boolean\n readonly shift: boolean\n readonly alt: boolean\n readonly meta: boolean\n}\n\nconst modifierOf = (part: string): 'ctrl' | 'shift' | 'alt' | 'meta' | null => {\n switch (part) {\n case 'ctrl':\n case 'control':\n return 'ctrl'\n case 'shift':\n return 'shift'\n case 'alt':\n case 'option':\n return 'alt'\n case 'meta':\n case 'cmd':\n case 'command':\n return 'meta'\n default:\n return null\n }\n}\n\nconst keyNameOf = (part: string): string => {\n switch (part) {\n case 'spacebar':\n case ' ':\n return 'space'\n case 'esc':\n return 'escape'\n default:\n return part\n }\n}\n\n/**\n * Hotkey matching as a pure function over a `Pressed` payload: `matchHotkey`\n * answers whether a press is the shortcut, so `update` stays a table of\n * chords instead of a nest of modifier checks. Patterns read like\n * `\"ctrl+shift+k\"`: modifiers first (any order), the key last,\n * case-insensitive, with `cmd`/`command` and `option` as the Mac names for\n * meta and alt. Matching is exact — unlisted modifiers must be up — and\n * auto-repeat never matches, so holding a chord fires once. `space` (or a\n * bare space) means the space key; `esc` means escape. A pattern with\n * no key, or two keys, matches nothing: predicates return false, they do\n * not throw on the hot path.\n */\nexport const matchHotkey = (pattern: string, press: KeyPress): boolean => {\n if (press.repeat) return false\n const trimmed = pattern\n .split('+')\n .map(part => part.trim().toLowerCase())\n .filter(part => part !== '')\n // A bare whitespace pattern means the space key; an empty pattern is malformed.\n const parts = trimmed.length === 0 && pattern.length > 0 ? ['space'] : trimmed\n const keys = parts.filter(part => modifierOf(part) === null)\n if (keys.length !== 1) return false\n if (keyNameOf(keys[0]!) !== keyNameOf(press.key.toLowerCase())) return false\n const required: Record<'ctrl' | 'shift' | 'alt' | 'meta', boolean> = {\n ctrl: false,\n shift: false,\n alt: false,\n meta: false,\n }\n for (const part of parts) {\n const modifier = modifierOf(part)\n if (modifier !== null) required[modifier] = true\n }\n return (\n required.ctrl === press.ctrl &&\n required.shift === press.shift &&\n required.alt === press.alt &&\n required.meta === press.meta\n )\n}\n","import { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const IdleModel = Schema.Struct({ idle: Schema.Boolean })\nexport type IdleModel = typeof IdleModel.Type\n\nexport const IdleMessage = defineMessageUnion({ BecameIdle: {}, BecameActive: {} })\nexport type IdleMessage = typeof IdleMessage.Type\n\nconst activity = (): Stream.Stream<unknown> => {\n if (typeof window === 'undefined') return Stream.empty\n // One stream per sense; the merge fans into the debounce below.\n const senses: Array<Stream.Stream<unknown>> = [\n Stream.fromEventListener(window, 'mousemove'),\n Stream.fromEventListener(window, 'keydown'),\n Stream.fromEventListener(window, 'pointerdown'),\n Stream.fromEventListener(window, 'scroll'),\n ]\n return Stream.mergeAll(senses, { concurrency: 'unbounded' })\n}\n\n/**\n * Idleness as a bundle: `idle` in the Model, flipped by activity and\n * silence. While active, activity debounced past `timeoutMs` settles to\n * `BecameIdle`; while idle, the first activity wakes to `BecameActive` and\n * the dependency flip restarts the watch. Starts active — a lurker idles\n * when the timeout elapses. The clock is Effect's, so tests drive it with\n * TestClock.\n */\nexport const Idle = Bundle.make('Idle', {\n Model: IdleModel,\n Message: IdleMessage,\n args: Schema.Struct({\n timeoutMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { idle: false } }),\n update: (model, message) =>\n IdleMessage.match<Update.ReturnWithOutMessage<IdleModel, IdleMessage, never>>(message, {\n // One already in flight when the watch restarts can repeat the state.\n BecameIdle: () => (model.idle ? { model } : { model: { ...model, idle: true } }),\n BecameActive: () => (model.idle ? { model: { ...model, idle: false } } : { model }),\n }),\n subscriptions: ({ timeoutMs }): Subscription.Subscriptions<IdleModel, IdleMessage> =>\n Subscription.make<IdleModel, IdleMessage>()(entry => ({\n watch: entry(\n { idle: Schema.Boolean },\n {\n modelToDependencies: model => ({ idle: model.idle }),\n dependenciesToStream: ({ idle }) =>\n idle\n ? activity().pipe(\n Stream.take(1),\n Stream.map(() => IdleMessage.BecameActive()),\n )\n : // Seeded with subscribe time as last-known-alive: silence from\n // here still settles, so a lurker idles without moving first.\n Stream.concat(Stream.make(null), activity()).pipe(\n Stream.debounce(timeoutMs),\n Stream.map(() => IdleMessage.BecameIdle()),\n ),\n },\n ),\n })),\n})\n","/**\n * Keyboard as an entry — not a bundle, because the parent owns whatever key\n * state it keeps (a held-keys set, a shortcut map, a focused field): this\n * reports presses and releases, including auto-repeat, and the parent\n * stores what matters. Hotkey matching stays application policy.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { KeyPress } from './hotkeys.js'\n\nexport const KeyboardMessage = defineMessageUnion({\n Pressed: {\n key: Schema.String,\n repeat: Schema.Boolean,\n ctrl: Schema.Boolean,\n shift: Schema.Boolean,\n alt: Schema.Boolean,\n meta: Schema.Boolean,\n },\n Released: { key: Schema.String },\n})\nexport type KeyboardMessage = typeof KeyboardMessage.Type\n\n/**\n * Key presses and releases on the window. Without a window the stream is\n * empty instead of throwing. Lift with `Subscription.persistentEntry`, mapping\n * into the parent's Message.\n */\nexport const keyboardEvents = (\n options: {\n /**\n * Whether to cancel a press's default (the page scrolling under an arrow\n * key, the browser's own shortcut), decided in the listener, since a\n * default can only be cancelled before the event finishes dispatching.\n */\n readonly preventDefault?: (press: KeyPress) => boolean\n } = {},\n): Stream.Stream<KeyboardMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n return Stream.callback<KeyboardMessage>(queue =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const down = (event: KeyboardEvent) => {\n const press = KeyboardMessage.Pressed({\n key: event.key,\n repeat: event.repeat,\n ctrl: event.ctrlKey,\n shift: event.shiftKey,\n alt: event.altKey,\n meta: event.metaKey,\n })\n if (options.preventDefault?.(press) === true) event.preventDefault()\n Queue.offerUnsafe(queue, press)\n }\n const up = (event: KeyboardEvent) =>\n Queue.offerUnsafe(queue, KeyboardMessage.Released({ key: event.key }))\n window.addEventListener('keydown', down)\n window.addEventListener('keyup', up)\n return { down, up }\n }),\n ({ down, up }) =>\n Effect.sync(() => {\n window.removeEventListener('keydown', down)\n window.removeEventListener('keyup', up)\n }),\n ),\n )\n}\n","/**\n * Pointer position as an entry — not a bundle, because the parent owns what\n * it keeps (a cursor, a drag anchor, hover state): this reports moves, and\n * the parent stores what matters. Element-scoped pointer needs are Mounts,\n * not this window-level entry.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const PointerMessage = defineMessageUnion({\n Moved: { x: Schema.Number, y: Schema.Number },\n})\nexport type PointerMessage = typeof PointerMessage.Type\n\n/**\n * Pointer moves on the window. Without a window the stream is empty instead\n * of throwing. Lift with `Subscription.persistentEntry`, mapping into the\n * parent's Message.\n */\nexport const pointerEvents = (): Stream.Stream<PointerMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n return Stream.fromEventListener<PointerEvent>(window, 'pointermove').pipe(\n Stream.map(event => PointerMessage.Moved({ x: event.clientX, y: event.clientY })),\n )\n}\n","/**\n * Window scroll position as an entry — not a bundle, because the parent\n * owns what it keeps (a stuck header, a back-to-top gate, restore-on-back):\n * this reports positions, and the parent stores what matters.\n * Element-scoped scrolling needs a Mount, not this window-level entry.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ScrollMessage = defineMessageUnion({\n Scrolled: { x: Schema.Number, y: Schema.Number },\n})\nexport type ScrollMessage = typeof ScrollMessage.Type\n\n/**\n * Scrolls on the window, in the capture phase so container scrolls (which\n * do not bubble) report too. Without a window the stream is empty instead\n * of throwing. Lift with `Subscription.persistentEntry`, mapping into the\n * parent's Message.\n */\nexport const scrollEvents = (): Stream.Stream<ScrollMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n return Stream.fromEventListener(window, 'scroll', { capture: true }).pipe(\n Stream.map(() => ScrollMessage.Scrolled({ x: window.scrollX, y: window.scrollY })),\n )\n}\n","import { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const VisibilityModel = Schema.Struct({ visible: Schema.Boolean })\nexport type VisibilityModel = typeof VisibilityModel.Type\n\nexport const VisibilityMessage = defineMessageUnion({ Changed: { visible: Schema.Boolean } })\nexport type VisibilityMessage = typeof VisibilityMessage.Type\n\nconst isVisible = (): boolean =>\n typeof document === 'undefined' || document.visibilityState !== 'hidden'\n\nconst visibilityStream = (): Stream.Stream<VisibilityMessage> => {\n if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') {\n return Stream.empty\n }\n return Stream.concat(\n Stream.make(VisibilityMessage.Changed({ visible: isVisible() })),\n Stream.fromEventListener(document, 'visibilitychange').pipe(\n Stream.map(() => VisibilityMessage.Changed({ visible: isVisible() })),\n ),\n )\n}\n\n/**\n * Page visibility as a bundle: `visible` in the Model, read from the\n * document at startup and kept current by `visibilitychange`. A hidden page\n * is the application's cue to pause polling, animations, and realtime\n * streams; the decision stays in application `update`, not here.\n */\nexport const Visibility = Bundle.make('Visibility', {\n Model: VisibilityModel,\n Message: VisibilityMessage,\n // No document (SSR): assume visible; the stream corrects on subscribe.\n init: () => ({ model: { visible: isVisible() } }),\n // The stream opens with the state `init` already read.\n update: (model, message) =>\n message.visible === model.visible ? { model } : { model: { visible: message.visible } },\n subscriptions: (): Subscription.Subscriptions<VisibilityModel, VisibilityMessage> =>\n Subscription.make<VisibilityModel, VisibilityMessage>()(() => ({\n changes: Subscription.persistentEntry(visibilityStream()),\n })),\n})\n","import { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\nexport const WindowSizeModel = Schema.Struct({ width: Schema.Number, height: Schema.Number })\nexport type WindowSizeModel = typeof WindowSizeModel.Type\n\nexport const WindowSizeMessage = defineMessageUnion({\n Changed: { width: Schema.Number, height: Schema.Number },\n})\nexport type WindowSizeMessage = typeof WindowSizeMessage.Type\n\nconst readSize = (): { readonly width: number; readonly height: number } => ({\n width: window.innerWidth,\n height: window.innerHeight,\n})\n\nconst sizeStream = (): Stream.Stream<WindowSizeMessage> => {\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {\n return Stream.empty\n }\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<WindowSizeMessage>()\n const onResize = () => {\n const size = readSize()\n Effect.runFork(\n Queue.offer(queue, WindowSizeMessage.Changed({ width: size.width, height: size.height })),\n )\n }\n window.addEventListener('resize', onResize)\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n window.removeEventListener('resize', onResize)\n }),\n )\n const size = readSize()\n return Stream.concat(\n Stream.make(WindowSizeMessage.Changed({ width: size.width, height: size.height })),\n Stream.fromQueue(queue),\n )\n }),\n )\n}\n\n/**\n * Window size as a bundle: `{ width, height }` in the Model, read from the\n * window at startup and kept current by one `resize` listener. Breakpoints\n * owns its own listener and derives names; place this one when views need\n * the raw pixels. Placing both doubles resize listeners — pick the one the\n * view reads.\n */\nexport const WindowSize = Bundle.make('WindowSize', {\n Model: WindowSizeModel,\n Message: WindowSizeMessage,\n init: () => ({ model: { width: 0, height: 0 } }),\n update: (model, message) => ({\n model: unlessSame(model, { width: message.width, height: message.height }),\n }),\n subscriptions: (): Subscription.Subscriptions<WindowSizeModel, WindowSizeMessage> =>\n Subscription.make<WindowSizeModel, WindowSizeMessage>()(() => ({\n changes: Subscription.persistentEntry(sizeStream()),\n })),\n})\n"],"mappings":"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"}