elements-kit 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +20 -19
  2. package/dist/attributes.d.mts +1 -1
  3. package/dist/await.d.mts +6 -5
  4. package/dist/await.mjs +0 -36
  5. package/dist/{types-ncUK9rki.d.mts → children-s3nFjpLA.d.mts} +125 -227
  6. package/dist/custom-elements-CdpilMDu.d.mts +87 -0
  7. package/dist/custom-elements.d.mts +2 -2
  8. package/dist/{element-D3om9yA6.mjs → element-Di0PYFX1.mjs} +15 -38
  9. package/dist/for.d.mts +9 -4
  10. package/dist/for.mjs +1 -1
  11. package/dist/{fragment-ROPRBJQd.mjs → fragment-Bvs1sGu1.mjs} +3 -3
  12. package/dist/hydrate/index.mjs +1 -1
  13. package/dist/{hydrate-6Qx4n4Su.mjs → hydrate-CHomBRY9.mjs} +4 -4
  14. package/dist/integrations/astro-client.mjs +2 -2
  15. package/dist/integrations/astro-server.mjs +2 -2
  16. package/dist/integrations/astro-slots.d.mts +6 -5
  17. package/dist/integrations/astro-slots.mjs +9 -8
  18. package/dist/integrations/react.d.mts +1 -1
  19. package/dist/integrations/react.mjs +1 -1
  20. package/dist/jsx-runtime/index.d.mts +2 -2
  21. package/dist/jsx-runtime/index.mjs +2 -2
  22. package/dist/polyfill-BVNd6ogU.d.mts +9 -0
  23. package/dist/render.mjs +1 -1
  24. package/dist/server/index.mjs +1 -1
  25. package/dist/{server-iFSng50i.mjs → server-CTkyXTV6.mjs} +2 -2
  26. package/dist/signals/index.d.mts +1 -1
  27. package/dist/slot.d.mts +83 -2
  28. package/dist/slot.mjs +179 -2
  29. package/dist/ui/badge/badge.css +5 -5
  30. package/dist/ui/button/button.css +15 -15
  31. package/dist/ui/card/card.css +2 -2
  32. package/dist/ui/group/group.css +61 -0
  33. package/dist/ui/marketing/marketing.css +5 -3
  34. package/dist/ui/otp-input/index.d.mts +132 -0
  35. package/dist/ui/otp-input/index.mjs +328 -0
  36. package/dist/ui/otp-input/otp-input.css +157 -0
  37. package/dist/ui/overlay/index.css +52 -4
  38. package/dist/ui/overlay/index.d.mts +237 -63
  39. package/dist/ui/overlay/index.mjs +658 -66
  40. package/dist/ui/overlay/overlay.css +58 -2
  41. package/dist/ui/styles/index.css +664 -0
  42. package/dist/ui/styles/radius.css +7 -7
  43. package/dist/ui/styles/unset.css +212 -210
  44. package/dist/utilities/active-element.d.mts +1 -1
  45. package/dist/utilities/async.d.mts +1 -1
  46. package/dist/utilities/debounced.d.mts +1 -1
  47. package/dist/utilities/dom-lifecycle.bench.mjs +2 -2
  48. package/dist/utilities/element-rect.d.mts +1 -1
  49. package/dist/utilities/element-scroll.d.mts +1 -1
  50. package/dist/utilities/event-driven.d.mts +1 -1
  51. package/dist/utilities/event-listener.d.mts +1 -1
  52. package/dist/utilities/focus-within.d.mts +1 -1
  53. package/dist/utilities/hover.d.mts +1 -1
  54. package/dist/utilities/interval.d.mts +1 -1
  55. package/dist/utilities/location.d.mts +1 -1
  56. package/dist/utilities/media-devices.d.mts +1 -1
  57. package/dist/utilities/media-player.d.mts +1 -1
  58. package/dist/utilities/media-query.d.mts +1 -1
  59. package/dist/utilities/network.d.mts +1 -1
  60. package/dist/utilities/orientation.d.mts +1 -1
  61. package/dist/utilities/previous.d.mts +1 -1
  62. package/dist/utilities/promise.d.mts +1 -1
  63. package/dist/utilities/routing.d.mts +1 -1
  64. package/dist/utilities/search-params.d.mts +1 -1
  65. package/dist/utilities/storage.d.mts +1 -1
  66. package/dist/utilities/throttled.d.mts +1 -1
  67. package/dist/utilities/timeout.d.mts +1 -1
  68. package/dist/utilities/window-focus.d.mts +1 -1
  69. package/dist/utilities/window-size.d.mts +1 -1
  70. package/package.json +10 -1
  71. package/dist/custom-elements-DK5VUcWA.d.mts +0 -55
  72. package/dist/slot-BFWXeFRl.d.mts +0 -77
  73. package/dist/slot-Dvf30O0B.mjs +0 -138
  74. /package/dist/{attributes-3r7Diua4.d.mts → attributes-DILeh3-s.d.mts} +0 -0
  75. /package/dist/{scope-zbEG299F.mjs → scope-C1vELXqP.mjs} +0 -0
@@ -1,19 +1,46 @@
1
1
  import { _ as onCleanup, u as effect } from "../../lib-DYypKhxk.mjs";
2
2
  import "../../signals/index.mjs";
3
3
  import { createElementRect } from "../../utilities/element-rect.mjs";
