@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 +21 -0
- package/README.md +73 -2
- package/dist/align.d.ts +76 -0
- package/dist/align.js +321 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +524 -0
- package/dist/measure.d.ts +70 -0
- package/dist/measure.js +274 -0
- package/dist/place.d.ts +45 -0
- package/dist/place.js +274 -0
- package/dist/rect.d.ts +27 -0
- package/dist/rect.js +147 -0
- package/dist/types-PJD4F9tg.d.ts +52 -0
- package/package.json +84 -5
package/dist/rect.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
|
|
10
|
+
// src/rect/index.ts
|
|
11
|
+
function rectOf(target, options = {}) {
|
|
12
|
+
const { box = "border", space = "viewport" } = options;
|
|
13
|
+
const t = classify(target);
|
|
14
|
+
switch (t.kind) {
|
|
15
|
+
case "rect":
|
|
16
|
+
return { ...t.rect };
|
|
17
|
+
case "viewport":
|
|
18
|
+
return moveSpace(
|
|
19
|
+
{
|
|
20
|
+
x: 0,
|
|
21
|
+
y: 0,
|
|
22
|
+
width: window.innerWidth,
|
|
23
|
+
height: window.innerHeight
|
|
24
|
+
},
|
|
25
|
+
"viewport",
|
|
26
|
+
space
|
|
27
|
+
);
|
|
28
|
+
case "element":
|
|
29
|
+
return elementRect(t.el, box, space);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function classify(target) {
|
|
33
|
+
if (target === "viewport" || target === window) return { kind: "viewport" };
|
|
34
|
+
if (typeof target === "string") {
|
|
35
|
+
const el = document.querySelector(target);
|
|
36
|
+
if (!el)
|
|
37
|
+
throw new Error(`calipers: no element matches selector "${target}"`);
|
|
38
|
+
return { kind: "element", el };
|
|
39
|
+
}
|
|
40
|
+
if (isElement(target)) return { kind: "element", el: target };
|
|
41
|
+
if (isRectLike(target)) return { kind: "rect", rect: target };
|
|
42
|
+
if (isPointLike(target))
|
|
43
|
+
return {
|
|
44
|
+
kind: "rect",
|
|
45
|
+
rect: { x: target.x, y: target.y, width: 0, height: 0 }
|
|
46
|
+
};
|
|
47
|
+
const first = target[0];
|
|
48
|
+
if (first == null)
|
|
49
|
+
throw new Error("calipers: empty list has no measurable to resolve");
|
|
50
|
+
return classify(first);
|
|
51
|
+
}
|
|
52
|
+
function isPointLike(v) {
|
|
53
|
+
return typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number" && typeof v.width !== "number";
|
|
54
|
+
}
|
|
55
|
+
function isRectLike(v) {
|
|
56
|
+
return typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number" && typeof v.width === "number" && typeof v.height === "number";
|
|
57
|
+
}
|
|
58
|
+
function isSVGGraphics(el) {
|
|
59
|
+
return typeof SVGGraphicsElement !== "undefined" && el instanceof SVGGraphicsElement;
|
|
60
|
+
}
|
|
61
|
+
function elementRect(el, box, space) {
|
|
62
|
+
return isSVGGraphics(el) ? svgRect(el, box, space) : htmlRect(el, box, space);
|
|
63
|
+
}
|
|
64
|
+
function htmlRect(el, box, space) {
|
|
65
|
+
if (box === "visual") {
|
|
66
|
+
const r = el.getBoundingClientRect();
|
|
67
|
+
return moveSpace(
|
|
68
|
+
{ x: r.x, y: r.y, width: r.width, height: r.height },
|
|
69
|
+
"viewport",
|
|
70
|
+
space
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const border = layoutBorderBox(el);
|
|
74
|
+
const cs = getComputedStyle(el);
|
|
75
|
+
let rect;
|
|
76
|
+
if (box === "content") {
|
|
77
|
+
const bl = px(cs.borderLeftWidth);
|
|
78
|
+
const br = px(cs.borderRightWidth);
|
|
79
|
+
const bt = px(cs.borderTopWidth);
|
|
80
|
+
const bb = px(cs.borderBottomWidth);
|
|
81
|
+
const pl = px(cs.paddingLeft);
|
|
82
|
+
const pr = px(cs.paddingRight);
|
|
83
|
+
const pt = px(cs.paddingTop);
|
|
84
|
+
const pb = px(cs.paddingBottom);
|
|
85
|
+
rect = {
|
|
86
|
+
x: border.x + bl + pl,
|
|
87
|
+
y: border.y + bt + pt,
|
|
88
|
+
width: Math.max(0, border.width - bl - br - pl - pr),
|
|
89
|
+
height: Math.max(0, border.height - bt - bb - pt - pb)
|
|
90
|
+
};
|
|
91
|
+
} else if (box === "margin") {
|
|
92
|
+
const ml = px(cs.marginLeft);
|
|
93
|
+
const mr = px(cs.marginRight);
|
|
94
|
+
const mt = px(cs.marginTop);
|
|
95
|
+
const mb = px(cs.marginBottom);
|
|
96
|
+
rect = {
|
|
97
|
+
x: border.x - ml,
|
|
98
|
+
y: border.y - mt,
|
|
99
|
+
width: border.width + ml + mr,
|
|
100
|
+
height: border.height + mt + mb
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
rect = border;
|
|
104
|
+
}
|
|
105
|
+
return moveSpace(rect, "document", space);
|
|
106
|
+
}
|
|
107
|
+
function svgRect(el, box, space) {
|
|
108
|
+
if (box === "bbox") {
|
|
109
|
+
const b = el.getBBox();
|
|
110
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
111
|
+
}
|
|
112
|
+
const r = el.getBoundingClientRect();
|
|
113
|
+
return moveSpace(
|
|
114
|
+
{ x: r.x, y: r.y, width: r.width, height: r.height },
|
|
115
|
+
"viewport",
|
|
116
|
+
space
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function layoutBorderBox(el) {
|
|
120
|
+
let x = 0;
|
|
121
|
+
let y = 0;
|
|
122
|
+
let node = el;
|
|
123
|
+
const fixed = getComputedStyle(el).position === "fixed";
|
|
124
|
+
while (node) {
|
|
125
|
+
x += node.offsetLeft;
|
|
126
|
+
y += node.offsetTop;
|
|
127
|
+
node = node.offsetParent;
|
|
128
|
+
}
|
|
129
|
+
if (fixed) {
|
|
130
|
+
x += window.scrollX;
|
|
131
|
+
y += window.scrollY;
|
|
132
|
+
}
|
|
133
|
+
return { x, y, width: el.offsetWidth, height: el.offsetHeight };
|
|
134
|
+
}
|
|
135
|
+
function moveSpace(rect, from, to) {
|
|
136
|
+
if (from === to) return rect;
|
|
137
|
+
const dx = from === "viewport" ? window.scrollX : -window.scrollX;
|
|
138
|
+
const dy = from === "viewport" ? window.scrollY : -window.scrollY;
|
|
139
|
+
return {
|
|
140
|
+
x: rect.x + dx,
|
|
141
|
+
y: rect.y + dy,
|
|
142
|
+
width: rect.width,
|
|
143
|
+
height: rect.height
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { rectOf };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared core types for calipers.
|
|
3
|
+
* See docs/plans/calipers-plan.md §4–§6.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A 2D point. In `Anchor` position the coordinates are fractions (0–1) of the
|
|
7
|
+
* rect; everywhere else they are absolute coordinates in the active space.
|
|
8
|
+
*/
|
|
9
|
+
interface Point {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
/** A normalized rectangle in a named coordinate space. */
|
|
14
|
+
interface Rect {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Which box of an element is measured.
|
|
22
|
+
* - `visual` includes CSS transforms (what the user actually sees).
|
|
23
|
+
* - `bbox` is for SVG elements: `getBBox()` in local user units, untransformed.
|
|
24
|
+
*/
|
|
25
|
+
type Box = 'content' | 'border' | 'margin' | 'visual' | 'bbox';
|
|
26
|
+
/** The coordinate space the numbers live in. */
|
|
27
|
+
type Space = 'viewport' | 'document';
|
|
28
|
+
/**
|
|
29
|
+
* A per-axis gap in px. A single number applies to both axes;
|
|
30
|
+
* `[x, y]` / `{ x, y }` set each axis separately. Negative values overlap.
|
|
31
|
+
*/
|
|
32
|
+
type Gap = number | readonly [number, number] | {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
};
|
|
36
|
+
/** Anything the library can measure or place relative to. A `Point` resolves to a zero-size rect at that point. */
|
|
37
|
+
type Measurable = string | Element | ArrayLike<Element | string | Rect> | Window | 'viewport' | Point | Rect;
|
|
38
|
+
/**
|
|
39
|
+
* The 13 classic anchor codes — sugar over fractional points
|
|
40
|
+
* (e.g. `tl` ≡ `{ x: 0, y: 0 }`, `cc` ≡ `{ x: 0.5, y: 0.5 }`).
|
|
41
|
+
*/
|
|
42
|
+
type AnchorCode = 'tl' | 'tc' | 'tr' | 'cl' | 'cc' | 'cr' | 'bl' | 'bc' | 'br' | 't' | 'b' | 'l' | 'r';
|
|
43
|
+
/**
|
|
44
|
+
* An anchor resolves to exactly one point of a rect.
|
|
45
|
+
* `{ x, y }` / `[x, y]` are fractions (0–1) of the rect's width/height.
|
|
46
|
+
*/
|
|
47
|
+
type Anchor = AnchorCode | {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
} | [number, number] | ((rect: Rect) => Point);
|
|
51
|
+
|
|
52
|
+
export type { Anchor as A, Box as B, Gap as G, Measurable as M, Point as P, Rect as R, Space as S, AnchorCode as a };
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"author": {
|
|
3
|
+
"name": "Samir Damle",
|
|
4
|
+
"url": "https://github.com/samirdamle"
|
|
5
|
+
},
|
|
6
|
+
"description": "Tiny, dependency-free geometry toolkit for the web: measure, align, place, and design with HTML and SVG elements.",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
9
|
+
"@biomejs/biome": "^2.2.0",
|
|
10
|
+
"esbuild": "^0.25.0",
|
|
11
|
+
"happy-dom": "^18.0.1",
|
|
12
|
+
"tsup": "^8.5.0",
|
|
13
|
+
"typescript": "~5.9.2",
|
|
14
|
+
"vitest": "^3.2.4"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./align": {
|
|
25
|
+
"types": "./dist/align.d.ts",
|
|
26
|
+
"default": "./dist/align.js"
|
|
27
|
+
},
|
|
28
|
+
"./measure": {
|
|
29
|
+
"types": "./dist/measure.d.ts",
|
|
30
|
+
"default": "./dist/measure.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json",
|
|
33
|
+
"./place": {
|
|
34
|
+
"types": "./dist/place.d.ts",
|
|
35
|
+
"default": "./dist/place.js"
|
|
36
|
+
},
|
|
37
|
+
"./rect": {
|
|
38
|
+
"types": "./dist/rect.d.ts",
|
|
39
|
+
"default": "./dist/rect.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"keywords": [
|
|
46
|
+
"geometry",
|
|
47
|
+
"rect",
|
|
48
|
+
"position",
|
|
49
|
+
"align",
|
|
50
|
+
"measure",
|
|
51
|
+
"svg",
|
|
52
|
+
"html",
|
|
53
|
+
"util",
|
|
54
|
+
"layout",
|
|
55
|
+
"viewport",
|
|
56
|
+
"responsive"
|
|
57
|
+
],
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"main": "./dist/index.js",
|
|
60
|
+
"name": "@samirdamle/calipers",
|
|
61
|
+
"packageManager": "pnpm@12.8.1",
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "https://github.com/samirdamle/calipers.git"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"attw": "attw --pack --profile esm-only",
|
|
71
|
+
"build": "tsup",
|
|
72
|
+
"ci": "pnpm lint && pnpm typecheck && pnpm build && pnpm test && pnpm size",
|
|
73
|
+
"demo": "pnpm build && node scripts/serve.mjs .",
|
|
74
|
+
"lint": "biome check .",
|
|
75
|
+
"lint:fix": "biome check --write .",
|
|
76
|
+
"prepublishOnly": "pnpm build",
|
|
77
|
+
"size": "node scripts/check-size.mjs",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"typecheck": "tsc --noEmit"
|
|
80
|
+
},
|
|
81
|
+
"sideEffects": false,
|
|
82
|
+
"type": "module",
|
|
83
|
+
"types": "./dist/index.d.ts",
|
|
84
|
+
"version": "0.1.0"
|
|
85
|
+
}
|