elements-kit 0.27.9 → 0.27.11

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.
@@ -7,6 +7,10 @@ type Axis = keyof ReadonlyBox;
7
7
  interface IDirection {
8
8
  readonly direction: "ltr" | "rtl";
9
9
  }
10
+ interface Point {
11
+ x: number;
12
+ y: number;
13
+ }
10
14
  interface ReadonlyBox {
11
15
  readonly x: number;
12
16
  readonly y: number;
@@ -78,28 +82,31 @@ type NoInline = {
78
82
  * both edges of one axis.
79
83
  */
80
84
  type Boundary = (BlockEdge & InlineEdge) | (BlockEdge & NoInline) | (NoBlock & InlineEdge);
81
- /** Where {@link Region.place} puts a box's top-left corner. */
82
- interface Placement {
83
- readonly x: number;
84
- readonly y: number;
85
+ /** Horizontal origin keyword, as CSS spells it. */
86
+ type OriginX = "left" | "center" | "right";
87
+ /** The vertical one. */
88
+ type OriginY = "top" | "center" | "bottom";
89
+ /** Which point of a box lands on its position. A `transform-origin`. */
90
+ interface Origin {
91
+ readonly x: OriginX;
92
+ readonly y: OriginY;
93
+ }
94
+ /** Has an origin. */
95
+ interface IOrigin {
96
+ readonly origin: Origin;
85
97
  }
86
98
  /**
87
- * Somewhere a box may go: a semi-bounded plane, each axis pinned or free.
88
- * `place` is the only operation: it never writes, so the caller picks which
89
- * channels to take `box.x = region.place(box).x` moves one axis and leaves
90
- * the rest. Size is never returned: the box is placed as given, so one larger
91
- * than its room overflows rather than shrinks, as CSS does.
99
+ * Somewhere a box may go: each axis pinned or free. `place` never writes, so
100
+ * the caller takes the channels it wants, and never returns a size, so a box
101
+ * larger than its room overflows rather than shrinks, as CSS does.
92
102
  *
93
- * The four insets read back as CSS would want them: the pinned edge's
94
- * coordinate, `null` for the other three (`auto`). A centred axis is not an
95
- * inset — both its edges are `null`.
103
+ * `x`/`y` are the pin lines, `origin` the box point landing on them the
104
+ * pair places a box without measuring it. A free axis has no pin (`null`).
96
105
  */
97
- interface Region {
98
- readonly left: number | null;
99
- readonly right: number | null;
100
- readonly top: number | null;
101
- readonly bottom: number | null;
102
- place(box: ReadonlyBox): Placement;
106
+ interface Region extends IOrigin {
107
+ readonly x: number | null;
108
+ readonly y: number | null;
109
+ place(box: ReadonlyBox): Point;
103
110
  }
104
111
  /**
105
112
  * A {@link Region} driven by writes — for gestures, where the box grows away
@@ -109,6 +116,11 @@ interface Region {
109
116
  declare class MutableRegion implements Region {
110
117
  #private;
111
118
  constructor(boundary: Boundary);
119
+ /** The pin line on each axis — `null` where the axis is free. */
120
+ get x(): number | null;
121
+ get y(): number | null;
122
+ /** The box point that lands on ({@link x}, {@link y}). */
123
+ get origin(): Origin;
112
124
  get left(): number | null;
113
125
  get right(): number | null;
114
126
  get top(): number | null;
@@ -117,7 +129,7 @@ declare class MutableRegion implements Region {
117
129
  set right(v: number | null);
118
130
  set top(v: number | null);
119
131
  set bottom(v: number | null);
120
- place(box: ReadonlyBox): Placement;
132
+ place(box: ReadonlyBox): Point;
121
133
  }
122
134
  //#endregion
123
135
  //#region src/ui/overlay/anchor.d.ts
@@ -162,39 +174,29 @@ type Unordered<A extends string, B extends string> = `${A} ${B}` | `${B} ${A}`;
162
174
  */
163
175
  type PositionAreaValue = AreaKeyword | Unordered<BlockKeyword, InlineKeyword> | Unordered<BlockKeyword, AmbiguousKeyword> | Unordered<InlineKeyword, AmbiguousKeyword> | `${AmbiguousKeyword} ${AmbiguousKeyword}`;
164
176
  /**
165
- * The reactive `position-area` property: the region of the viewport an
166
- * overlay anchored to `anchor` may occupy, with the region's default
167
- * self-alignment as {@link Region.place} outward regions hug the anchor,
168
- * spans go flush against its far edge, center is `anchor-center`. Nothing is
169
- * written; the caller takes what it wants from `place`:
170
- *
171
- * const area = new PositionArea(a, "top span-left");
172
- * effect(() => { overlay.x = area.place(overlay).x; });
177
+ * The reactive `position-area` property: which of the anchor's edges a box
178
+ * pins to, and the self-alignment that follows outward regions hug the
179
+ * anchor's near edge, spans its far edge, center is `anchor-center`. Not a
180
+ * box: a pin per axis, with no geometry of its own.
173
181
  *
174
- * The value is parsed once, at construction; the geometry reads the anchor
175
- * and window on every access, so reading it inside an `effect` tracks both.
182
+ * Parsed once, at construction; the pins read the anchor on every access, so
183
+ * reading them in an `effect` tracks it.
176
184
  *
177
- * Bounded by the window. A tighter bound (a `Constraint`, a scroll container)
178
- * is an intersection with the region, so it composes afterwards rather than
179
- * being a parameter here.
180
- *
181
- * There is no gap parameter, for the same reason CSS has none: the offset off
182
- * the anchor is the overlay's own `margin`.
185
+ * No gap parameter, for the same reason CSS has none: the offset off the
186
+ * anchor is the overlay's own `margin`.
183
187
  */
184
- declare class PositionArea implements Region, ReadonlyBox {
188
+ declare class PositionArea implements Region {
185
189
  #private;
186
190
  readonly anchor: ReadonlyBox;
187
191
  constructor(anchor: ReadonlyBox, area: PositionAreaValue);
192
+ /** The pin lines — the viewport point {@link origin} lands on. */
188
193
  get x(): number;
189
194
  get y(): number;
190
- get w(): number;
191
- get h(): number;
192
- get left(): number | null;
193
- get right(): number | null;
194
- get top(): number | null;
195
- get bottom(): number | null;
195
+ /** The box point landing on ({@link x}, {@link y}). Two axes, not one
196
+ * side: a corner area pins a corner. */
197
+ get origin(): Origin;
196
198
  /** Every axis is pinned, so only the box's size is read. */
197
- place(box: Pick<ReadonlyBox, "w" | "h">): Placement;
199
+ place(box: Pick<ReadonlyBox, "w" | "h">): Point;
198
200
  }
199
201
  //#endregion
200
202
  //#region src/ui/overlay/overlay.d.ts
@@ -224,10 +226,16 @@ declare class Displacement extends PartialBox {
224
226
  apply(): void;
225
227
  clear(): void;
226
228
  }
227
- declare class OverlayBox extends TransformableBox implements IDirection {
229
+ declare class OverlayBox extends TransformableBox implements IDirection, IOrigin {
228
230
  #private;
229
231
  readonly element: HTMLElement;
230
232
  constructor(element: HTMLElement);
233
+ /** The box point that lands on (x, y), and the scale's origin. */
234
+ get origin(): Origin;
235
+ set origin({
236
+ x,
237
+ y
238
+ }: Origin);
231
239
  get w(): number;
232
240
  set w(value: number);
233
241
  get h(): number;
@@ -283,4 +291,4 @@ declare class Motion implements IMotion {
283
291
  abort(initial?: number): void;
284
292
  }
285
293
  //#endregion
286
- export { type Align, type Axis, type BlockSide, type Boundary, ElementBox, gestures_d_exports as Gestures, type IDirection, type IMotion, type InlineSide, type Inset, Motion, MutableRegion, OverlayBox, type PhysicalInset, type Pin, type Placement, PositionArea, type PositionAreaValue, type ReadonlyBox, type Region, WINDOW_BOX, WindowBox, anchor_length };
294
+ export { type Align, type Axis, type BlockSide, type Boundary, ElementBox, gestures_d_exports as Gestures, type IDirection, type IMotion, type IOrigin, type InlineSide, type Inset, Motion, MutableRegion, type Origin, type OriginX, type OriginY, OverlayBox, type PhysicalInset, type Pin, PositionArea, type PositionAreaValue, type ReadonlyBox, type Region, WINDOW_BOX, WindowBox, anchor_length };
@@ -12,10 +12,20 @@ function placePinned(pin, n) {
12
12
  if (pin.align === "end") return pin.at - n;
13
13
  return pin.at - n / 2;
14
14
  }
15
- /** One axis of a {@link Region.place}: pinned, or `own` if the axis is free. */
15
+ /** One axis of {@link Region.place}: pinned, or `own` if free. */
16
16
  function placeAxis(pin, own, n) {
17
17
  return pin ? placePinned(pin, n) : own;
18
18
  }
19
+ /** The box point a pin lands. Physical: direction is resolved before a pin
20
+ * exists, so `left` is left in RTL. A free axis lands the near edge. */
21
+ function originX(pin) {
22
+ if (!pin) return "left";
23
+ return pin.align === "start" ? "left" : pin.align === "end" ? "right" : "center";
24
+ }
25
+ function originY(pin) {
26
+ if (!pin) return "top";
27
+ return pin.align === "start" ? "top" : pin.align === "end" ? "bottom" : "center";
28
+ }
19
29
  /** The inset a pin yields on one edge: its coordinate if it pins that edge. */
20
30
  function inset(pin, edge) {
21
31
  return pin?.align === edge ? pin.at : null;
@@ -35,6 +45,20 @@ var MutableRegion = class {
35
45
  if (top !== void 0) this.top = top;
36
46
  if (bottom !== void 0) this.bottom = bottom;
37
47
  }
48
+ /** The pin line on each axis — `null` where the axis is free. */
49
+ get x() {
50
+ return this.#x()?.at ?? null;
51
+ }
52
+ get y() {
53
+ return this.#y()?.at ?? null;
54
+ }
55
+ /** The box point that lands on ({@link x}, {@link y}). */
56
+ get origin() {
57
+ return {
58
+ x: originX(this.#x()),
59
+ y: originY(this.#y())
60
+ };
61
+ }
38
62
  get left() {
39
63
  return inset(this.#x(), "start");
40
64
  }
@@ -79,47 +103,6 @@ var MutableRegion = class {
79
103
  }
80
104
  };
81
105
  //#endregion
82
- //#region src/ui/overlay/box.ts
83
- var WindowBox = class {
84
- get x() {
85
- return 0;
86
- }
87
- get y() {
88
- return 0;
89
- }
90
- get w() {
91
- return windowSize.width();
92
- }
93
- get h() {
94
- return windowSize.height();
95
- }
96
- get direction() {
97
- return direction();
98
- }
99
- };
100
- const WINDOW_BOX = new WindowBox();
101
- var ElementBox = class {
102
- #rect;
103
- constructor(el) {
104
- this.#rect = createElementRect(el);
105
- }
106
- [Symbol.dispose]() {
107
- this.#rect[Symbol.dispose]();
108
- }
109
- get x() {
110
- return this.#rect().left;
111
- }
112
- get y() {
113
- return this.#rect().top;
114
- }
115
- get w() {
116
- return this.#rect().width;
117
- }
118
- get h() {
119
- return this.#rect().height;
120
- }
121
- };
122
- //#endregion
123
106
  //#region src/ui/overlay/anchor.ts
124
107
  /**
125
108
  * The reactive `anchor()` function: the viewport coordinate of an anchor line
@@ -280,37 +263,17 @@ function resolveArea(area, dir = "ltr", selfDir = dir) {
280
263
  inline: inline ? toPhysical(inline, "inline", dir, selfDir) : "center"
281
264
  };
282
265
  }
283
- /** One axis of the position-area containing block, as `[lo, hi]` viewport
284
- * coordinates. The anchor's two edges cut `[bLo, bHi]` into before/over/after. */
285
- function axisSpan(lo, hi, bLo, bHi, region) {
286
- switch (region) {
287
- case "start": return [bLo, lo];
288
- case "end": return [hi, bHi];
289
- case "center": return [lo, hi];
290
- case "span-start": return [bLo, hi];
291
- case "span-end": return [lo, bHi];
292
- default: return [bLo, bHi];
293
- }
294
- }
295
266
  /**
296
- * The reactive `position-area` property: the region of the viewport an
297
- * overlay anchored to `anchor` may occupy, with the region's default
298
- * self-alignment as {@link Region.place} outward regions hug the anchor,
299
- * spans go flush against its far edge, center is `anchor-center`. Nothing is
300
- * written; the caller takes what it wants from `place`:
301
- *
302
- * const area = new PositionArea(a, "top span-left");
303
- * effect(() => { overlay.x = area.place(overlay).x; });
267
+ * The reactive `position-area` property: which of the anchor's edges a box
268
+ * pins to, and the self-alignment that follows outward regions hug the
269
+ * anchor's near edge, spans its far edge, center is `anchor-center`. Not a
270
+ * box: a pin per axis, with no geometry of its own.
304
271
  *
305
- * The value is parsed once, at construction; the geometry reads the anchor
306
- * and window on every access, so reading it inside an `effect` tracks both.
272
+ * Parsed once, at construction; the pins read the anchor on every access, so
273
+ * reading them in an `effect` tracks it.
307
274
  *
308
- * Bounded by the window. A tighter bound (a `Constraint`, a scroll container)
309
- * is an intersection with the region, so it composes afterwards rather than
310
- * being a parameter here.
311
- *
312
- * There is no gap parameter, for the same reason CSS has none: the offset off
313
- * the anchor is the overlay's own `margin`.
275
+ * No gap parameter, for the same reason CSS has none: the offset off the
276
+ * anchor is the overlay's own `margin`.
314
277
  */
315
278
  var PositionArea = class {
316
279
  anchor;
@@ -319,18 +282,6 @@ var PositionArea = class {
319
282
  this.anchor = anchor;
320
283
  this.#area = resolveArea(area, rootDirection());
321
284
  }
322
- /** The inline axis as `[lo, hi]`, re-read from the anchor and window. */
323
- get #ix() {
324
- const a = this.anchor;
325
- const w = WINDOW_BOX;
326
- return axisSpan(a.x, a.x + a.w, w.x, w.x + w.w, this.#area.inline);
327
- }
328
- /** The block axis as `[lo, hi]`. */
329
- get #iy() {
330
- const a = this.anchor;
331
- const w = WINDOW_BOX;
332
- return axisSpan(a.y, a.y + a.h, w.y, w.y + w.h, this.#area.block);
333
- }
334
285
  get #px() {
335
286
  const a = this.anchor;
336
287
  return pinOf(this.#area.inline, a.x, a.x + a.w);
@@ -339,31 +290,20 @@ var PositionArea = class {
339
290
  const a = this.anchor;
340
291
  return pinOf(this.#area.block, a.y, a.y + a.h);
341
292
  }
293
+ /** The pin lines — the viewport point {@link origin} lands on. */
342
294
  get x() {
343
- return this.#ix[0];
295
+ return this.#px.at;
344
296
  }
345
297
  get y() {
346
- return this.#iy[0];
298
+ return this.#py.at;
347
299
  }
348
- get w() {
349
- const [lo, hi] = this.#ix;
350
- return hi - lo;
351
- }
352
- get h() {
353
- const [lo, hi] = this.#iy;
354
- return hi - lo;
355
- }
356
- get left() {
357
- return inset(this.#px, "start");
358
- }
359
- get right() {
360
- return inset(this.#px, "end");
361
- }
362
- get top() {
363
- return inset(this.#py, "start");
364
- }
365
- get bottom() {
366
- return inset(this.#py, "end");
300
+ /** The box point landing on ({@link x}, {@link y}). Two axes, not one
301
+ * side: a corner area pins a corner. */
302
+ get origin() {
303
+ return {
304
+ x: originX(this.#px),
305
+ y: originY(this.#py)
306
+ };
367
307
  }
368
308
  /** Every axis is pinned, so only the box's size is read. */
369
309
  place(box) {
@@ -401,6 +341,47 @@ function pinOf(region, lo, hi) {
401
341
  }
402
342
  }
403
343
  //#endregion
344
+ //#region src/ui/overlay/box.ts
345
+ var WindowBox = class {
346
+ get x() {
347
+ return 0;
348
+ }
349
+ get y() {
350
+ return 0;
351
+ }
352
+ get w() {
353
+ return windowSize.width();
354
+ }
355
+ get h() {
356
+ return windowSize.height();
357
+ }
358
+ get direction() {
359
+ return direction();
360
+ }
361
+ };
362
+ const WINDOW_BOX = new WindowBox();
363
+ var ElementBox = class {
364
+ #rect;
365
+ constructor(el) {
366
+ this.#rect = createElementRect(el);
367
+ }
368
+ [Symbol.dispose]() {
369
+ this.#rect[Symbol.dispose]();
370
+ }
371
+ get x() {
372
+ return this.#rect().left;
373
+ }
374
+ get y() {
375
+ return this.#rect().top;
376
+ }
377
+ get w() {
378
+ return this.#rect().width;
379
+ }
380
+ get h() {
381
+ return this.#rect().height;
382
+ }
383
+ };
384
+ //#endregion
404
385
  //#region src/ui/overlay/overlay.ts
405
386
  var __create = Object.create;
406
387
  var __defProp = Object.defineProperty;
@@ -560,16 +541,27 @@ var Displacement = class extends PartialBox {
560
541
  });
561
542
  }
562
543
  };
544
+ const OFFSET = {
545
+ left: "0%",
546
+ top: "0%",
547
+ center: "50%",
548
+ right: "100%",
549
+ bottom: "100%"
550
+ };
563
551
  var OverlayBox = class extends TransformableBox {
564
552
  element;
565
553
  #rect;
554
+ #origin = {
555
+ x: "left",
556
+ y: "top"
557
+ };
566
558
  constructor(element) {
567
559
  super();
568
560
  this.element = element;
569
561
  this.#rect = createElementRect(element);
570
562
  element.style.setProperty("top", "0");
571
563
  element.style.setProperty("left", "0");
572
- element.style.setProperty("translate", "calc(var(--x, 0px) + var(--dx, 0px) + var(--_ex, 0px)) calc(var(--y, 0px) + var(--dy, 0px) + var(--_ey, 0px))");
564
+ element.style.setProperty("translate", "calc(var(--x, 0px) + var(--dx, 0px) + var(--_ex, 0px) - var(--_ox, 0%)) calc(var(--y, 0px) + var(--dy, 0px) + var(--_ey, 0px) - var(--_oy, 0%))");
573
565
  const [, stop] = scope(() => {
574
566
  effect(() => {
575
567
  this.#project("--x", this.transform.x);
@@ -595,6 +587,19 @@ var OverlayBox = class extends TransformableBox {
595
587
  this.#rect[Symbol.dispose]();
596
588
  };
597
589
  }
590
+ /** The box point that lands on (x, y), and the scale's origin. */
591
+ get origin() {
592
+ return this.#origin;
593
+ }
594
+ set origin({ x, y }) {
595
+ this.#origin = {
596
+ x,
597
+ y
598
+ };
599
+ this.element.style.setProperty("--_ox", OFFSET[x]);
600
+ this.element.style.setProperty("--_oy", OFFSET[y]);
601
+ this.element.style.transformOrigin = `${x} ${y}`;
602
+ }
598
603
  /** Write a size channel, or unset it when the axis is AUTO (NaN) so the
599
604
  * element falls back to content sizing (`var(--w, auto)`). */
600
605
  #project(name, ...value) {
@@ -2,19 +2,14 @@ import { r as MaybeReactive, t as Computed } from "../index-BSAqa0-0.mjs";
2
2
 
3
3
  //#region src/utilities/element-rect.d.ts
4
4
  /**
5
- * Observes the full bounding rect of `target` and returns it as ONE reactive
6
- * `DOMRect`, so every field a reader sees was measured at the same instant.
7
- * Change sources: a `ResizeObserver` for size, plus capture-phase `scroll` and
8
- * window `resize` for position a `ResizeObserver` stays silent when an
9
- * element merely moves.
5
+ * `target`'s bounding rect as one reactive `DOMRect`, every field from the
6
+ * same instant. Refreshed by a `ResizeObserver` (size) and capture-phase
7
+ * `scroll` / window `resize` (position), never on read. Size is the observer's
8
+ * border box, not the rect's: a transform scales the rect and never fires the
9
+ * observer.
10
10
  *
11
- * `target` may be a getter, in which case the observer follows it: the previous
12
- * element is unobserved before the new one is observed. The returned computed
13
- * keeps its identity across a swap, so consumers never rebind.
14
- *
15
- * The value is CACHED, not measured per read: it refreshes when one of the
16
- * sources above fires, not at the moment you read it. Dispose explicitly, or
17
- * let the enclosing scope do it.
11
+ * A reactive `target` is followed, keeping the computed's identity. Dispose
12
+ * explicitly, or let the enclosing scope do it.
18
13
  */
19
14
  declare function createElementRect(target: MaybeReactive<Element>): Computed<DOMRect> & Disposable;
20
15
  //#endregion
@@ -3,30 +3,38 @@ import { resolve } from "../signals/index.mjs";
3
3
  import { createResizeObserver } from "./resize-observer.mjs";
4
4
  //#region src/utilities/element-rect.ts
5
5
  /**
6
- * Observes the full bounding rect of `target` and returns it as ONE reactive
7
- * `DOMRect`, so every field a reader sees was measured at the same instant.
8
- * Change sources: a `ResizeObserver` for size, plus capture-phase `scroll` and
9
- * window `resize` for position a `ResizeObserver` stays silent when an
10
- * element merely moves.
6
+ * `target`'s bounding rect as one reactive `DOMRect`, every field from the
7
+ * same instant. Refreshed by a `ResizeObserver` (size) and capture-phase
8
+ * `scroll` / window `resize` (position), never on read. Size is the observer's
9
+ * border box, not the rect's: a transform scales the rect and never fires the
10
+ * observer.
11
11
  *
12
- * `target` may be a getter, in which case the observer follows it: the previous
13
- * element is unobserved before the new one is observed. The returned computed
14
- * keeps its identity across a swap, so consumers never rebind.
15
- *
16
- * The value is CACHED, not measured per read: it refreshes when one of the
17
- * sources above fires, not at the moment you read it. Dispose explicitly, or
18
- * let the enclosing scope do it.
12
+ * A reactive `target` is followed, keeping the computed's identity. Dispose
13
+ * explicitly, or let the enclosing scope do it.
19
14
  */
20
15
  function createElementRect(target) {
21
- const cache = signal(resolve(target).getBoundingClientRect());
16
+ let size;
17
+ const read = (el) => {
18
+ const r = el.getBoundingClientRect();
19
+ return size ? new DOMRect(r.x, r.y, size.width, size.height) : r;
20
+ };
21
+ const cache = signal(read(resolve(target)));
22
22
  const updateRect = (el) => {
23
- cache(el.getBoundingClientRect());
23
+ cache(read(el));
24
24
  };
25
25
  const stop = effectScope(() => {
26
26
  effect(() => {
27
27
  const el = resolve(target);
28
+ size = void 0;
28
29
  createResizeObserver(el, (entries) => {
29
- for (const entry of entries) updateRect(entry.target);
30
+ for (const entry of entries) {
31
+ const box = entry.borderBoxSize?.[0];
32
+ if (box) size = {
33
+ width: box.inlineSize,
34
+ height: box.blockSize
35
+ };
36
+ updateRect(entry.target);
37
+ }
30
38
  });
31
39
  const remeasure = () => updateRect(el);
32
40
  window.addEventListener("scroll", remeasure, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "elements-kit",
3
3
  "type": "module",
4
- "version": "0.27.9",
4
+ "version": "0.27.11",
5
5
  "description": "A lightweight reactive UI library that transforms native HTMLElements into reactive components with signals. Ideal for framework-agnostic applications and web components.",
6
6
  "keywords": [
7
7
  "webcomponents",