foldkit-primitives 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/device/index.d.mts +312 -0
- package/dist/device/index.d.mts.map +1 -0
- package/dist/device/index.mjs +490 -0
- package/dist/device/index.mjs.map +1 -0
- package/dist/dom/index.d.mts +73 -0
- package/dist/dom/index.d.mts.map +1 -0
- package/dist/dom/index.mjs +218 -0
- package/dist/dom/index.mjs.map +1 -0
- package/dist/events/index.d.mts +154 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +287 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +10 -0
- package/dist/media/index.d.mts +79 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +127 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/motion/index.d.mts +112 -0
- package/dist/motion/index.d.mts.map +1 -0
- package/dist/motion/index.mjs +208 -0
- package/dist/motion/index.mjs.map +1 -0
- package/dist/net/index.d.mts +199 -0
- package/dist/net/index.d.mts.map +1 -0
- package/dist/net/index.mjs +329 -0
- package/dist/net/index.mjs.map +1 -0
- package/dist/observers/index.d.mts +59 -0
- package/dist/observers/index.d.mts.map +1 -0
- package/dist/observers/index.mjs +156 -0
- package/dist/observers/index.mjs.map +1 -0
- package/dist/state/index.d.mts +342 -0
- package/dist/state/index.d.mts.map +1 -0
- package/dist/state/index.mjs +504 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/time/index.d.mts +129 -0
- package/dist/time/index.d.mts.map +1 -0
- package/dist/time/index.mjs +213 -0
- package/dist/time/index.mjs.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import * as Subscription from "foldkit/subscription";
|
|
4
|
+
import { Bundle } from "foldkit-bundle";
|
|
5
|
+
//#region src/events/active-element.ts
|
|
6
|
+
/**
|
|
7
|
+
* Focused element as an entry — not a bundle, because elements are not
|
|
8
|
+
* Model facts: this reports the active element's tag and id (both
|
|
9
|
+
* serializable), and the parent stores what matters. Starts with the
|
|
10
|
+
* current answer, then follows `focusin`/`focusout`.
|
|
11
|
+
*/
|
|
12
|
+
const ActiveElementMessage = defineMessageUnion({ Changed: {
|
|
13
|
+
tag: Schema.String,
|
|
14
|
+
id: Schema.NullOr(Schema.String)
|
|
15
|
+
} });
|
|
16
|
+
/**
|
|
17
|
+
* Focus changes on the document. Without a document the stream is empty
|
|
18
|
+
* instead of throwing. Lift with `Subscription.persistent`, mapping into
|
|
19
|
+
* the parent's Message.
|
|
20
|
+
*/
|
|
21
|
+
const activeElementEvents = () => {
|
|
22
|
+
if (typeof document === "undefined") return Stream.empty;
|
|
23
|
+
const read = () => {
|
|
24
|
+
const element = document.activeElement;
|
|
25
|
+
return ActiveElementMessage.Changed({
|
|
26
|
+
tag: element?.tagName ?? "",
|
|
27
|
+
id: element?.id !== void 0 && element.id !== "" ? element.id : null
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const changes = Stream.mergeAll([Stream.fromEventListener(document, "focusin").pipe(Stream.map(read)), Stream.fromEventListener(document, "focusout").pipe(Stream.map(read))], { concurrency: "unbounded" });
|
|
31
|
+
return Stream.concat(Stream.make(read()), changes);
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/events/hotkeys.ts
|
|
35
|
+
const modifierOf = (part) => {
|
|
36
|
+
switch (part) {
|
|
37
|
+
case "ctrl":
|
|
38
|
+
case "control": return "ctrl";
|
|
39
|
+
case "shift": return "shift";
|
|
40
|
+
case "alt":
|
|
41
|
+
case "option": return "alt";
|
|
42
|
+
case "meta":
|
|
43
|
+
case "cmd":
|
|
44
|
+
case "command": return "meta";
|
|
45
|
+
default: return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const keyNameOf = (part) => {
|
|
49
|
+
switch (part) {
|
|
50
|
+
case "spacebar":
|
|
51
|
+
case " ": return "space";
|
|
52
|
+
case "esc": return "escape";
|
|
53
|
+
default: return part;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const matchHotkey = (pattern, press) => {
|
|
57
|
+
if (press.repeat) return false;
|
|
58
|
+
const trimmed = pattern.split("+").map((part) => part.trim().toLowerCase()).filter((part) => part !== "");
|
|
59
|
+
const parts = trimmed.length === 0 && pattern.length > 0 ? ["space"] : trimmed;
|
|
60
|
+
const keys = parts.filter((part) => modifierOf(part) === null);
|
|
61
|
+
if (keys.length !== 1) return false;
|
|
62
|
+
if (keyNameOf(keys[0]) !== keyNameOf(press.key.toLowerCase())) return false;
|
|
63
|
+
const required = {
|
|
64
|
+
ctrl: false,
|
|
65
|
+
shift: false,
|
|
66
|
+
alt: false,
|
|
67
|
+
meta: false
|
|
68
|
+
};
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
const modifier = modifierOf(part);
|
|
71
|
+
if (modifier !== null) required[modifier] = true;
|
|
72
|
+
}
|
|
73
|
+
return required.ctrl === press.ctrl && required.shift === press.shift && required.alt === press.alt && required.meta === press.meta;
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/events/idle.ts
|
|
77
|
+
/**
|
|
78
|
+
* Idleness as a bundle: `idle` in the Model, flipped by activity and
|
|
79
|
+
* silence. While active, activity debounced past `timeoutMs` settles to
|
|
80
|
+
* `BecameIdle`; while idle, the first activity wakes to `BecameActive` and
|
|
81
|
+
* the dependency flip restarts the watch. Starts active — a lurker idles
|
|
82
|
+
* when the timeout elapses. The clock is Effect's, so tests drive it with
|
|
83
|
+
* TestClock.
|
|
84
|
+
*/
|
|
85
|
+
const IdleModel = Schema.Struct({ idle: Schema.Boolean });
|
|
86
|
+
const IdleMessage = defineMessageUnion({
|
|
87
|
+
BecameIdle: {},
|
|
88
|
+
BecameActive: {}
|
|
89
|
+
});
|
|
90
|
+
const activity = () => {
|
|
91
|
+
if (typeof window === "undefined") return Stream.empty;
|
|
92
|
+
const senses = [
|
|
93
|
+
Stream.fromEventListener(window, "mousemove"),
|
|
94
|
+
Stream.fromEventListener(window, "keydown"),
|
|
95
|
+
Stream.fromEventListener(window, "pointerdown"),
|
|
96
|
+
Stream.fromEventListener(window, "scroll")
|
|
97
|
+
];
|
|
98
|
+
return Stream.mergeAll(senses, { concurrency: "unbounded" });
|
|
99
|
+
};
|
|
100
|
+
const Idle = Bundle.make("Idle", {
|
|
101
|
+
Model: IdleModel,
|
|
102
|
+
Message: IdleMessage,
|
|
103
|
+
args: Schema.Struct({ timeoutMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) }),
|
|
104
|
+
init: () => ({ model: { idle: false } }),
|
|
105
|
+
update: (model, message) => IdleMessage.match(message, {
|
|
106
|
+
BecameIdle: () => ({ model: {
|
|
107
|
+
...model,
|
|
108
|
+
idle: true
|
|
109
|
+
} }),
|
|
110
|
+
BecameActive: () => ({ model: {
|
|
111
|
+
...model,
|
|
112
|
+
idle: false
|
|
113
|
+
} })
|
|
114
|
+
}),
|
|
115
|
+
subscriptions: ({ timeoutMs }) => Subscription.make()((entry) => ({ watch: entry({ idle: Schema.Boolean }, {
|
|
116
|
+
modelToDependencies: (model) => ({ idle: model.idle }),
|
|
117
|
+
dependenciesToStream: ({ idle }) => idle ? activity().pipe(Stream.take(1), Stream.map(() => IdleMessage.BecameActive())) : Stream.concat(Stream.make(null), activity()).pipe(Stream.debounce(timeoutMs), Stream.map(() => IdleMessage.BecameIdle()))
|
|
118
|
+
}) }))
|
|
119
|
+
});
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/events/keyboard.ts
|
|
122
|
+
/**
|
|
123
|
+
* Keyboard as an entry — not a bundle, because the parent owns whatever key
|
|
124
|
+
* state it keeps (a held-keys set, a shortcut map, a focused field): this
|
|
125
|
+
* reports presses and releases, including auto-repeat, and the parent
|
|
126
|
+
* stores what matters. Hotkey matching stays application policy.
|
|
127
|
+
*/
|
|
128
|
+
const KeyboardMessage = defineMessageUnion({
|
|
129
|
+
Pressed: {
|
|
130
|
+
key: Schema.String,
|
|
131
|
+
repeat: Schema.Boolean,
|
|
132
|
+
ctrl: Schema.Boolean,
|
|
133
|
+
shift: Schema.Boolean,
|
|
134
|
+
alt: Schema.Boolean,
|
|
135
|
+
meta: Schema.Boolean
|
|
136
|
+
},
|
|
137
|
+
Released: { key: Schema.String }
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Key presses and releases on the window. Without a window the stream is
|
|
141
|
+
* empty instead of throwing. Lift with `Subscription.persistent`, mapping
|
|
142
|
+
* into the parent's Message.
|
|
143
|
+
*/
|
|
144
|
+
const keyboardEvents = () => {
|
|
145
|
+
if (typeof window === "undefined") return Stream.empty;
|
|
146
|
+
const downs = Stream.fromEventListener(window, "keydown").pipe(Stream.map((event) => KeyboardMessage.Pressed({
|
|
147
|
+
key: event.key,
|
|
148
|
+
repeat: event.repeat,
|
|
149
|
+
ctrl: event.ctrlKey,
|
|
150
|
+
shift: event.shiftKey,
|
|
151
|
+
alt: event.altKey,
|
|
152
|
+
meta: event.metaKey
|
|
153
|
+
})));
|
|
154
|
+
const ups = Stream.fromEventListener(window, "keyup").pipe(Stream.map((event) => KeyboardMessage.Released({ key: event.key })));
|
|
155
|
+
return Stream.mergeAll([downs, ups], { concurrency: "unbounded" });
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/events/pointer.ts
|
|
159
|
+
/**
|
|
160
|
+
* Pointer position as an entry — not a bundle, because the parent owns what
|
|
161
|
+
* it keeps (a cursor, a drag anchor, hover state): this reports moves, and
|
|
162
|
+
* the parent stores what matters. Element-scoped pointer needs are Mounts,
|
|
163
|
+
* not this window-level entry.
|
|
164
|
+
*/
|
|
165
|
+
const PointerMessage = defineMessageUnion({ Moved: {
|
|
166
|
+
x: Schema.Number,
|
|
167
|
+
y: Schema.Number
|
|
168
|
+
} });
|
|
169
|
+
/**
|
|
170
|
+
* Pointer moves on the window. Without a window the stream is empty instead
|
|
171
|
+
* of throwing. Lift with `Subscription.persistent`, mapping into the
|
|
172
|
+
* parent's Message.
|
|
173
|
+
*/
|
|
174
|
+
const pointerEvents = () => {
|
|
175
|
+
if (typeof window === "undefined") return Stream.empty;
|
|
176
|
+
return Stream.fromEventListener(window, "pointermove").pipe(Stream.map((event) => PointerMessage.Moved({
|
|
177
|
+
x: event.clientX,
|
|
178
|
+
y: event.clientY
|
|
179
|
+
})));
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/events/scroll.ts
|
|
183
|
+
/**
|
|
184
|
+
* Window scroll position as an entry — not a bundle, because the parent
|
|
185
|
+
* owns what it keeps (a stuck header, a back-to-top gate, restore-on-back):
|
|
186
|
+
* this reports positions, and the parent stores what matters.
|
|
187
|
+
* Element-scoped scrolling needs a Mount, not this window-level entry.
|
|
188
|
+
*/
|
|
189
|
+
const ScrollMessage = defineMessageUnion({ Scrolled: {
|
|
190
|
+
x: Schema.Number,
|
|
191
|
+
y: Schema.Number
|
|
192
|
+
} });
|
|
193
|
+
/**
|
|
194
|
+
* Scrolls on the window, in the capture phase so container scrolls (which
|
|
195
|
+
* do not bubble) report too. Without a window the stream is empty instead
|
|
196
|
+
* of throwing. Lift with `Subscription.persistent`, mapping into the
|
|
197
|
+
* parent's Message.
|
|
198
|
+
*/
|
|
199
|
+
const scrollEvents = () => {
|
|
200
|
+
if (typeof window === "undefined") return Stream.empty;
|
|
201
|
+
return Stream.fromEventListener(window, "scroll", { capture: true }).pipe(Stream.map(() => ScrollMessage.Scrolled({
|
|
202
|
+
x: window.scrollX,
|
|
203
|
+
y: window.scrollY
|
|
204
|
+
})));
|
|
205
|
+
};
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/events/visibility.ts
|
|
208
|
+
/**
|
|
209
|
+
* Page visibility as a bundle: `visible` in the Model, read from the
|
|
210
|
+
* document at startup and kept current by `visibilitychange`. A hidden page
|
|
211
|
+
* is the application's cue to pause polling, animations, and realtime
|
|
212
|
+
* streams; the decision stays in application `update`, not here.
|
|
213
|
+
*/
|
|
214
|
+
const VisibilityModel = Schema.Struct({ visible: Schema.Boolean });
|
|
215
|
+
const VisibilityMessage = defineMessageUnion({ Changed: { visible: Schema.Boolean } });
|
|
216
|
+
const isVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
217
|
+
const visibilityStream = () => {
|
|
218
|
+
if (typeof document === "undefined" || typeof document.addEventListener !== "function") return Stream.empty;
|
|
219
|
+
return Stream.concat(Stream.make(VisibilityMessage.Changed({ visible: isVisible() })), Stream.fromEventListener(document, "visibilitychange").pipe(Stream.map(() => VisibilityMessage.Changed({ visible: isVisible() }))));
|
|
220
|
+
};
|
|
221
|
+
const Visibility = Bundle.make("Visibility", {
|
|
222
|
+
Model: VisibilityModel,
|
|
223
|
+
Message: VisibilityMessage,
|
|
224
|
+
init: () => ({ model: { visible: isVisible() } }),
|
|
225
|
+
update: (model, message) => ({ model: { visible: message.visible } }),
|
|
226
|
+
subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(visibilityStream()) }))
|
|
227
|
+
});
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/events/window-size.ts
|
|
230
|
+
/**
|
|
231
|
+
* Window size as a bundle: `{ width, height }` in the Model, read from the
|
|
232
|
+
* window at startup and kept current by one `resize` listener. Breakpoints
|
|
233
|
+
* owns its own listener and derives names; place this one when views need
|
|
234
|
+
* the raw pixels. Placing both doubles resize listeners — pick the one the
|
|
235
|
+
* view reads.
|
|
236
|
+
*/
|
|
237
|
+
const WindowSizeModel = Schema.Struct({
|
|
238
|
+
width: Schema.Number,
|
|
239
|
+
height: Schema.Number
|
|
240
|
+
});
|
|
241
|
+
const WindowSizeMessage = defineMessageUnion({ Changed: {
|
|
242
|
+
width: Schema.Number,
|
|
243
|
+
height: Schema.Number
|
|
244
|
+
} });
|
|
245
|
+
const readSize = () => ({
|
|
246
|
+
width: window.innerWidth,
|
|
247
|
+
height: window.innerHeight
|
|
248
|
+
});
|
|
249
|
+
const sizeStream = () => {
|
|
250
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return Stream.empty;
|
|
251
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
252
|
+
const queue = yield* Queue.unbounded();
|
|
253
|
+
const onResize = () => {
|
|
254
|
+
const size = readSize();
|
|
255
|
+
Effect.runFork(Queue.offer(queue, WindowSizeMessage.Changed({
|
|
256
|
+
width: size.width,
|
|
257
|
+
height: size.height
|
|
258
|
+
})));
|
|
259
|
+
};
|
|
260
|
+
window.addEventListener("resize", onResize);
|
|
261
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
262
|
+
window.removeEventListener("resize", onResize);
|
|
263
|
+
}));
|
|
264
|
+
const size = readSize();
|
|
265
|
+
return Stream.concat(Stream.make(WindowSizeMessage.Changed({
|
|
266
|
+
width: size.width,
|
|
267
|
+
height: size.height
|
|
268
|
+
})), Stream.fromQueue(queue));
|
|
269
|
+
}));
|
|
270
|
+
};
|
|
271
|
+
const WindowSize = Bundle.make("WindowSize", {
|
|
272
|
+
Model: WindowSizeModel,
|
|
273
|
+
Message: WindowSizeMessage,
|
|
274
|
+
init: () => ({ model: {
|
|
275
|
+
width: 0,
|
|
276
|
+
height: 0
|
|
277
|
+
} }),
|
|
278
|
+
update: (model, message) => ({ model: {
|
|
279
|
+
width: message.width,
|
|
280
|
+
height: message.height
|
|
281
|
+
} }),
|
|
282
|
+
subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(sizeStream()) }))
|
|
283
|
+
});
|
|
284
|
+
//#endregion
|
|
285
|
+
export { ActiveElementMessage, Idle, IdleMessage, IdleModel, KeyboardMessage, PointerMessage, ScrollMessage, Visibility, VisibilityMessage, VisibilityModel, WindowSize, WindowSizeMessage, WindowSizeModel, activeElementEvents, keyboardEvents, matchHotkey, pointerEvents, scrollEvents };
|
|
286
|
+
|
|
287
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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 BecameIdle: () => ({ model: { ...model, idle: true } }),\n BecameActive: () => ({ model: { ...model, idle: false } }),\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 { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\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 = (): Stream.Stream<KeyboardMessage> => {\n if (typeof window === 'undefined') return Stream.empty\n const downs: Stream.Stream<KeyboardMessage> = Stream.fromEventListener<KeyboardEvent>(\n window,\n 'keydown',\n ).pipe(\n Stream.map(event =>\n 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 ),\n )\n const ups: Stream.Stream<KeyboardMessage> = Stream.fromEventListener<KeyboardEvent>(\n window,\n 'keyup',\n ).pipe(Stream.map(event => KeyboardMessage.Released({ key: event.key })))\n return Stream.mergeAll([downs, ups], { concurrency: 'unbounded' })\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 update: (model, message) => ({ 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'\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) => ({ model: { width: message.width, height: message.height } }),\n subscriptions: (): Subscription.Subscriptions<WindowSizeModel, WindowSizeMessage> =>\n Subscription.make<WindowSizeModel, WindowSizeMessage>()(() => ({\n changes: Subscription.persistent(sizeStream()),\n })),\n})\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Coordinates, FullscreenMessage, Geolocation, GeolocationMessage, GeolocationModel, LiveStream, LiveStreamService, MediaDevice, MediaDevicesHandle, MediaDevicesMessage, MediaDevicesModel, MediaStreamConstraints, MediaStreamHandle, MediaStreamMessage, MediaStreamModel, MediaTrackHandle, PermissionState, PermissionStatusHandle, PermissionsHandle, PermissionsMessage, PermissionsModel, Watch, WatchService, enterFullscreen, exitFullscreen, fullscreenChanges, mediaDevices, mediaStream, permissions } from "./device/index.mjs";
|
|
2
|
+
import { Autofocus, ClipboardMessage, Focused, Input, InputMask, ScriptMessage, ShareData, ShareMessage, applyMask, copyText, loadScript, share } from "./dom/index.mjs";
|
|
3
|
+
import { ActiveElementMessage, Idle, IdleMessage, IdleModel, KeyPress, KeyboardMessage, PointerMessage, ScrollMessage, Visibility, VisibilityMessage, VisibilityModel, WindowSize, WindowSizeMessage, WindowSizeModel, activeElementEvents, keyboardEvents, matchHotkey, pointerEvents, scrollEvents } from "./events/index.mjs";
|
|
4
|
+
import { Breakpoints, BreakpointsMessage, BreakpointsModel, MediaQuery, MediaQueryMessage, MediaQueryModel, Platform, PrefersDark, PrefersReducedMotion, breakpointFor, isBrowser, isServer, platformFromUA } from "./media/index.mjs";
|
|
5
|
+
import { Presence, PresenceMessage, PresenceModel, Spring, SpringMessage, SpringModel, Tween, TweenMessage, TweenModel, isVisible } from "./motion/index.mjs";
|
|
6
|
+
import { Acquired, AcquiredSource, BroadcastChannelHandle, BroadcastMessage, Online, OnlineMessage, OnlineModel, Socket, SocketHandle, SocketService, Source, SourceHandle, SourceService, SseMessage, SseModel, WebSocketMessage, WebSocketModel, broadcastMessages, postBroadcast, sse, websocket } from "./net/index.mjs";
|
|
7
|
+
import { Bounds, Intersection, IntersectionChanged, Measured, Mutated, Mutation, Resize, Resized } from "./observers/index.mjs";
|
|
8
|
+
import { HistoryModel, Locale, LocaleMessage, LocaleModel, MasonryPlacement, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualLayout, VirtualMessage, VirtualModel, VirtualWindow, canRedo, canUndo, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor } from "./state/index.mjs";
|
|
9
|
+
import { Interval, IntervalMessage, IntervalModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, debounce, formatRelativeTime } from "./time/index.mjs";
|
|
10
|
+
export { Acquired, AcquiredSource, ActiveElementMessage, Autofocus, Bounds, Breakpoints, BreakpointsMessage, BreakpointsModel, BroadcastChannelHandle, BroadcastMessage, ClipboardMessage, Coordinates, Focused, FullscreenMessage, Geolocation, GeolocationMessage, GeolocationModel, HistoryModel, Idle, IdleMessage, IdleModel, Input, InputMask, Intersection, IntersectionChanged, Interval, IntervalMessage, IntervalModel, KeyPress, KeyboardMessage, LiveStream, LiveStreamService, Locale, LocaleMessage, LocaleModel, MasonryPlacement, MeasureRow, Measured, MediaDevice, MediaDevicesHandle, MediaDevicesMessage, MediaDevicesModel, MediaQuery, MediaQueryMessage, MediaQueryModel, MediaStreamConstraints, MediaStreamHandle, MediaStreamMessage, MediaStreamModel, MediaTrackHandle, Mutated, Mutation, Online, OnlineMessage, OnlineModel, Pagination, PaginationMessage, PaginationModel, PermissionState, PermissionStatusHandle, PermissionsHandle, PermissionsMessage, PermissionsModel, Platform, PointerMessage, PrefersDark, PrefersReducedMotion, Presence, PresenceMessage, PresenceModel, Resize, Resized, RowMeasured, ScriptMessage, ScrollMessage, SelectionSet, SelectionSetMessage, SelectionSetModel, ShareData, ShareMessage, Socket, SocketHandle, SocketService, Source, SourceHandle, SourceService, Spring, SpringMessage, SpringModel, SseMessage, SseModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, Tween, TweenMessage, TweenModel, Viewport, ViewportScrolled, Virtual, VirtualLayout, VirtualMessage, VirtualModel, VirtualWindow, Visibility, VisibilityMessage, VisibilityModel, Watch, WatchService, WebSocketMessage, WebSocketModel, WindowSize, WindowSizeMessage, WindowSizeModel, activeElementEvents, applyMask, breakpointFor, broadcastMessages, canRedo, canUndo, copyText, debounce, enterFullscreen, exitFullscreen, formatRelativeTime, fullscreenChanges, history, isAtEnd, isBrowser, isSelected, isServer, isVisible, keyboardEvents, loadScript, masonry, matchHotkey, mediaDevices, mediaStream, offset, offsetFor, pageCount, permissions, platformFromUA, pointerEvents, postBroadcast, range, scrollEvents, share, sse, stickyHeader, totalHeight, visibleRange, websocket, windowFor };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Autofocus, ClipboardMessage, Focused, Input, InputMask, ScriptMessage, ShareData, ShareMessage, applyMask, copyText, loadScript, share } from "./dom/index.mjs";
|
|
2
|
+
import { Coordinates, FullscreenMessage, Geolocation, GeolocationMessage, GeolocationModel, LiveStream, MediaDevice, MediaDevicesMessage, MediaDevicesModel, MediaStreamMessage, MediaStreamModel, PermissionState, PermissionsMessage, PermissionsModel, Watch, enterFullscreen, exitFullscreen, fullscreenChanges, mediaDevices, mediaStream, permissions } from "./device/index.mjs";
|
|
3
|
+
import { ActiveElementMessage, Idle, IdleMessage, IdleModel, KeyboardMessage, PointerMessage, ScrollMessage, Visibility, VisibilityMessage, VisibilityModel, WindowSize, WindowSizeMessage, WindowSizeModel, activeElementEvents, keyboardEvents, matchHotkey, pointerEvents, scrollEvents } from "./events/index.mjs";
|
|
4
|
+
import { Breakpoints, BreakpointsMessage, BreakpointsModel, MediaQuery, MediaQueryMessage, MediaQueryModel, Platform, PrefersDark, PrefersReducedMotion, breakpointFor, isBrowser, isServer, platformFromUA } from "./media/index.mjs";
|
|
5
|
+
import { Presence, PresenceMessage, PresenceModel, Spring, SpringMessage, SpringModel, Tween, TweenMessage, TweenModel, isVisible } from "./motion/index.mjs";
|
|
6
|
+
import { BroadcastMessage, Online, OnlineMessage, OnlineModel, Socket, Source, SseMessage, SseModel, WebSocketMessage, WebSocketModel, broadcastMessages, postBroadcast, sse, websocket } from "./net/index.mjs";
|
|
7
|
+
import { Bounds, Intersection, IntersectionChanged, Measured, Mutated, Mutation, Resize, Resized } from "./observers/index.mjs";
|
|
8
|
+
import { HistoryModel, Locale, LocaleMessage, LocaleModel, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, canRedo, canUndo, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor } from "./state/index.mjs";
|
|
9
|
+
import { Interval, IntervalMessage, IntervalModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, debounce, formatRelativeTime } from "./time/index.mjs";
|
|
10
|
+
export { ActiveElementMessage, Autofocus, Bounds, Breakpoints, BreakpointsMessage, BreakpointsModel, BroadcastMessage, ClipboardMessage, Coordinates, Focused, FullscreenMessage, Geolocation, GeolocationMessage, GeolocationModel, HistoryModel, Idle, IdleMessage, IdleModel, Input, InputMask, Intersection, IntersectionChanged, Interval, IntervalMessage, IntervalModel, KeyboardMessage, LiveStream, Locale, LocaleMessage, LocaleModel, MeasureRow, Measured, MediaDevice, MediaDevicesMessage, MediaDevicesModel, MediaQuery, MediaQueryMessage, MediaQueryModel, MediaStreamMessage, MediaStreamModel, Mutated, Mutation, Online, OnlineMessage, OnlineModel, Pagination, PaginationMessage, PaginationModel, PermissionState, PermissionsMessage, PermissionsModel, Platform, PointerMessage, PrefersDark, PrefersReducedMotion, Presence, PresenceMessage, PresenceModel, Resize, Resized, RowMeasured, ScriptMessage, ScrollMessage, SelectionSet, SelectionSetMessage, SelectionSetModel, ShareData, ShareMessage, Socket, Source, Spring, SpringMessage, SpringModel, SseMessage, SseModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, Tween, TweenMessage, TweenModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, Visibility, VisibilityMessage, VisibilityModel, Watch, WebSocketMessage, WebSocketModel, WindowSize, WindowSizeMessage, WindowSizeModel, activeElementEvents, applyMask, breakpointFor, broadcastMessages, canRedo, canUndo, copyText, debounce, enterFullscreen, exitFullscreen, formatRelativeTime, fullscreenChanges, history, isAtEnd, isBrowser, isSelected, isServer, isVisible, keyboardEvents, loadScript, masonry, matchHotkey, mediaDevices, mediaStream, offset, offsetFor, pageCount, permissions, platformFromUA, pointerEvents, postBroadcast, range, scrollEvents, share, sse, stickyHeader, totalHeight, visibleRange, websocket, windowFor };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Bundle } from "foldkit-bundle";
|
|
3
|
+
//#region src/media/breakpoints.d.ts
|
|
4
|
+
export declare const BreakpointsModel: Schema.Struct<{
|
|
5
|
+
readonly width: Schema.Number;
|
|
6
|
+
readonly breakpoint: Schema.NullOr<Schema.String>;
|
|
7
|
+
}>;
|
|
8
|
+
export type BreakpointsModel = typeof BreakpointsModel.Type;
|
|
9
|
+
export declare const BreakpointsMessage: import("foldkit/message").MessageUnion<{
|
|
10
|
+
readonly Changed: {
|
|
11
|
+
readonly width: Schema.Number;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export type BreakpointsMessage = typeof BreakpointsMessage.Type;
|
|
15
|
+
export declare const breakpointFor: (width: number, breakpoints: Readonly<Record<string, number>>) => string | null;
|
|
16
|
+
export declare const Breakpoints: Bundle.Bundle<"Breakpoints", {
|
|
17
|
+
readonly breakpoints: {
|
|
18
|
+
readonly [x: string]: number;
|
|
19
|
+
};
|
|
20
|
+
}, {
|
|
21
|
+
width: number;
|
|
22
|
+
breakpoint: string | null;
|
|
23
|
+
}, {
|
|
24
|
+
readonly _tag: "Changed";
|
|
25
|
+
readonly width: number;
|
|
26
|
+
}, never, never, never, void, {}, {}>;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/media/mediaquery.d.ts
|
|
29
|
+
export declare const MediaQueryModel: Schema.Struct<{
|
|
30
|
+
readonly matches: Schema.Boolean;
|
|
31
|
+
}>;
|
|
32
|
+
export type MediaQueryModel = typeof MediaQueryModel.Type;
|
|
33
|
+
export declare const MediaQueryMessage: import("foldkit/message").MessageUnion<{
|
|
34
|
+
readonly Changed: {
|
|
35
|
+
readonly matches: Schema.Boolean;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
38
|
+
export type MediaQueryMessage = typeof MediaQueryMessage.Type;
|
|
39
|
+
export declare const MediaQuery: Bundle.Bundle<"MediaQuery", {
|
|
40
|
+
readonly query: string;
|
|
41
|
+
}, {
|
|
42
|
+
matches: boolean;
|
|
43
|
+
}, {
|
|
44
|
+
readonly _tag: "Changed";
|
|
45
|
+
readonly matches: boolean;
|
|
46
|
+
}, never, never, never, void, {}, {}>;
|
|
47
|
+
/** `(prefers-color-scheme: dark)`, bound so placements give no args. */
|
|
48
|
+
export declare const PrefersDark: Bundle.Bundle<"MediaQuery", void, {
|
|
49
|
+
matches: boolean;
|
|
50
|
+
}, {
|
|
51
|
+
readonly _tag: "Changed";
|
|
52
|
+
readonly matches: boolean;
|
|
53
|
+
}, never, never, never, void, {}, {}>;
|
|
54
|
+
/** `(prefers-reduced-motion: reduce)`, bound so placements give no args. */
|
|
55
|
+
export declare const PrefersReducedMotion: Bundle.Bundle<"MediaQuery", void, {
|
|
56
|
+
matches: boolean;
|
|
57
|
+
}, {
|
|
58
|
+
readonly _tag: "Changed";
|
|
59
|
+
readonly matches: boolean;
|
|
60
|
+
}, never, never, never, void, {}, {}>;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/media/platform.d.ts
|
|
63
|
+
/**
|
|
64
|
+
* Platform detection as pure functions. `platformFromUA` reads a user-agent
|
|
65
|
+
* string (passed in, so tests pin it without stubbing globals); order
|
|
66
|
+
* matters — Android contains "Linux", iPhones mention "Mac" — so mobile
|
|
67
|
+
* checks come first. `isBrowser`/`isServer` split SSR from client for init
|
|
68
|
+
* defaults. Desktop-mode iPads report `mac` — their UA hides the tablet;
|
|
69
|
+
* touch detection stays the application's job.
|
|
70
|
+
*/
|
|
71
|
+
export declare const Platform: readonly ["mac", "windows", "linux", "android", "ios", "unknown"];
|
|
72
|
+
export type Platform = (typeof Platform)[number];
|
|
73
|
+
export declare const platformFromUA: (userAgent: string) => Platform;
|
|
74
|
+
/** Whether a window exists: the client side of an SSR split. */
|
|
75
|
+
export declare const isBrowser: () => boolean;
|
|
76
|
+
/** Whether no window exists: the server side of an SSR split. */
|
|
77
|
+
export declare const isServer: () => boolean;
|
|
78
|
+
//#endregion
|
|
79
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/media/breakpoints.ts","../../src/media/mediaquery.ts","../../src/media/platform.ts"],"mappings":";;;qBAWa,kBAAgB,OAAA;;;;YAIjB,0BAA0B,iBAAiB;qBAE1C,8CAAkB;;oBAAlB,OAAA;;;YACD,4BAA4B,mBAAmB;qBAE9C,gBAAa,eACX,aACA,SAAS;qBAyCX,aAAW,OAAA;;;;;;;;;;;;;qBCrDX,iBAAe,OAAA;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;sBAAjB,OAAA;;;YACD,2BAA2B,kBAAkB;qBA4B5C,YAAU,OAAA;;;;;;;;;qBAaV,aAAW,OAAA;;;;;;;qBAGX,sBAAoB,OAAA;;;;;;;;;;;;;;;;qBClDpB;YACD,mBAAmB;qBAElB,iBAAc,sBAAwB;;qBAWtC;;qBAGA"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import * as Subscription from "foldkit/subscription";
|
|
4
|
+
import { Bundle } from "foldkit-bundle";
|
|
5
|
+
//#region src/media/breakpoints.ts
|
|
6
|
+
/**
|
|
7
|
+
* Named breakpoints over the viewport width: one `resize` listener, derived
|
|
8
|
+
* names in `update`. Thresholds are mobile-first min-widths: the breakpoint
|
|
9
|
+
* is the largest name whose threshold is <= width. Ties break
|
|
10
|
+
* alphabetically so duplicates stay deterministic.
|
|
11
|
+
*/
|
|
12
|
+
const BreakpointsModel = Schema.Struct({
|
|
13
|
+
width: Schema.Number,
|
|
14
|
+
breakpoint: Schema.NullOr(Schema.String)
|
|
15
|
+
});
|
|
16
|
+
const BreakpointsMessage = defineMessageUnion({ Changed: { width: Schema.Number } });
|
|
17
|
+
const breakpointFor = (width, breakpoints) => {
|
|
18
|
+
let best = null;
|
|
19
|
+
let bestThreshold = -Infinity;
|
|
20
|
+
for (const name of Object.keys(breakpoints)) {
|
|
21
|
+
const threshold = breakpoints[name];
|
|
22
|
+
if (threshold <= width && (threshold > bestThreshold || threshold === bestThreshold && (best === null || name > best))) {
|
|
23
|
+
best = name;
|
|
24
|
+
bestThreshold = threshold;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
};
|
|
29
|
+
const widthStream = () => {
|
|
30
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return Stream.empty;
|
|
31
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
32
|
+
const queue = yield* Queue.unbounded();
|
|
33
|
+
const onResize = () => {
|
|
34
|
+
Effect.runFork(Queue.offer(queue, BreakpointsMessage.Changed({ width: window.innerWidth })));
|
|
35
|
+
};
|
|
36
|
+
window.addEventListener("resize", onResize);
|
|
37
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
38
|
+
window.removeEventListener("resize", onResize);
|
|
39
|
+
}));
|
|
40
|
+
return Stream.concat(Stream.make(BreakpointsMessage.Changed({ width: window.innerWidth })), Stream.fromQueue(queue));
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
const Breakpoints = Bundle.make("Breakpoints", {
|
|
44
|
+
Model: BreakpointsModel,
|
|
45
|
+
Message: BreakpointsMessage,
|
|
46
|
+
args: Schema.Struct({ breakpoints: Schema.Record(Schema.String, Schema.Number.pipe(Schema.check(Schema.isFinite()))) }),
|
|
47
|
+
init: (args) => ({ model: {
|
|
48
|
+
width: 0,
|
|
49
|
+
breakpoint: breakpointFor(0, args.breakpoints)
|
|
50
|
+
} }),
|
|
51
|
+
update: (model, message, args) => BreakpointsMessage.match(message, { Changed: ({ width }) => ({ model: {
|
|
52
|
+
width,
|
|
53
|
+
breakpoint: breakpointFor(width, args.breakpoints)
|
|
54
|
+
} }) }),
|
|
55
|
+
subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(widthStream()) }))
|
|
56
|
+
});
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/media/mediaquery.ts
|
|
59
|
+
/**
|
|
60
|
+
* A media query as a bundle: `matches` in the Model, kept current by a
|
|
61
|
+
* `matchMedia` change stream. State stays in the parent Model, so it replays
|
|
62
|
+
* and time-travels like any other state; the browser only reports facts.
|
|
63
|
+
*/
|
|
64
|
+
const MediaQueryModel = Schema.Struct({ matches: Schema.Boolean });
|
|
65
|
+
const MediaQueryMessage = defineMessageUnion({ Changed: { matches: Schema.Boolean } });
|
|
66
|
+
const matchMediaStream = (query) => {
|
|
67
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return Stream.empty;
|
|
68
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
69
|
+
const queue = yield* Queue.unbounded();
|
|
70
|
+
const list = window.matchMedia(query);
|
|
71
|
+
const onChange = (event) => {
|
|
72
|
+
Effect.runFork(Queue.offer(queue, MediaQueryMessage.Changed({ matches: event.matches })));
|
|
73
|
+
};
|
|
74
|
+
list.addEventListener("change", onChange);
|
|
75
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
76
|
+
list.removeEventListener("change", onChange);
|
|
77
|
+
}));
|
|
78
|
+
return Stream.concat(Stream.make(MediaQueryMessage.Changed({ matches: list.matches })), Stream.fromQueue(queue));
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
const MediaQuery = Bundle.make("MediaQuery", {
|
|
82
|
+
Model: MediaQueryModel,
|
|
83
|
+
Message: MediaQueryMessage,
|
|
84
|
+
args: Schema.Struct({ query: Schema.String }),
|
|
85
|
+
init: () => ({ model: { matches: false } }),
|
|
86
|
+
update: (model, message) => ({ model: { matches: message.matches } }),
|
|
87
|
+
subscriptions: ({ query }) => Subscription.make()(() => ({ changes: Subscription.persistent(matchMediaStream(query)) }))
|
|
88
|
+
});
|
|
89
|
+
/** `(prefers-color-scheme: dark)`, bound so placements give no args. */
|
|
90
|
+
const PrefersDark = MediaQuery.with({ query: "(prefers-color-scheme: dark)" });
|
|
91
|
+
/** `(prefers-reduced-motion: reduce)`, bound so placements give no args. */
|
|
92
|
+
const PrefersReducedMotion = MediaQuery.with({ query: "(prefers-reduced-motion: reduce)" });
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/media/platform.ts
|
|
95
|
+
/**
|
|
96
|
+
* Platform detection as pure functions. `platformFromUA` reads a user-agent
|
|
97
|
+
* string (passed in, so tests pin it without stubbing globals); order
|
|
98
|
+
* matters — Android contains "Linux", iPhones mention "Mac" — so mobile
|
|
99
|
+
* checks come first. `isBrowser`/`isServer` split SSR from client for init
|
|
100
|
+
* defaults. Desktop-mode iPads report `mac` — their UA hides the tablet;
|
|
101
|
+
* touch detection stays the application's job.
|
|
102
|
+
*/
|
|
103
|
+
const Platform = [
|
|
104
|
+
"mac",
|
|
105
|
+
"windows",
|
|
106
|
+
"linux",
|
|
107
|
+
"android",
|
|
108
|
+
"ios",
|
|
109
|
+
"unknown"
|
|
110
|
+
];
|
|
111
|
+
const platformFromUA = (userAgent) => {
|
|
112
|
+
const ua = userAgent.toLowerCase();
|
|
113
|
+
if (ua.includes("android")) return "android";
|
|
114
|
+
if (ua.includes("iphone") || ua.includes("ipad") || ua.includes("ipod")) return "ios";
|
|
115
|
+
if (ua.includes("windows") || ua.includes("win32") || ua.includes("win64")) return "windows";
|
|
116
|
+
if (ua.includes("macintosh") || ua.includes("mac os")) return "mac";
|
|
117
|
+
if (ua.includes("linux")) return "linux";
|
|
118
|
+
return "unknown";
|
|
119
|
+
};
|
|
120
|
+
/** Whether a window exists: the client side of an SSR split. */
|
|
121
|
+
const isBrowser = () => typeof window !== "undefined";
|
|
122
|
+
/** Whether no window exists: the server side of an SSR split. */
|
|
123
|
+
const isServer = () => !isBrowser();
|
|
124
|
+
//#endregion
|
|
125
|
+
export { Breakpoints, BreakpointsMessage, BreakpointsModel, MediaQuery, MediaQueryMessage, MediaQueryModel, Platform, PrefersDark, PrefersReducedMotion, breakpointFor, isBrowser, isServer, platformFromUA };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/media/breakpoints.ts","../../src/media/mediaquery.ts","../../src/media/platform.ts"],"sourcesContent":["/**\n * Named breakpoints over the viewport width: one `resize` listener, derived\n * names in `update`. Thresholds are mobile-first min-widths: the breakpoint\n * is the largest name whose threshold is <= width. Ties break\n * alphabetically so duplicates stay deterministic.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const BreakpointsModel = Schema.Struct({\n width: Schema.Number,\n breakpoint: Schema.NullOr(Schema.String),\n})\nexport type BreakpointsModel = typeof BreakpointsModel.Type\n\nexport const BreakpointsMessage = defineMessageUnion({ Changed: { width: Schema.Number } })\nexport type BreakpointsMessage = typeof BreakpointsMessage.Type\n\nexport const breakpointFor = (\n width: number,\n breakpoints: Readonly<Record<string, number>>,\n): string | null => {\n let best: string | null = null\n let bestThreshold = -Infinity\n for (const name of Object.keys(breakpoints)) {\n const threshold = breakpoints[name]!\n if (\n threshold <= width &&\n (threshold > bestThreshold || (threshold === bestThreshold && (best === null || name > best)))\n ) {\n best = name\n bestThreshold = threshold\n }\n }\n return best\n}\n\nconst widthStream = (): Stream.Stream<BreakpointsMessage> => {\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<BreakpointsMessage>()\n const onResize = () => {\n Effect.runFork(Queue.offer(queue, BreakpointsMessage.Changed({ width: window.innerWidth })))\n }\n window.addEventListener('resize', onResize)\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n window.removeEventListener('resize', onResize)\n }),\n )\n return Stream.concat(\n Stream.make(BreakpointsMessage.Changed({ width: window.innerWidth })),\n Stream.fromQueue(queue),\n )\n }),\n )\n}\n\nexport const Breakpoints = Bundle.make('Breakpoints', {\n Model: BreakpointsModel,\n Message: BreakpointsMessage,\n args: Schema.Struct({\n breakpoints: Schema.Record(Schema.String, Schema.Number.pipe(Schema.check(Schema.isFinite()))),\n }),\n init: args => ({\n model: { width: 0, breakpoint: breakpointFor(0, args.breakpoints) },\n }),\n update: (model, message, args) =>\n BreakpointsMessage.match(message, {\n Changed: ({ width }) => ({\n model: { width, breakpoint: breakpointFor(width, args.breakpoints) },\n }),\n }),\n subscriptions: (): Subscription.Subscriptions<BreakpointsModel, BreakpointsMessage> =>\n Subscription.make<BreakpointsModel, BreakpointsMessage>()(() => ({\n changes: Subscription.persistent(widthStream()),\n })),\n})\n","/**\n * A media query as a bundle: `matches` in the Model, kept current by a\n * `matchMedia` change stream. State stays in the parent Model, so it replays\n * and time-travels like any other state; the browser only reports facts.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const MediaQueryModel = Schema.Struct({ matches: Schema.Boolean })\nexport type MediaQueryModel = typeof MediaQueryModel.Type\n\nexport const MediaQueryMessage = defineMessageUnion({ Changed: { matches: Schema.Boolean } })\nexport type MediaQueryMessage = typeof MediaQueryMessage.Type\n\nconst matchMediaStream = (query: string): Stream.Stream<MediaQueryMessage> => {\n // No window (SSR) or no matchMedia (old browser, minimal DOM): nothing to\n // listen to, so the slice keeps its initial value instead of throwing.\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return Stream.empty\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<MediaQueryMessage>()\n const list = window.matchMedia(query)\n const onChange = (event: MediaQueryListEvent) => {\n Effect.runFork(Queue.offer(queue, MediaQueryMessage.Changed({ matches: event.matches })))\n }\n list.addEventListener('change', onChange)\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n list.removeEventListener('change', onChange)\n }),\n )\n // The current value first, so the Model is right before the first change.\n return Stream.concat(\n Stream.make(MediaQueryMessage.Changed({ matches: list.matches })),\n Stream.fromQueue(queue),\n )\n }),\n )\n}\n\nexport const MediaQuery = Bundle.make('MediaQuery', {\n Model: MediaQueryModel,\n Message: MediaQueryMessage,\n args: Schema.Struct({ query: Schema.String }),\n init: () => ({ model: { matches: false } }),\n update: (model, message) => ({ model: { matches: message.matches } }),\n subscriptions: ({ query }): Subscription.Subscriptions<MediaQueryModel, MediaQueryMessage> =>\n Subscription.make<MediaQueryModel, MediaQueryMessage>()(() => ({\n changes: Subscription.persistent(matchMediaStream(query)),\n })),\n})\n\n/** `(prefers-color-scheme: dark)`, bound so placements give no args. */\nexport const PrefersDark = MediaQuery.with({ query: '(prefers-color-scheme: dark)' })\n\n/** `(prefers-reduced-motion: reduce)`, bound so placements give no args. */\nexport const PrefersReducedMotion = MediaQuery.with({ query: '(prefers-reduced-motion: reduce)' })\n","/**\n * Platform detection as pure functions. `platformFromUA` reads a user-agent\n * string (passed in, so tests pin it without stubbing globals); order\n * matters — Android contains \"Linux\", iPhones mention \"Mac\" — so mobile\n * checks come first. `isBrowser`/`isServer` split SSR from client for init\n * defaults. Desktop-mode iPads report `mac` — their UA hides the tablet;\n * touch detection stays the application's job.\n */\nexport const Platform = ['mac', 'windows', 'linux', 'android', 'ios', 'unknown'] as const\nexport type Platform = (typeof Platform)[number]\n\nexport const platformFromUA = (userAgent: string): Platform => {\n const ua = userAgent.toLowerCase()\n if (ua.includes('android')) return 'android'\n if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod')) return 'ios'\n if (ua.includes('windows') || ua.includes('win32') || ua.includes('win64')) return 'windows'\n if (ua.includes('macintosh') || ua.includes('mac os')) return 'mac'\n if (ua.includes('linux')) return 'linux'\n return 'unknown'\n}\n\n/** Whether a window exists: the client side of an SSR split. */\nexport const isBrowser = (): boolean => typeof window !== 'undefined'\n\n/** Whether no window exists: the server side of an SSR split. */\nexport const isServer = (): boolean => !isBrowser()\n"],"mappings":"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"}
|