4
- //#region src/ui/overlay/constrain.ts
4
+ import { arrow, autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
5
+ //#region src/ui/overlay/constraint.ts
6
+ /**
7
+ * Builds a {@link Region}: from an `Element` (observed via
8
+ * `ResizeObserver` through `createElementRect`; cleanup routes through
9
+ * the current scope), from a plain rect (static), or with no argument —
10
+ * the live viewport (read at call time, not observed).
11
+ */
12
+ function constraint(source) {
13
+ if (source === void 0) return {
14
+ top: () => 0,
15
+ left: () => 0,
16
+ width: () => window.innerWidth,
17
+ height: () => window.innerHeight
18
+ };
19
+ if (source instanceof Element) {
20
+ const rect = createElementRect(source);
21
+ onCleanup(() => rect[Symbol.dispose]());
22
+ return rect;
23
+ }
24
+ return {
25
+ top: () => source.top,
26
+ left: () => source.left,
27
+ width: () => source.width,
28
+ height: () => source.height
29
+ };
30
+ }
5
31
  /**
6
32
  * Resolves a custom property holding a length to pixels. Plain `px` values
7
33
  * parse directly; anything else (`svh` / `calc()` / fractions of the
8
- * constraint) resolves natively by measuring a hidden probe.
34
+ * constraint) resolves natively by measuring a hidden probe. `fallback` is
35
+ * a CSS length expression used when the property is unset.
9
36
  */
10
- function resolveVarPx(overlay, name, axis) {
37
+ function resolveVarPx(overlay, name, axis, fallback) {
11
38
  const raw = getComputedStyle(overlay).getPropertyValue(name).trim();
12
39
  if (/^-?\d+(\.\d+)?px$/.test(raw)) return parseFloat(raw);
13
40
  const probe = document.createElement("div");
14
41
  probe.style.position = "absolute";
15
42
  probe.style.visibility = "hidden";
16
- probe.style[axis] = `var(${name})`;
43
+ probe.style[axis] = fallback ? `var(${name}, ${fallback})` : `var(${name})`;
17
44
  overlay.appendChild(probe);
18
45
  const px = probe.getBoundingClientRect()[axis];
19
46
  probe.remove();
@@ -29,14 +56,14 @@ function resolveConstraint(overlay) {
29
56
  };
30
57
  }
31
58
  /**
32
- * Confines an overlay to a rect source by syncing it into the
33
- * `--overlay-constraint-*` variables. Pass an `Element` (observed via
34
- * `ResizeObserver` through `createElementRect`) or a custom
35
- * {@link ConstraintRect}. Every location clamp and gesture bound derives
36
- * from those variables, so the overlay re-clamps when the rect changes.
59
+ * Confines an overlay to a {@link Region} by syncing it into the
60
+ * `--overlay-constraint-*` variables. Every location clamp and gesture
61
+ * bound derives from those variables, so the overlay re-clamps when the
62
+ * region changes.
37
63
  *
38
- * Caveat: `ResizeObserver` fires on size changes — a container that moves
39
- * without resizing (e.g. page scroll) does not retrigger the sync.
64
+ * Caveat: an element region observes size changes (`ResizeObserver`) — a
65
+ * container that moves without resizing (e.g. page scroll) does not
66
+ * retrigger the sync.
40
67
  *
41
68
  * Registers its cleanup with the current scope (`onCleanup`) and also
42
69
  * returns it as `dispose` / `Symbol.dispose`; disposing removes the
@@ -44,24 +71,21 @@ function resolveConstraint(overlay) {
44
71
  *
45
72
  * @example
46
73
  * ```ts
47
- * import { constrainOverlay } from "elements-kit/ui/overlay";
74
+ * import { constraint, confine } from "elements-kit/ui/overlay";
48
75
  *
49
76
  * const panel = document.querySelector("dialog.x-overlay")!;
50
- * constrainOverlay(panel, document.querySelector("main")!);
77
+ * confine(panel, constraint(document.querySelector("main")!));
51
78
  * ```
52
79
  */
53
- function constrainOverlay(overlay, source) {
54
- const owned = source instanceof Element;
55
- const rect = owned ? createElementRect(source) : source;
80
+ function confine(overlay, region) {
56
81
  const stop = effect(() => {
57
- overlay.style.setProperty("--overlay-constraint-top", `${rect.top()}px`);
58
- overlay.style.setProperty("--overlay-constraint-left", `${rect.left()}px`);
59
- overlay.style.setProperty("--overlay-constraint-width", `${rect.width()}px`);
60
- overlay.style.setProperty("--overlay-constraint-height", `${rect.height()}px`);
82
+ overlay.style.setProperty("--overlay-constraint-top", `${region.top()}px`);
83
+ overlay.style.setProperty("--overlay-constraint-left", `${region.left()}px`);
84
+ overlay.style.setProperty("--overlay-constraint-width", `${region.width()}px`);
85
+ overlay.style.setProperty("--overlay-constraint-height", `${region.height()}px`);
61
86
  });
62
87
  const dispose = () => {
63
88
  stop();
64
- if (owned) rect[Symbol.dispose]();
65
89
  overlay.style.removeProperty("--overlay-constraint-top");
66
90
  overlay.style.removeProperty("--overlay-constraint-left");
67
91
  overlay.style.removeProperty("--overlay-constraint-width");
@@ -74,11 +98,368 @@ function constrainOverlay(overlay, source) {
74
98
  };
75
99
  }
76
100
  //#endregion
101
+ //#region src/ui/overlay/anchor.ts
102
+ /** The same compound gate as the index.css ANCHORED MODE block — both
103
+ * halves or neither, so the CSS and JS tiers can never disagree. */
104
+ const nativeAnchorSupport = () => typeof CSS !== "undefined" && CSS.supports("anchor-name: --x") && CSS.supports("position-area: block-end");
105
+ /** Measure whether the chain rule actually lands on the anchor's VISUAL
106
+ * box — `CSS.supports` is a parser check, and Firefox 151 passes it
107
+ * while resolving `anchor()` against the pre-transform layout box (a
108
+ * `translate`d trigger pins half a box off). The probe host is
109
+ * deliberately translated to catch exactly that. Inconclusive without a
110
+ * layout engine (tests) — then trust the declared support. */
111
+ let chainMeasured;
112
+ function probeChain() {
113
+ const host = document.createElement("div");
114
+ host.style.cssText = "position:fixed;top:80px;left:60px;width:40px;height:20px;translate:10px 5px;visibility:hidden;pointer-events:none;anchor-name:--ek-chain-probe";
115
+ const pin = document.createElement("div");
116
+ pin.style.cssText = "position:fixed;visibility:hidden;pointer-events:none;position-anchor:--ek-chain-probe;top:anchor(top);left:anchor(left);width:anchor-size(width);height:anchor-size(height)";
117
+ document.body.append(host, pin);
118
+ const hostRect = host.getBoundingClientRect();
119
+ const rect = pin.getBoundingClientRect();
120
+ host.remove();
121
+ pin.remove();
122
+ if (Math.abs(hostRect.top - 85) > 1) return true;
123
+ return Math.abs(rect.top - 85) < 1 && Math.abs(rect.left - 70) < 1 && Math.abs(rect.width - 40) < 1;
124
+ }
125
+ /** The follow pin needs more than the placement gate — the chain rule
126
+ * mirrors the followed box with `anchor()`/`anchor-size()`. A browser
127
+ * that places but can't chain falls back to the JS rect copy. */
128
+ const chainSupport = () => {
129
+ if (!nativeAnchorSupport() || !CSS.supports("top: anchor(top)") || !CSS.supports("width: anchor-size(width)")) return false;
130
+ chainMeasured ??= probeChain();
131
+ return chainMeasured;
132
+ };
133
+ /** While the Floating UI loop tracks, geometry writes must land
134
+ * instantly — but ONLY geometry. Enter/exit (opacity, scale) and
135
+ * close (display) keep transitioning, so `@starting-style` still plays. */
136
+ const TRACKING_TRANSITIONS = "opacity, scale, display";
137
+ let anchorNames = 0;
138
+ /**
139
+ * Maps a `position-area` value to the Floating UI placement. Spanning
140
+ * toward an edge leaves the box flush with the *opposite* edge, so
141
+ * `span-*-end` is a `-start` alignment (and vice versa) — Floating UI
142
+ * resolves `-start`/`-end` logically, so the span tokens need no dir
143
+ * check; only the physical inline sides do. Unknown values fall back to
144
+ * `bottom` (the CSS default, `block-end`).
145
+ */
146
+ function areaToPlacement(area, rtl = false) {
147
+ const tokens = area.trim().split(/\s+/);
148
+ const main = tokens.find((t) => !t.startsWith("span-"));
149
+ const span = tokens.find((t) => t.startsWith("span-")) ?? "";
150
+ return `${main === "block-start" ? "top" : main === "inline-start" ? rtl ? "right" : "left" : main === "inline-end" ? rtl ? "left" : "right" : "bottom"}${span.endsWith("-end") ? "-start" : span.endsWith("-start") ? "-end" : ""}`;
151
+ }
152
+ /**
153
+ * Gives `overlay` its anchor element and returns it. The overlay follows
154
+ * the anchor for life; the anchor follows `follow` (element or rect)
155
+ * until something moves it. Pass the returned element to `draggable()`
156
+ * to make the composition tearable — dragging moves the anchor.
157
+ *
158
+ * `follow` may be a getter reading a signal — re-pinning on change is
159
+ * how a shared popover slides between nav triggers: the native chain
160
+ * glides there on the anchor element's CSS transition; the channel
161
+ * engine lets that one write animate.
162
+ *
163
+ * Registers all cleanup with the current scope (`onCleanup`): the anchor
164
+ * element, the wiring, and the stamped attributes are removed together.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { anchor, draggable, rubber } from "elements-kit/ui/overlay";
169
+ *
170
+ * const a = anchor(panel, trigger);
171
+ * draggable(a, undefined, rubber()).attach(panel);
172
+ *
173
+ * // shared nav popover — re-anchors (and glides) when the signal changes
174
+ * const active = signal(firstTrigger);
175
+ * anchor(menu, () => active(), { arrow: true });
176
+ * ```
177
+ */
178
+ function anchor(overlay, follow, opts = {}) {
179
+ const native = nativeAnchorSupport() && !opts.within && !opts.arrow;
180
+ let disposed = false;
181
+ const resolveFollow = () => (typeof follow === "function" ? follow() : follow) ?? void 0;
182
+ const el = document.createElement("span");
183
+ el.className = "x-overlay-anchor";
184
+ el.setAttribute("aria-hidden", "true");
185
+ if (!native) el.style.transitionProperty = "none";
186
+ document.body.append(el);
187
+ const proxyName = `--overlay-anchor-${anchorNames++}`;
188
+ const followName = `--overlay-follow-${anchorNames++}`;
189
+ const placeAtRect = (rect) => {
190
+ el.style.top = `${rect.top}px`;
191
+ el.style.left = `${rect.left}px`;
192
+ el.style.width = `${rect.width}px`;
193
+ el.style.height = `${rect.height}px`;
194
+ };
195
+ /** Pin the anchor element to a follow target (the `data-follow`
196
+ * contract — `draggable()` tears it on the first pointer-down).
197
+ * Re-pinning to another element keeps the SAME `position-anchor` name,
198
+ * so under the native chain the proxy's `anchor()` insets resolve to
199
+ * the new box and its CSS transition GLIDES there — the nav slide. */
200
+ let pinnedEl;
201
+ let hasPinned = false;
202
+ let stopFollowSync;
203
+ const releasePinMachinery = () => {
204
+ stopFollowSync?.();
205
+ stopFollowSync = void 0;
206
+ pinnedEl?.style.removeProperty("anchor-name");
207
+ pinnedEl = void 0;
208
+ };
209
+ const pin = (target) => {
210
+ const repositioning = hasPinned;
211
+ const flip = repositioning && target instanceof Element && chainSupport();
212
+ if (flip) placeAtRect(el.getBoundingClientRect());
213
+ releasePinMachinery();
214
+ if (!target) {
215
+ el.style.top = "50vh";
216
+ el.style.left = "50vw";
217
+ return;
218
+ }
219
+ hasPinned = true;
220
+ if (!(target instanceof Element)) {
221
+ el.removeAttribute("data-follow");
222
+ placeAtRect(target);
223
+ return;
224
+ }
225
+ el.setAttribute("data-follow", "");
226
+ if (chainSupport()) {
227
+ target.style?.setProperty("anchor-name", followName);
228
+ pinnedEl = target;
229
+ el.style.setProperty("position-anchor", followName);
230
+ if (flip) el.offsetTop;
231
+ el.style.removeProperty("top");
232
+ el.style.removeProperty("left");
233
+ el.style.removeProperty("width");
234
+ el.style.removeProperty("height");
235
+ } else {
236
+ let first = !repositioning;
237
+ const sync = () => {
238
+ if (first) {
239
+ first = false;
240
+ const prev = el.style.transitionProperty;
241
+ el.style.transitionProperty = "none";
242
+ placeAtRect(target.getBoundingClientRect());
243
+ el.offsetTop;
244
+ if (prev) el.style.transitionProperty = prev;
245
+ else el.style.removeProperty("transition-property");
246
+ return;
247
+ }
248
+ placeAtRect(target.getBoundingClientRect());
249
+ };
250
+ stopFollowSync = autoUpdate(target, el, sync);
251
+ }
252
+ };
253
+ const unpinCleanup = () => releasePinMachinery();
254
+ const followObserver = new MutationObserver(() => {
255
+ if (!el.hasAttribute("data-follow")) unpinCleanup();
256
+ });
257
+ followObserver.observe(el, {
258
+ attributes: true,
259
+ attributeFilter: ["data-follow"]
260
+ });
261
+ const stampPlacedHint = () => {
262
+ const style = getComputedStyle(overlay);
263
+ const hint = areaToPlacement(style.getPropertyValue("--overlay-area"), style.direction === "rtl");
264
+ overlay.setAttribute("data-placed", hint.split("-")[0]);
265
+ };
266
+ const isOpen = () => {
267
+ if (overlay.hasAttribute("open")) return true;
268
+ try {
269
+ return overlay.matches(":popover-open");
270
+ } catch {
271
+ return false;
272
+ }
273
+ };
274
+ let haltLoop;
275
+ let engineDispose;
276
+ /** Channel engine only: reposition against a re-pinned target, letting
277
+ * that one write animate (the morph to the new anchor). */
278
+ let onRepoint;
279
+ if (native) {
280
+ overlay.setAttribute("data-anchor", "element");
281
+ el.style.setProperty("anchor-name", proxyName);
282
+ overlay.style.setProperty("position-anchor", proxyName);
283
+ engineDispose = () => {
284
+ const rect = overlay.getBoundingClientRect();
285
+ if (isOpen() && rect.width > 0) {
286
+ const c = resolveConstraint(overlay);
287
+ overlay.style.setProperty("--overlay-x", `${rect.left + rect.width / 2 - c.left}px`);
288
+ overlay.style.setProperty("--overlay-y", `${rect.top + rect.height / 2 - c.top}px`);
289
+ }
290
+ overlay.style.removeProperty("position-anchor");
291
+ overlay.removeAttribute("data-anchor");
292
+ };
293
+ } else {
294
+ let arrowEl = null;
295
+ let arrowOwned = false;
296
+ if (opts.arrow) {
297
+ arrowEl = overlay.querySelector(":scope > .x-overlay-arrow");
298
+ if (!arrowEl) {
299
+ arrowEl = document.createElement("span");
300
+ arrowEl.className = "x-overlay-arrow";
301
+ arrowEl.setAttribute("aria-hidden", "true");
302
+ overlay.append(arrowEl);
303
+ arrowOwned = true;
304
+ }
305
+ if (typeof opts.arrow === "number") overlay.style.setProperty("--overlay-arrow-size", `${opts.arrow}px`);
306
+ }
307
+ const boundaryRect = () => {
308
+ const w = opts.within;
309
+ if (!w) return void 0;
310
+ return {
311
+ x: w.left(),
312
+ y: w.top(),
313
+ width: w.width(),
314
+ height: w.height()
315
+ };
316
+ };
317
+ const update = async () => {
318
+ const style = getComputedStyle(overlay);
319
+ const placementHint = areaToPlacement(style.getPropertyValue("--overlay-area"), style.direction === "rtl");
320
+ const gapPx = resolveVarPx(overlay, "--overlay-gap", "width", "var(--space-2, 8px)");
321
+ const boundary = boundaryRect();
322
+ const middleware = [
323
+ offset(gapPx),
324
+ flip(boundary ? { boundary } : void 0),
325
+ shift(boundary ? { boundary } : void 0)
326
+ ];
327
+ if (arrowEl) middleware.push(arrow({ element: arrowEl }));
328
+ const { x, y, placement, middlewareData } = await computePosition(el, overlay, {
329
+ strategy: "fixed",
330
+ placement: placementHint,
331
+ middleware
332
+ });
333
+ if (disposed) return;
334
+ const constraint = resolveConstraint(overlay);
335
+ overlay.style.setProperty("--overlay-x", `${x + overlay.offsetWidth / 2 - constraint.left}px`);
336
+ overlay.style.setProperty("--overlay-y", `${y + overlay.offsetHeight / 2 - constraint.top}px`);
337
+ if (placement) overlay.setAttribute("data-placed", placement.split("-")[0]);
338
+ const a = middlewareData.arrow;
339
+ if (a) {
340
+ if (a.x !== void 0) overlay.style.setProperty("--overlay-arrow-x", `${a.x}px`);
341
+ if (a.y !== void 0) overlay.style.setProperty("--overlay-arrow-y", `${a.y}px`);
342
+ }
343
+ };
344
+ let stop;
345
+ let morphIn = isOpen();
346
+ const suppress = () => overlay.style.setProperty("transition-property", TRACKING_TRANSITIONS);
347
+ const release = () => overlay.style.removeProperty("transition-property");
348
+ const startLoop = () => {
349
+ if (stop) return;
350
+ const instant = !morphIn;
351
+ morphIn = false;
352
+ if (instant) suppress();
353
+ else if (!overlay.style.getPropertyValue("--overlay-x")) {
354
+ const rect = overlay.getBoundingClientRect();
355
+ const c = resolveConstraint(overlay);
356
+ overlay.style.setProperty("--overlay-x", `${rect.left + rect.width / 2 - c.left}px`);
357
+ overlay.style.setProperty("--overlay-y", `${rect.top + rect.height / 2 - c.top}px`);
358
+ }
359
+ let pending = instant;
360
+ stop = autoUpdate(el, overlay, () => update().then(() => {
361
+ if (pending && !disposed) {
362
+ pending = false;
363
+ release();
364
+ }
365
+ }));
366
+ };
367
+ haltLoop = () => {
368
+ stop?.();
369
+ stop = void 0;
370
+ release();
371
+ };
372
+ onRepoint = () => {
373
+ if (stop) update();
374
+ };
375
+ const sync = () => {
376
+ if (disposed) return;
377
+ if (isOpen()) startLoop();
378
+ else haltLoop?.();
379
+ };
380
+ const onDragMove = () => {
381
+ suppress();
382
+ update();
383
+ };
384
+ const onDragEnd = () => {
385
+ if (disposed || !stop) return;
386
+ release();
387
+ update();
388
+ };
389
+ overlay.addEventListener("toggle", sync);
390
+ overlay.addEventListener("close", sync);
391
+ el.addEventListener("dragmove", onDragMove);
392
+ el.addEventListener("dragend", onDragEnd);
393
+ const openObserver = new MutationObserver(sync);
394
+ openObserver.observe(overlay, {
395
+ attributes: true,
396
+ attributeFilter: ["open"]
397
+ });
398
+ sync();
399
+ engineDispose = () => {
400
+ haltLoop?.();
401
+ openObserver.disconnect();
402
+ overlay.removeEventListener("toggle", sync);
403
+ overlay.removeEventListener("close", sync);
404
+ el.removeEventListener("dragmove", onDragMove);
405
+ el.removeEventListener("dragend", onDragEnd);
406
+ overlay.style.removeProperty("--overlay-arrow-x");
407
+ overlay.style.removeProperty("--overlay-arrow-y");
408
+ if (typeof opts.arrow === "number") overlay.style.removeProperty("--overlay-arrow-size");
409
+ if (arrowOwned) arrowEl?.remove();
410
+ };
411
+ }
412
+ const repin = (event) => {
413
+ if (disposed) return;
414
+ const target = resolveFollow();
415
+ if (!target) return;
416
+ if ((event.newState === "open" || event.type !== "toggle" && isOpen()) && !el.hasAttribute("data-follow")) {
417
+ el.style.removeProperty("top");
418
+ el.style.removeProperty("left");
419
+ el.style.removeProperty("width");
420
+ el.style.removeProperty("height");
421
+ pin(target);
422
+ }
423
+ };
424
+ overlay.addEventListener("toggle", repin);
425
+ const repinObserver = new MutationObserver((mutations) => {
426
+ if (mutations.some((m) => m.attributeName === "open")) repin(new Event("open"));
427
+ });
428
+ repinObserver.observe(overlay, {
429
+ attributes: true,
430
+ attributeFilter: ["open"]
431
+ });
432
+ stampPlacedHint();
433
+ let stopFollowEffect;
434
+ if (typeof follow === "function") {
435
+ let first = true;
436
+ stopFollowEffect = effect(() => {
437
+ pin(resolveFollow());
438
+ if (!first) onRepoint?.();
439
+ first = false;
440
+ });
441
+ } else pin(resolveFollow());
442
+ const dispose = () => {
443
+ disposed = true;
444
+ stopFollowEffect?.();
445
+ engineDispose?.();
446
+ unpinCleanup();
447
+ followObserver.disconnect();
448
+ repinObserver.disconnect();
449
+ overlay.removeEventListener("toggle", repin);
450
+ overlay.removeAttribute("data-placed");
451
+ el.remove();
452
+ };
453
+ onCleanup(dispose);
454
+ return el;
455
+ }
456
+ //#endregion
77
457
  //#region src/ui/overlay/resize-strategy.ts
78
458
  /**
79
459
  * Resize strategies — pluggable policy for a resize drag's live bounds
80
460
  * and resting size. Pure (no DOM); the gesture injects the context.
81
- * Built-ins: `freeResize` (default), `detents`.
461
+ * Built-in: `freeResize` (default). `detents()` (detents.ts) quantizes a
462
+ * constraint region and doubles as a strategy.
82
463
  */
83
464
  /** How far (ms) a release velocity is projected when picking a rest. */
84
465
  const PROJECTION_MS = 160;
@@ -117,19 +498,35 @@ function freeResize(opts) {
117
498
  }
118
499
  };
119
500
  }
501
+ //#endregion
502
+ //#region src/ui/overlay/detents.ts
120
503
  /**
121
- * Snap to discrete steps each a fraction of the constraint along the
122
- * axis (number `0–1`) or a CSS length (string). Flick-aware; shrinking
123
- * past the smallest step dismisses.
504
+ * Quantizes a region: each stop is a fraction of the region along the
505
+ * axis (number `0–1`) or a CSS length (string, resize-only). Flick-aware
506
+ * on release; shrinking past the smallest stop dismisses (when the
507
+ * consumer allows dismissal).
508
+ *
509
+ * @example
510
+ * ```ts
511
+ * import { constraint, detents } from "elements-kit/ui/overlay";
512
+ *
513
+ * const d = detents(constraint(), [0.25, 0.6, 0.9]); // of the viewport
514
+ * ```
124
515
  */
125
- function detents(steps) {
126
- const resolved = (ctx) => steps.map((s) => clamp(ctx.resolve(s), ctx.min, ctx.max)).sort((a, b) => a - b);
516
+ function detents(region, stops) {
517
+ const resolved = (ctx) => stops.map((s) => clamp(ctx.resolve(s), ctx.min, ctx.max)).sort((a, b) => a - b);
127
518
  return {
128
- bounds: (ctx) => {
519
+ region,
520
+ stops,
521
+ positions(axis) {
522
+ const size = axis === "width" ? region.width() : region.height();
523
+ return stops.filter((s) => typeof s === "number").map((s) => s * size).sort((a, b) => a - b);
524
+ },
525
+ bounds(ctx) {
129
526
  const s = resolved(ctx);
130
527
  return [s[0] ?? ctx.min, s[s.length - 1] ?? ctx.max];
131
528
  },
132
- rest: (ctx) => {
529
+ rest(ctx) {
133
530
  const s = resolved(ctx);
134
531
  const i = closestDetent(ctx.size, s, ctx.velocity, ctx.dismissible, ctx.velocityThreshold);
135
532
  return i === -1 ? null : s[i];
@@ -276,13 +673,177 @@ function slidePastRoom(resisted, max, sign) {
276
673
  * `null` when docked (the CSS clamp already holds that edge). The single
277
674
  * source of anchoring truth for block, inline, and corner resizes.
278
675
  */
279
- function anchor(args) {
676
+ function anchor$1(args) {
280
677
  const { axis, center0, constraint, anchorSign, startSize, size, docked } = args;
281
678
  if (docked) return null;
282
679
  const origin = axis === "width" ? constraint.left : constraint.top;
283
680
  return (axis === "width" ? center0.x : center0.y) - origin + anchorSign * (size - startSize) / 2;
284
681
  }
285
682
  //#endregion
683
+ //#region src/ui/overlay/draggable.ts
684
+ const isSpace = (s) => "positions" in s;
685
+ function draggable(target, space, ...effects) {
686
+ const region = space && isSpace(space) ? space.region : space;
687
+ const boundsFor = (axis) => {
688
+ if (!region) return axis === "x" ? [0, window.innerWidth] : [0, window.innerHeight];
689
+ return axis === "x" ? [region.left(), region.left() + region.width()] : [region.top(), region.top() + region.height()];
690
+ };
691
+ const during = (value, axis) => {
692
+ const bounds = boundsFor(axis);
693
+ let v = value;
694
+ for (const e of effects) if (e.during) v = e.during(v, bounds);
695
+ return v;
696
+ };
697
+ const write = (x, y) => {
698
+ target.style.left = `${x}px`;
699
+ target.style.top = `${y}px`;
700
+ };
701
+ const emit = (type, detail) => target.dispatchEvent(new CustomEvent(type, {
702
+ detail,
703
+ bubbles: true
704
+ }));
705
+ /** Tear a `data-follow` pin: freeze the current rect inline (inline
706
+ * wins over the pin rule — no jump), then drop the pin machinery. */
707
+ const takeOver = () => {
708
+ const rect = target.getBoundingClientRect();
709
+ write(rect.left, rect.top);
710
+ if (target.hasAttribute("data-follow")) {
711
+ target.style.width = `${rect.width}px`;
712
+ target.style.height = `${rect.height}px`;
713
+ target.removeAttribute("data-follow");
714
+ target.style.removeProperty("position-anchor");
715
+ }
716
+ return {
717
+ left: rect.left,
718
+ top: rect.top
719
+ };
720
+ };
721
+ const settleAxis = (value, velocity, axis) => {
722
+ const bounds = boundsFor(axis);
723
+ let v = value;
724
+ for (const e of effects) {
725
+ if (!e.settle) continue;
726
+ const s = e.settle(v, velocity, bounds);
727
+ if (s === null) return null;
728
+ v = s;
729
+ }
730
+ if (space && isSpace(space)) {
731
+ const stops = space.positions(axis === "x" ? "width" : "height");
732
+ if (stops.length && region) {
733
+ const origin = axis === "x" ? region.left() : region.top();
734
+ const projected = v + velocity * 160;
735
+ let best = origin + stops[0];
736
+ for (const stop of stops) {
737
+ const candidate = origin + stop;
738
+ if (Math.abs(candidate - projected) < Math.abs(best - projected)) best = candidate;
739
+ }
740
+ v = best;
741
+ }
742
+ }
743
+ return v;
744
+ };
745
+ const attachments = [];
746
+ const attach = (handle) => {
747
+ let engaged = null;
748
+ const onDown = (event) => {
749
+ if (engaged || event.button !== 0) return;
750
+ if (event.target?.closest("button, a, label, input, select, textarea, [contenteditable]")) return;
751
+ engaged = {
752
+ start: takeOver(),
753
+ down: {
754
+ x: event.clientX,
755
+ y: event.clientY
756
+ },
757
+ tracker: {
758
+ prev: {
759
+ x: event.clientX,
760
+ y: event.clientY
761
+ },
762
+ lastTime: event.timeStamp,
763
+ velocity: {
764
+ x: 0,
765
+ y: 0
766
+ }
767
+ },
768
+ transition: target.style.transitionProperty
769
+ };
770
+ target.style.transitionProperty = "none";
771
+ handle.setPointerCapture(event.pointerId);
772
+ };
773
+ const onMove = (event) => {
774
+ if (!engaged) return;
775
+ const client = {
776
+ x: event.clientX,
777
+ y: event.clientY
778
+ };
779
+ engaged.tracker.velocity = updateVelocity(engaged.tracker, client, event.timeStamp);
780
+ engaged.tracker.prev = client;
781
+ engaged.tracker.lastTime = event.timeStamp;
782
+ const x = during(engaged.start.left + (client.x - engaged.down.x), "x");
783
+ const y = during(engaged.start.top + (client.y - engaged.down.y), "y");
784
+ write(x, y);
785
+ emit("dragmove", {
786
+ x,
787
+ y
788
+ });
789
+ };
790
+ const onUp = (event) => {
791
+ if (!engaged) return;
792
+ const { start, down, tracker, transition } = engaged;
793
+ engaged = null;
794
+ target.style.transitionProperty = transition;
795
+ const raw = {
796
+ x: start.left + (event.clientX - down.x),
797
+ y: start.top + (event.clientY - down.y)
798
+ };
799
+ const x = settleAxis(raw.x, tracker.velocity.x, "x");
800
+ const y = settleAxis(raw.y, tracker.velocity.y, "y");
801
+ const rest = x === null || y === null ? null : {
802
+ x,
803
+ y
804
+ };
805
+ if (rest) write(rest.x, rest.y);
806
+ emit("dragend", {
807
+ ...raw,
808
+ velocity: tracker.velocity,
809
+ rest
810
+ });
811
+ };
812
+ handle.addEventListener("pointerdown", onDown);
813
+ handle.addEventListener("pointermove", onMove);
814
+ handle.addEventListener("pointerup", onUp);
815
+ handle.addEventListener("pointercancel", onUp);
816
+ const dispose = () => {
817
+ handle.removeEventListener("pointerdown", onDown);
818
+ handle.removeEventListener("pointermove", onMove);
819
+ handle.removeEventListener("pointerup", onUp);
820
+ handle.removeEventListener("pointercancel", onUp);
821
+ };
822
+ attachments.push(dispose);
823
+ return { dispose };
824
+ };
825
+ const update = (point) => {
826
+ takeOver();
827
+ const x = during(point.x, "x");
828
+ const y = during(point.y, "y");
829
+ write(x, y);
830
+ emit("dragmove", {
831
+ x,
832
+ y
833
+ });
834
+ };
835
+ const dispose = () => {
836
+ for (const detach of attachments.splice(0)) detach();
837
+ };
838
+ onCleanup(dispose);
839
+ return {
840
+ update,
841
+ attach,
842
+ dispose,
843
+ [Symbol.dispose]: dispose
844
+ };
845
+ }
846
+ //#endregion
286
847
  //#region src/ui/overlay/move-session.ts
287
848
  /** Move bounds on the box center — the same clamp the stylesheet applies to
288
849
  * the persisted location point (center floor = half the box). */
@@ -636,7 +1197,7 @@ function resizeHeightSession(snap, resizer, io, side) {
636
1197
  min: 0,
637
1198
  max: hardMax
638
1199
  };
639
- const anchorAt = (size) => anchor({
1200
+ const anchorAt = (size) => anchor$1({
640
1201
  axis: "height",
641
1202
  center0: snap.center0,
642
1203
  constraint: snap.constraint,
@@ -741,7 +1302,7 @@ function resizeSession(snap, resizer, io, block, inline) {
741
1302
  const frameFor = (w, h) => ({
742
1303
  w,
743
1304
  h,
744
- x: anchor({
1305
+ x: anchor$1({
745
1306
  axis: "width",
746
1307
  center0: snap.center0,
747
1308
  constraint: snap.constraint,
@@ -750,7 +1311,7 @@ function resizeSession(snap, resizer, io, block, inline) {
750
1311
  size: w,
751
1312
  docked: false
752
1313
  }),
753
- y: anchor({
1314
+ y: anchor$1({
754
1315
  axis: "height",
755
1316
  center0: snap.center0,
756
1317
  constraint: snap.constraint,
@@ -816,7 +1377,7 @@ function resizeWidthSession(snap, resizer, io, side) {
816
1377
  min: 0,
817
1378
  max: hardMax
818
1379
  };
819
- const anchorAt = (size) => anchor({
1380
+ const anchorAt = (size) => anchor$1({
820
1381
  axis: "width",
821
1382
  center0: snap.center0,
822
1383
  constraint: snap.constraint,
@@ -904,10 +1465,10 @@ function selectSession(key, parsed, snapshot, resizer, move, io) {
904
1465
  *
905
1466
  * @example
906
1467
  * ```ts
907
- * import { createOverlayGestures, detents } from "elements-kit/ui/overlay";
1468
+ * import { constraint, createOverlayGestures, detents } from "elements-kit/ui/overlay";
908
1469
  *
909
1470
  * const el = document.querySelector("dialog.x-overlay")!;
910
- * createOverlayGestures(el, { resize: detents([0.25, 0.6, 0.9]) });
1471
+ * createOverlayGestures(el, { resize: detents(constraint(), [0.25, 0.6, 0.9]) });
911
1472
  * el.addEventListener("resizechange", (e) => console.log(e.detail));
912
1473
  * ```
913
1474
  */
@@ -960,42 +1521,73 @@ function createOverlayGestures(overlay, options) {
960
1521
  };
961
1522
  }
962
1523
  //#endregion
963
- //#region src/ui/overlay/overlay.ts
964
- /**
965
- * One-call wiring for an interactive `.x-overlay`: optional constraint
966
- * sync + pointer gestures, under a single disposable. Structure stays in
967
- * markup (`data-resize` / `data-draggable`); policy is config.
968
- *
969
- * Registers its cleanup with the current scope (`onCleanup`) and also
970
- * returns it as `dispose` / `Symbol.dispose`.
971
- *
972
- * @example
973
- * ```ts
974
- * import { overlay, detents } from "elements-kit/ui/overlay";
975
- *
976
- * const el = document.querySelector("dialog.x-overlay")!;
977
- * const o = overlay(el, {
978
- * constrain: document.querySelector("main")!,
979
- * resize: detents([0.25, 0.6, 0.9]),
980
- * onResize: (size) => console.log(size),
981
- * });
982
- * ```
983
- */
984
- function overlay(el, config) {
985
- const { constrain, ...gestureOptions } = config ?? {};
986
- let constraint = null;
987
- if (constrain) constraint = constrainOverlay(el, constrain);
988
- const gestures = createOverlayGestures(el, gestureOptions);
1524
+ //#region src/ui/overlay/resize.ts
1525
+ /** Chain settle effects after the strategy's rest. */
1526
+ function withEffects(base, effects) {
1527
+ if (!effects.length) return base;
1528
+ return {
1529
+ bounds: base.bounds?.bind(base),
1530
+ rest(ctx) {
1531
+ let rested = base.rest(ctx);
1532
+ if (rested === null) return null;
1533
+ for (const e of effects) {
1534
+ if (!e.settle) continue;
1535
+ const s = e.settle(rested, ctx.velocity, [ctx.min, ctx.max]);
1536
+ if (s === null) return null;
1537
+ rested = s;
1538
+ }
1539
+ return rested;
1540
+ }
1541
+ };
1542
+ }
1543
+ function resize(target, space, ...effects) {
1544
+ const io = createFrameIO(target, {
1545
+ strategy: withEffects(space ?? freeResize(), effects),
1546
+ dismissible: true,
1547
+ velocityThreshold: .5
1548
+ });
1549
+ const buildSession = () => {
1550
+ const parsed = parseResize(target.getAttribute("data-resize") ?? "");
1551
+ const { snapshot, resizer } = io.engage();
1552
+ if (parsed.block !== null && parsed.inline !== null) return resizeSession(snapshot, resizer, io, parsed.block, parsed.inline);
1553
+ if (parsed.block !== null) return resizeHeightSession(snapshot, resizer, io, parsed.block);
1554
+ if (parsed.inline !== null) return resizeWidthSession(snapshot, resizer, io, parsed.inline);
1555
+ return null;
1556
+ };
1557
+ const attachments = [];
1558
+ const attach = (handle) => {
1559
+ const recognizer = createGestureRecognizer(handle, {
1560
+ canEngage: () => true,
1561
+ engage: () => buildSession()
1562
+ });
1563
+ attachments.push(recognizer.dispose);
1564
+ return { dispose: recognizer.dispose };
1565
+ };
1566
+ const update = (size) => {
1567
+ const parsed = parseResize(target.getAttribute("data-resize") ?? "");
1568
+ const key = parsed.block !== null && parsed.inline === null ? "h" : "w";
1569
+ io.commit({ [key]: size });
1570
+ };
989
1571
  const dispose = () => {
990
- gestures.dispose();
991
- constraint?.dispose();
1572
+ for (const detach of attachments.splice(0)) detach();
992
1573
  };
993
1574
  onCleanup(dispose);
994
1575
  return {
995
- resize: (size) => gestures.resize(size),
1576
+ update,
1577
+ attach,
996
1578
  dispose,
997
1579
  [Symbol.dispose]: dispose
998
1580
  };
999
1581
  }
1000
1582
  //#endregion
1001
- export { PROJECTION_MS, clamp, closestDetent, constrainOverlay, createOverlayGestures, detents, freeResize, overlay, resolveConstraint };
1583
+ //#region src/ui/overlay/rubber.ts
1584
+ /**
1585
+ * Edge resistance — values past the bounds move at a fraction of the
1586
+ * pointer (the iOS rubber band). Pure `resist()` under the hood; no
1587
+ * channels involved, so it applies to anything a service moves.
1588
+ */
1589
+ function rubber() {
1590
+ return { during: (value, [lo, hi]) => resist(value, lo, hi) };
1591
+ }
1592
+ //#endregion
1593
+ export { PROJECTION_MS, anchor, areaToPlacement, clamp, closestDetent, confine, constraint, createOverlayGestures, detents, draggable, freeResize, resize, resolveConstraint, resolveVarPx, rubber };