@samirdamle/calipers 0.0.0-stage → 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samir Damle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # calipers
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A tiny, dependency-free JavaScript geometry toolkit for the web — measure, align, place, and design with HTML and SVG elements.
4
+
5
+ ```js
6
+ import { measure, place } from '@samirdamle/calipers';
7
+
8
+ // How far is the card from the hero?
9
+ measure('.card', '.hero', { anchor: 'tl', ofAnchor: 'bl', fields: ['dy', 'gapY'] });
10
+ // → { pointA: {…}, pointB: {…}, dy: -24, gapY: 24 }
11
+
12
+ // Park the tooltip's top-center 8px below the button's bottom-center.
13
+ place('.tooltip', {
14
+ anchor: 'tc',
15
+ at: { anchor: 'bc', of: '.button' },
16
+ offset: { y: 8 },
17
+ });
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm i @samirdamle/calipers
24
+ # pnpm add @samirdamle/calipers
25
+ ```
26
+
27
+ Import only what you use — every module is a separate, tree-shakeable entry point:
28
+
29
+ ```js
30
+ import { rectOf } from '@samirdamle/calipers/rect';
31
+ import { measure, calipers, anchorPoint } from '@samirdamle/calipers/measure';
32
+ import { place } from '@samirdamle/calipers/place';
33
+ ```
34
+
35
+ ## Modules
36
+
37
+ | Import | Does | Size (min+br) |
38
+ |---|---|---|
39
+ | `@samirdamle/calipers/rect` | `rectOf` — resolve anything measurable into a rect, with an explicit box model and coordinate space | 863 B |
40
+ | `@samirdamle/calipers/measure` | `measure` / `calipers` — pairwise geometry, plus a DOM-free pure core | 1723 B |
41
+ | `@samirdamle/calipers/place` | `place` — position an element relative to a point or another element | 1444 B |
42
+ | `@samirdamle/calipers/align` | `align` / `distribute` / `stack` — design-tool alignment, even spacing, and stacking | 1890 B |
43
+
44
+ Sizes are measured from the actual build (see `size-budgets.json`); a stale figure is a bug.
45
+
46
+ `fit` and the dev-only `debug` overlay are on the roadmap (plan §9–§10).
47
+
48
+ ## Anchors
49
+
50
+ Every anchor resolves to exactly one point. The 13 codes are sugar over fractional points — `{ x: 0.25, y: 0 }` and `(rect) => point` work too.
51
+
52
+ ```
53
+ tl ─── tc ─── tr
54
+ │ │
55
+ cl ─── cc ─── cr
56
+ │ │
57
+ bl ─── bc ─── br
58
+ ```
59
+
60
+ `t`/`b`/`l`/`r` are the edge midpoints (`tc`/`bc`/`cl`/`cr`).
61
+
62
+ ## The one rule
63
+
64
+ Every number names its **box** (`content` | `border` | `margin` | `visual` | `bbox`) and its **coordinate space** (`viewport` | `document`). No silent defaults that change meaning on scroll or under transforms.
65
+
66
+ ## Docs
67
+
68
+ - [API reference](docs/API.md) — every function, field, and option
69
+ - [Playground demo](https://samirdamle.github.io/calipers/demo/) — drag boxes, watch measurements update (or `pnpm demo` locally)
70
+ - [Library plan](docs/plans/calipers-plan.md) and [goal](docs/meta/goal.md)
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,76 @@
1
+ import { M as Measurable, B as Box, G as Gap } from './types-PJD4F9tg.js';
2
+
3
+ type AlignEdge = 'left' | 'right' | 'top' | 'bottom' | 'center-x' | 'center-y';
4
+ interface AlignOptions {
5
+ /**
6
+ * Reference to align to. Omit to align to the selection's extreme
7
+ * (edges) or mean (centers), design-tool style.
8
+ */
9
+ to?: Measurable;
10
+ /** Box model for measurement. @default 'border' */
11
+ box?: Box;
12
+ }
13
+ type DistributeAxis = 'x' | 'y';
14
+ interface DistributeOptions {
15
+ /** Fixed gap in px. Omit for even spacing across the selection span. */
16
+ gap?: number;
17
+ /** Box model for measurement. @default 'border' */
18
+ box?: Box;
19
+ }
20
+ /**
21
+ * Align elements along one edge or centerline, moving each via the CSS
22
+ * `translate` property (accumulates across calls, composes with `transform`).
23
+ * Measurement includes each element's existing translate, so repeating the
24
+ * same call is a no-op instead of re-applying the delta.
25
+ */
26
+ declare function align(els: ArrayLike<Element>, edge: AlignEdge, options?: AlignOptions): void;
27
+ /**
28
+ * Spread elements evenly along an axis, in positional order, moving each via
29
+ * the CSS `translate` property. No-op for fewer than two elements.
30
+ * Measurement includes each element's existing translate, so repeating the
31
+ * same call is a no-op instead of re-applying the deltas.
32
+ */
33
+ declare function distribute(els: ArrayLike<Element>, axis: DistributeAxis, options?: DistributeOptions): void;
34
+ /** Which way a stack grows. */
35
+ type StackDirection = 't' | 'b' | 'l' | 'r';
36
+ /**
37
+ * Lateral alignment of stacked boxes, relative to the anchor box. Only the
38
+ * axis perpendicular to `direction` applies; anything else centers.
39
+ */
40
+ type StackAlign = 't' | 'b' | 'l' | 'r' | 'c';
41
+ interface StackOptions {
42
+ /**
43
+ * Reference to stack from (the source). Omit to stack from the selection's
44
+ * trailing extreme in the stacking direction — that element stays put and
45
+ * the rest pile after it.
46
+ */
47
+ to?: Measurable;
48
+ /** Which way the stack grows. @default 'b' */
49
+ direction?: StackDirection;
50
+ /**
51
+ * Gap between consecutive boxes. A single number is the axial separation
52
+ * (as before); `[x, y]` / `{ x, y }` keeps the axial meaning on the
53
+ * stacking axis and adds a per-step lateral cascade on the other —
54
+ * e.g. `gap: { x: 12, y: 10 }` with `direction: 'b'` stacks boxes 10px
55
+ * apart, each 12px further right: a staircase. @default 0
56
+ */
57
+ gap?: Gap;
58
+ /**
59
+ * Lateral alignment relative to the anchor box. @default 'c'
60
+ */
61
+ align?: StackAlign;
62
+ /** Box model for measurement. @default 'border' */
63
+ box?: Box;
64
+ }
65
+ /**
66
+ * Stack elements one after another from a reference, moving each via the CSS
67
+ * `translate` property (accumulates across calls, composes with `transform`).
68
+ * Targets are ordered nearest-the-anchor first along the stacking axis, so
69
+ * e.g. `stack(targets, { to: source, direction: 't', gap: 10, align: 'c' })`
70
+ * piles them upward from the source with 10px between consecutive boxes,
71
+ * center-aligned. Measurement includes each element's existing translate, so
72
+ * repeating the same call is a no-op.
73
+ */
74
+ declare function stack(els: ArrayLike<Element>, options?: StackOptions): void;
75
+
76
+ export { type AlignEdge, type AlignOptions, type DistributeAxis, type DistributeOptions, type StackAlign, type StackDirection, type StackOptions, align, distribute, stack };
package/dist/align.js ADDED
@@ -0,0 +1,321 @@
1
+ // src/internal/dom.ts
2
+ function isElement(v) {
3
+ return typeof Element !== "undefined" && v instanceof Element;
4
+ }
5
+ function px(value) {
6
+ const n = parseFloat(value);
7
+ return Number.isFinite(n) ? n : 0;
8
+ }
9
+ function parseTranslate(value) {
10
+ if (!value || value === "none") return { x: 0, y: 0 };
11
+ const [x = "0", y = "0"] = value.trim().split(/\s+/);
12
+ return { x: px(x), y: px(y) };
13
+ }
14
+ function getTranslate(el) {
15
+ return parseTranslate(getComputedStyle(el).translate);
16
+ }
17
+ function shiftTranslate(el, prev, dx, dy) {
18
+ if (dx === 0 && dy === 0) return;
19
+ el.style.setProperty("translate", `${prev.x + dx}px ${prev.y + dy}px`);
20
+ }
21
+
22
+ // src/internal/gap.ts
23
+ function normalizeGap(gap) {
24
+ if (gap === void 0) return { x: 0, y: 0 };
25
+ if (typeof gap === "number") return { x: gap, y: gap };
26
+ if ("x" in gap) return { x: gap.x, y: gap.y };
27
+ return { x: gap[0], y: gap[1] };
28
+ }
29
+
30
+ // src/rect/index.ts
31
+ function rectOf(target, options = {}) {
32
+ const { box = "border", space = "viewport" } = options;
33
+ const t = classify(target);
34
+ switch (t.kind) {
35
+ case "rect":
36
+ return { ...t.rect };
37
+ case "viewport":
38
+ return moveSpace(
39
+ {
40
+ x: 0,
41
+ y: 0,
42
+ width: window.innerWidth,
43
+ height: window.innerHeight
44
+ },
45
+ "viewport",
46
+ space
47
+ );
48
+ case "element":
49
+ return elementRect(t.el, box, space);
50
+ }
51
+ }
52
+ function classify(target) {
53
+ if (target === "viewport" || target === window) return { kind: "viewport" };
54
+ if (typeof target === "string") {
55
+ const el = document.querySelector(target);
56
+ if (!el)
57
+ throw new Error(`calipers: no element matches selector "${target}"`);
58
+ return { kind: "element", el };
59
+ }
60
+ if (isElement(target)) return { kind: "element", el: target };
61
+ if (isRectLike(target)) return { kind: "rect", rect: target };
62
+ if (isPointLike(target))
63
+ return {
64
+ kind: "rect",
65
+ rect: { x: target.x, y: target.y, width: 0, height: 0 }
66
+ };
67
+ const first = target[0];
68
+ if (first == null)
69
+ throw new Error("calipers: empty list has no measurable to resolve");
70
+ return classify(first);
71
+ }
72
+ function isPointLike(v) {
73
+ return typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number" && typeof v.width !== "number";
74
+ }
75
+ function isRectLike(v) {
76
+ return typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number" && typeof v.width === "number" && typeof v.height === "number";
77
+ }
78
+ function isSVGGraphics(el) {
79
+ return typeof SVGGraphicsElement !== "undefined" && el instanceof SVGGraphicsElement;
80
+ }
81
+ function elementRect(el, box, space) {
82
+ return isSVGGraphics(el) ? svgRect(el, box, space) : htmlRect(el, box, space);
83
+ }
84
+ function htmlRect(el, box, space) {
85
+ if (box === "visual") {
86
+ const r = el.getBoundingClientRect();
87
+ return moveSpace(
88
+ { x: r.x, y: r.y, width: r.width, height: r.height },
89
+ "viewport",
90
+ space
91
+ );
92
+ }
93
+ const border = layoutBorderBox(el);
94
+ const cs = getComputedStyle(el);
95
+ let rect;
96
+ if (box === "content") {
97
+ const bl = px(cs.borderLeftWidth);
98
+ const br = px(cs.borderRightWidth);
99
+ const bt = px(cs.borderTopWidth);
100
+ const bb = px(cs.borderBottomWidth);
101
+ const pl = px(cs.paddingLeft);
102
+ const pr = px(cs.paddingRight);
103
+ const pt = px(cs.paddingTop);
104
+ const pb = px(cs.paddingBottom);
105
+ rect = {
106
+ x: border.x + bl + pl,
107
+ y: border.y + bt + pt,
108
+ width: Math.max(0, border.width - bl - br - pl - pr),
109
+ height: Math.max(0, border.height - bt - bb - pt - pb)
110
+ };
111
+ } else if (box === "margin") {
112
+ const ml = px(cs.marginLeft);
113
+ const mr = px(cs.marginRight);
114
+ const mt = px(cs.marginTop);
115
+ const mb = px(cs.marginBottom);
116
+ rect = {
117
+ x: border.x - ml,
118
+ y: border.y - mt,
119
+ width: border.width + ml + mr,
120
+ height: border.height + mt + mb
121
+ };
122
+ } else {
123
+ rect = border;
124
+ }
125
+ return moveSpace(rect, "document", space);
126
+ }
127
+ function svgRect(el, box, space) {
128
+ if (box === "bbox") {
129
+ const b = el.getBBox();
130
+ return { x: b.x, y: b.y, width: b.width, height: b.height };
131
+ }
132
+ const r = el.getBoundingClientRect();
133
+ return moveSpace(
134
+ { x: r.x, y: r.y, width: r.width, height: r.height },
135
+ "viewport",
136
+ space
137
+ );
138
+ }
139
+ function layoutBorderBox(el) {
140
+ let x = 0;
141
+ let y = 0;
142
+ let node = el;
143
+ const fixed = getComputedStyle(el).position === "fixed";
144
+ while (node) {
145
+ x += node.offsetLeft;
146
+ y += node.offsetTop;
147
+ node = node.offsetParent;
148
+ }
149
+ if (fixed) {
150
+ x += window.scrollX;
151
+ y += window.scrollY;
152
+ }
153
+ return { x, y, width: el.offsetWidth, height: el.offsetHeight };
154
+ }
155
+ function moveSpace(rect, from, to) {
156
+ if (from === to) return rect;
157
+ const dx = from === "viewport" ? window.scrollX : -window.scrollX;
158
+ const dy = from === "viewport" ? window.scrollY : -window.scrollY;
159
+ return {
160
+ x: rect.x + dx,
161
+ y: rect.y + dy,
162
+ width: rect.width,
163
+ height: rect.height
164
+ };
165
+ }
166
+
167
+ // src/internal/rendered.ts
168
+ function rendered(el, box) {
169
+ const rect = rectOf(el, { box, space: "viewport" });
170
+ const translate = getTranslate(el);
171
+ if (box === "visual" || translate.x === 0 && translate.y === 0) {
172
+ return { rect, translate };
173
+ }
174
+ return {
175
+ rect: { ...rect, x: rect.x + translate.x, y: rect.y + translate.y },
176
+ translate
177
+ };
178
+ }
179
+
180
+ // src/align/index.ts
181
+ var pos = (r, ax) => ax === "x" ? r.x : r.y;
182
+ var size = (r, ax) => ax === "x" ? r.width : r.height;
183
+ var other = (ax) => ax === "x" ? "y" : "x";
184
+ function toRect(to, box) {
185
+ return isElement(to) ? rendered(to, box).rect : rectOf(to, { box, space: "viewport" });
186
+ }
187
+ function mean(vals) {
188
+ return vals.reduce((a, b) => a + b, 0) / vals.length;
189
+ }
190
+ function align(els, edge, options = {}) {
191
+ const list = Array.from(els);
192
+ if (list.length === 0) return;
193
+ const box = options.box ?? "border";
194
+ const ax = edge === "left" || edge === "right" || edge === "center-x" ? "x" : "y";
195
+ const items = list.map((el) => ({ el, ...rendered(el, box) }));
196
+ const ref = options.to === void 0 ? null : toRect(options.to, box);
197
+ const line = ref !== null ? edgePos(edge, ref) : aggregate(
198
+ edge,
199
+ items.map((it) => edgePos(edge, it.rect))
200
+ );
201
+ for (const { el, rect, translate } of items) {
202
+ const delta = line - edgePos(edge, rect);
203
+ shiftTranslate(
204
+ el,
205
+ translate,
206
+ ax === "x" ? delta : 0,
207
+ ax === "y" ? delta : 0
208
+ );
209
+ }
210
+ }
211
+ function edgePos(edge, r) {
212
+ switch (edge) {
213
+ case "left":
214
+ return r.x;
215
+ case "right":
216
+ return r.x + r.width;
217
+ case "center-x":
218
+ return r.x + r.width / 2;
219
+ case "top":
220
+ return r.y;
221
+ case "bottom":
222
+ return r.y + r.height;
223
+ case "center-y":
224
+ return r.y + r.height / 2;
225
+ }
226
+ }
227
+ function aggregate(edge, vals) {
228
+ if (edge === "center-x" || edge === "center-y") return mean(vals);
229
+ return edge === "left" || edge === "top" ? Math.min(...vals) : Math.max(...vals);
230
+ }
231
+ function distribute(els, axis, options = {}) {
232
+ const list = Array.from(els);
233
+ if (list.length < 2) return;
234
+ const box = options.box ?? "border";
235
+ const ax = axis;
236
+ const items = list.map((el) => ({ el, ...rendered(el, box) })).sort((a, b) => pos(a.rect, ax) - pos(b.rect, ax));
237
+ const first = items[0].rect;
238
+ const last = items[items.length - 1].rect;
239
+ const span = pos(last, ax) + size(last, ax) - pos(first, ax);
240
+ const sizes = items.reduce((sum, it) => sum + size(it.rect, ax), 0);
241
+ const gap = options.gap ?? (span - sizes) / (items.length - 1);
242
+ let cursor = pos(first, ax);
243
+ for (const { el, rect, translate } of items) {
244
+ const delta = cursor - pos(rect, ax);
245
+ shiftTranslate(
246
+ el,
247
+ translate,
248
+ ax === "x" ? delta : 0,
249
+ ax === "y" ? delta : 0
250
+ );
251
+ cursor += size(rect, ax) + gap;
252
+ }
253
+ }
254
+ function stack(els, options = {}) {
255
+ const list = Array.from(els);
256
+ if (list.length === 0) return;
257
+ const box = options.box ?? "border";
258
+ const direction = options.direction ?? "b";
259
+ const lateral = options.align ?? "c";
260
+ const ax = direction === "t" || direction === "b" ? "y" : "x";
261
+ const forward = direction === "b" || direction === "r";
262
+ const gap = normalizeGap(options.gap);
263
+ const gapAx = ax === "x" ? gap.x : gap.y;
264
+ const gapLat = typeof options.gap === "number" || options.gap === void 0 ? 0 : ax === "x" ? gap.y : gap.x;
265
+ const items = list.map((el) => ({ el, ...rendered(el, box) }));
266
+ let anchorEl;
267
+ let anchorRect;
268
+ if (options.to !== void 0) {
269
+ const to = options.to;
270
+ anchorEl = isElement(to) ? to : null;
271
+ anchorRect = toRect(to, box);
272
+ } else {
273
+ anchorEl = items[0].el;
274
+ anchorRect = items[0].rect;
275
+ for (const it of items) {
276
+ if (moreTrailing(it.rect, anchorRect, ax, forward)) {
277
+ anchorEl = it.el;
278
+ anchorRect = it.rect;
279
+ }
280
+ }
281
+ }
282
+ const targets = items.filter((it) => it.el !== anchorEl).sort(
283
+ (a, b) => forward ? pos(a.rect, ax) - pos(b.rect, ax) : pos(b.rect, ax) + size(b.rect, ax) - (pos(a.rect, ax) + size(a.rect, ax))
284
+ );
285
+ if (targets.length === 0) return;
286
+ let cursor = forward ? pos(anchorRect, ax) + size(anchorRect, ax) + gapAx : pos(anchorRect, ax) - gapAx;
287
+ let step = 0;
288
+ for (const { el, rect, translate } of targets) {
289
+ step += 1;
290
+ const s = size(rect, ax);
291
+ const slot = forward ? cursor : cursor - s;
292
+ const delta = slot - pos(rect, ax);
293
+ const lat = lateralOffset(lateral, anchorRect, rect, ax) + step * gapLat;
294
+ shiftTranslate(
295
+ el,
296
+ translate,
297
+ ax === "x" ? delta : lat,
298
+ ax === "y" ? delta : lat
299
+ );
300
+ cursor = forward ? slot + s + gapAx : slot - gapAx;
301
+ }
302
+ }
303
+ function moreTrailing(r, best, ax, forward) {
304
+ const e = forward ? pos(r, ax) : pos(r, ax) + size(r, ax);
305
+ const b = forward ? pos(best, ax) : pos(best, ax) + size(best, ax);
306
+ return forward ? e < b : e > b;
307
+ }
308
+ function lateralOffset(lateral, anchor, r, ax) {
309
+ const p = other(ax);
310
+ const leading = p === "x" ? "l" : "t";
311
+ const trailing = p === "x" ? "r" : "b";
312
+ const a0 = pos(anchor, p);
313
+ const a1 = a0 + size(anchor, p);
314
+ const r0 = pos(r, p);
315
+ const r1 = r0 + size(r, p);
316
+ if (lateral === leading) return a0 - r0;
317
+ if (lateral === trailing) return a1 - r1;
318
+ return (a0 + a1) / 2 - (r0 + r1) / 2;
319
+ }
320
+
321
+ export { align, distribute, stack };
@@ -0,0 +1,5 @@
1
+ export { AlignEdge, AlignOptions, DistributeAxis, DistributeOptions, StackAlign, StackDirection, StackOptions, align, distribute, stack } from './align.js';
2
+ export { Field, MeasureOptions, MeasureResult, anchorPoint, angleBetween, calipers, containsRect, distanceBetween, gapBetween, measure, overlapArea, overlaps } from './measure.js';
3
+ export { PlaceAt, PlaceOptions, place } from './place.js';
4
+ export { RectOptions, rectOf } from './rect.js';
5
+ export { A as Anchor, a as AnchorCode, B as Box, G as Gap, M as Measurable, P as Point, R as Rect, S as Space } from './types-PJD4F9tg.js';