@camp.dev/bones 0.2.0 → 0.4.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/README.md +47 -20
- package/dist/css/auto.css +40 -27
- package/dist/css/bones.css +68 -27
- package/dist/element/boundary.d.mts +26 -0
- package/dist/element/boundary.mjs +220 -0
- package/dist/element/index.d.mts +14 -0
- package/dist/element/index.mjs +6 -0
- package/dist/element/measure.mjs +99 -0
- package/dist/element/overlay.mjs +184 -0
- package/dist/react/boundary.d.mts +40 -0
- package/dist/react/boundary.mjs +21 -0
- package/dist/react/create-bones.mjs +2 -6
- package/dist/react/index.d.mts +2 -2
- package/dist/react/index.mjs +2 -2
- package/dist/server/bootstrap.d.mts +4 -0
- package/dist/server/bootstrap.mjs +3 -0
- package/dist/server/index.d.mts +17 -0
- package/dist/server/index.mjs +57 -0
- package/package.json +7 -3
- package/src/css/auto.css +40 -27
- package/src/css/bones.css +68 -27
- package/dist/react/bones.d.mts +0 -15
- package/dist/react/bones.mjs +0 -27
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { MeasuredOverlay } from "./overlay.mjs";
|
|
2
|
+
//#region src/element/boundary.ts
|
|
3
|
+
const DEFAULT_DELAY = 200;
|
|
4
|
+
const DEFAULT_MIN_DURATION = 400;
|
|
5
|
+
const UPGRADE_PROPERTIES = [
|
|
6
|
+
"busy",
|
|
7
|
+
"force",
|
|
8
|
+
"delay",
|
|
9
|
+
"minDuration",
|
|
10
|
+
"transition",
|
|
11
|
+
"precision"
|
|
12
|
+
];
|
|
13
|
+
const swallow = () => {};
|
|
14
|
+
function parseMs(value, fallback) {
|
|
15
|
+
if (value === null || value.trim() === "") return fallback;
|
|
16
|
+
const ms = Number(value);
|
|
17
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : fallback;
|
|
18
|
+
}
|
|
19
|
+
const Base = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
|
|
20
|
+
var BonesBoundary = class extends Base {
|
|
21
|
+
static observedAttributes = [
|
|
22
|
+
"busy",
|
|
23
|
+
"force",
|
|
24
|
+
"aria-busy",
|
|
25
|
+
"inert",
|
|
26
|
+
"precision"
|
|
27
|
+
];
|
|
28
|
+
#state = "idle";
|
|
29
|
+
#timer;
|
|
30
|
+
#connected = false;
|
|
31
|
+
#shownAt = 0;
|
|
32
|
+
#hideToken = 0;
|
|
33
|
+
#writingOutput = false;
|
|
34
|
+
#overlay = new MeasuredOverlay(this);
|
|
35
|
+
get busy() {
|
|
36
|
+
return this.hasAttribute("busy");
|
|
37
|
+
}
|
|
38
|
+
set busy(value) {
|
|
39
|
+
this.toggleAttribute("busy", Boolean(value));
|
|
40
|
+
}
|
|
41
|
+
get force() {
|
|
42
|
+
return this.hasAttribute("force");
|
|
43
|
+
}
|
|
44
|
+
set force(value) {
|
|
45
|
+
this.toggleAttribute("force", Boolean(value));
|
|
46
|
+
}
|
|
47
|
+
get delay() {
|
|
48
|
+
return parseMs(this.getAttribute("delay"), 200);
|
|
49
|
+
}
|
|
50
|
+
set delay(value) {
|
|
51
|
+
if (value === null || value === void 0) this.removeAttribute("delay");
|
|
52
|
+
else this.setAttribute("delay", String(value));
|
|
53
|
+
}
|
|
54
|
+
get minDuration() {
|
|
55
|
+
return parseMs(this.getAttribute("min-duration"), 400);
|
|
56
|
+
}
|
|
57
|
+
set minDuration(value) {
|
|
58
|
+
if (value === null || value === void 0) this.removeAttribute("min-duration");
|
|
59
|
+
else this.setAttribute("min-duration", String(value));
|
|
60
|
+
}
|
|
61
|
+
get transition() {
|
|
62
|
+
return this.getAttribute("transition") === "none" ? "none" : "auto";
|
|
63
|
+
}
|
|
64
|
+
set transition(value) {
|
|
65
|
+
if (value === "none") this.setAttribute("transition", "none");
|
|
66
|
+
else this.removeAttribute("transition");
|
|
67
|
+
}
|
|
68
|
+
get precision() {
|
|
69
|
+
return this.getAttribute("precision") === "measured" ? "measured" : "css";
|
|
70
|
+
}
|
|
71
|
+
set precision(value) {
|
|
72
|
+
if (value === "measured") this.setAttribute("precision", "measured");
|
|
73
|
+
else this.removeAttribute("precision");
|
|
74
|
+
}
|
|
75
|
+
get showing() {
|
|
76
|
+
return this.#state === "showing" || this.#state === "draining" || this.#state === "hiding";
|
|
77
|
+
}
|
|
78
|
+
connectedCallback() {
|
|
79
|
+
for (const name of UPGRADE_PROPERTIES) this.#upgradeProperty(name);
|
|
80
|
+
this.#connected = true;
|
|
81
|
+
this.#syncPrecision();
|
|
82
|
+
if (this.getAttribute("aria-busy") === "true" && !this.showing) {
|
|
83
|
+
this.#show();
|
|
84
|
+
if (!this.busy && !this.force) this.#evaluate();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.#evaluate();
|
|
88
|
+
}
|
|
89
|
+
disconnectedCallback() {
|
|
90
|
+
this.#overlay.pause();
|
|
91
|
+
this.#connected = false;
|
|
92
|
+
this.#clearTimer();
|
|
93
|
+
if (this.#state === "pending") this.#state = "idle";
|
|
94
|
+
else if (this.#state === "hiding") this.#state = "showing";
|
|
95
|
+
}
|
|
96
|
+
attributeChangedCallback(name) {
|
|
97
|
+
if (!this.#connected) return;
|
|
98
|
+
if (name === "aria-busy" || name === "inert") {
|
|
99
|
+
this.#defendOutput();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (name === "precision") {
|
|
103
|
+
this.#syncPrecision();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.#evaluate();
|
|
107
|
+
}
|
|
108
|
+
#defendOutput() {
|
|
109
|
+
if (this.#writingOutput) return;
|
|
110
|
+
if (this.#state !== "showing" && this.#state !== "draining") return;
|
|
111
|
+
if (this.getAttribute("aria-busy") === "true" && this.hasAttribute("inert")) return;
|
|
112
|
+
this.#writeOutput(true);
|
|
113
|
+
}
|
|
114
|
+
#syncPrecision() {
|
|
115
|
+
if (this.precision === "measured") {
|
|
116
|
+
this.#overlay.prepare();
|
|
117
|
+
if (this.showing) this.#overlay.activate();
|
|
118
|
+
} else this.#overlay.deactivate();
|
|
119
|
+
}
|
|
120
|
+
#upgradeProperty(name) {
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(this, name)) return;
|
|
122
|
+
const self = this;
|
|
123
|
+
const value = self[name];
|
|
124
|
+
delete self[name];
|
|
125
|
+
self[name] = value;
|
|
126
|
+
}
|
|
127
|
+
#clearTimer() {
|
|
128
|
+
if (this.#timer !== void 0) clearTimeout(this.#timer);
|
|
129
|
+
this.#timer = void 0;
|
|
130
|
+
}
|
|
131
|
+
#evaluate() {
|
|
132
|
+
if (this.force) {
|
|
133
|
+
if (this.#state === "draining" || this.#state === "hiding") this.#resume();
|
|
134
|
+
else if (this.#state !== "showing") this.#show();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (this.busy) {
|
|
138
|
+
if (this.#state === "idle") {
|
|
139
|
+
const delay = this.delay;
|
|
140
|
+
if (delay === 0) this.#show();
|
|
141
|
+
else {
|
|
142
|
+
this.#state = "pending";
|
|
143
|
+
this.#timer = setTimeout(() => this.#show(), delay);
|
|
144
|
+
}
|
|
145
|
+
} else if (this.#state === "draining" || this.#state === "hiding") this.#resume();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (this.#state === "pending") {
|
|
149
|
+
this.#clearTimer();
|
|
150
|
+
this.#state = "idle";
|
|
151
|
+
} else if (this.#state === "showing" || this.#state === "draining") {
|
|
152
|
+
this.#clearTimer();
|
|
153
|
+
const remaining = this.#shownAt + this.minDuration - Date.now();
|
|
154
|
+
if (remaining <= 0) this.#hide();
|
|
155
|
+
else {
|
|
156
|
+
this.#state = "draining";
|
|
157
|
+
this.#timer = setTimeout(() => this.#hide(), remaining);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
#resume() {
|
|
162
|
+
this.#clearTimer();
|
|
163
|
+
this.#state = "showing";
|
|
164
|
+
this.#writeOutput(true);
|
|
165
|
+
}
|
|
166
|
+
#show() {
|
|
167
|
+
this.#clearTimer();
|
|
168
|
+
this.#state = "showing";
|
|
169
|
+
this.#shownAt = Date.now();
|
|
170
|
+
this.#writeOutput(true);
|
|
171
|
+
if (this.precision === "measured") this.#overlay.activate();
|
|
172
|
+
this.dispatchEvent(new CustomEvent("bones:show", {
|
|
173
|
+
bubbles: true,
|
|
174
|
+
composed: true
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
#hide() {
|
|
178
|
+
this.#clearTimer();
|
|
179
|
+
this.#state = "hiding";
|
|
180
|
+
const token = ++this.#hideToken;
|
|
181
|
+
const update = () => {
|
|
182
|
+
if (this.#state !== "hiding" || token !== this.#hideToken) return;
|
|
183
|
+
this.#state = "idle";
|
|
184
|
+
this.#writeOutput(false);
|
|
185
|
+
this.#overlay.deactivate();
|
|
186
|
+
this.dispatchEvent(new CustomEvent("bones:hide", {
|
|
187
|
+
bubbles: true,
|
|
188
|
+
composed: true
|
|
189
|
+
}));
|
|
190
|
+
};
|
|
191
|
+
if (this.#canTransition()) {
|
|
192
|
+
const transition = document.startViewTransition(update);
|
|
193
|
+
transition?.ready?.catch(swallow);
|
|
194
|
+
transition?.updateCallbackDone?.catch(swallow);
|
|
195
|
+
transition?.finished?.catch(swallow);
|
|
196
|
+
} else update();
|
|
197
|
+
}
|
|
198
|
+
#writeOutput(shown) {
|
|
199
|
+
this.#writingOutput = true;
|
|
200
|
+
try {
|
|
201
|
+
if (shown) {
|
|
202
|
+
this.setAttribute("aria-busy", "true");
|
|
203
|
+
this.toggleAttribute("inert", true);
|
|
204
|
+
} else {
|
|
205
|
+
this.removeAttribute("aria-busy");
|
|
206
|
+
this.removeAttribute("inert");
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
this.#writingOutput = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
#canTransition() {
|
|
213
|
+
if (this.transition === "none") return false;
|
|
214
|
+
if (typeof document.startViewTransition !== "function") return false;
|
|
215
|
+
if (typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
export { BonesBoundary, DEFAULT_DELAY, DEFAULT_MIN_DURATION };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BonesBoundary, DEFAULT_DELAY, DEFAULT_MIN_DURATION } from "./boundary.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/element/index.d.ts
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
"bones-boundary": BonesBoundary;
|
|
7
|
+
}
|
|
8
|
+
interface HTMLElementEventMap {
|
|
9
|
+
"bones:show": CustomEvent;
|
|
10
|
+
"bones:hide": CustomEvent;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { BonesBoundary, DEFAULT_DELAY, DEFAULT_MIN_DURATION };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BonesBoundary, DEFAULT_DELAY, DEFAULT_MIN_DURATION } from "./boundary.mjs";
|
|
2
|
+
//#region src/element/index.ts
|
|
3
|
+
if (typeof customElements !== "undefined") if (customElements.get("bones-boundary")) console.warn("bones-boundary is already defined; @camp.dev/bones/element did not register");
|
|
4
|
+
else customElements.define("bones-boundary", BonesBoundary);
|
|
5
|
+
//#endregion
|
|
6
|
+
export { BonesBoundary, DEFAULT_DELAY, DEFAULT_MIN_DURATION };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//#region src/element/measure.ts
|
|
2
|
+
const BLOCK_TAGS = new Set([
|
|
3
|
+
"img",
|
|
4
|
+
"svg",
|
|
5
|
+
"video",
|
|
6
|
+
"canvas",
|
|
7
|
+
"picture",
|
|
8
|
+
"iframe",
|
|
9
|
+
"embed",
|
|
10
|
+
"object",
|
|
11
|
+
"audio",
|
|
12
|
+
"button",
|
|
13
|
+
"input",
|
|
14
|
+
"select",
|
|
15
|
+
"textarea",
|
|
16
|
+
"progress",
|
|
17
|
+
"meter"
|
|
18
|
+
]);
|
|
19
|
+
const TEXT_BAR_SCALE = .55;
|
|
20
|
+
function verticalOverlap(a, b) {
|
|
21
|
+
return Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
|
|
22
|
+
}
|
|
23
|
+
function mergeLineRects(rects) {
|
|
24
|
+
const sorted = [...rects].sort((a, b) => a.top - b.top || a.left - b.left);
|
|
25
|
+
const merged = [];
|
|
26
|
+
for (const rect of sorted) {
|
|
27
|
+
const line = merged.findLast((candidate) => {
|
|
28
|
+
if (verticalOverlap(candidate, rect) < Math.min(candidate.height, rect.height) / 2) return false;
|
|
29
|
+
return rect.left - (candidate.left + candidate.width) <= Math.max(candidate.height, rect.height) / 2;
|
|
30
|
+
});
|
|
31
|
+
if (line === void 0) {
|
|
32
|
+
merged.push({ ...rect });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const right = Math.max(line.left + line.width, rect.left + rect.width);
|
|
36
|
+
const bottom = Math.max(line.top + line.height, rect.top + rect.height);
|
|
37
|
+
line.left = Math.min(line.left, rect.left);
|
|
38
|
+
line.top = Math.min(line.top, rect.top);
|
|
39
|
+
line.width = right - line.left;
|
|
40
|
+
line.height = bottom - line.top;
|
|
41
|
+
}
|
|
42
|
+
return merged;
|
|
43
|
+
}
|
|
44
|
+
function isVisible(rect) {
|
|
45
|
+
return rect.width > 0 && rect.height > 0;
|
|
46
|
+
}
|
|
47
|
+
function toRect(rect) {
|
|
48
|
+
return {
|
|
49
|
+
left: rect.left,
|
|
50
|
+
top: rect.top,
|
|
51
|
+
width: rect.width,
|
|
52
|
+
height: rect.height
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function measureBones(root) {
|
|
56
|
+
const doc = root.ownerDocument;
|
|
57
|
+
const blocks = [];
|
|
58
|
+
const textRects = [];
|
|
59
|
+
const visit = (node) => {
|
|
60
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
61
|
+
if (node.data.trim() === "") return;
|
|
62
|
+
const range = doc.createRange();
|
|
63
|
+
range.selectNodeContents(node);
|
|
64
|
+
if (typeof range.getClientRects !== "function") return;
|
|
65
|
+
for (const rect of Array.from(range.getClientRects())) {
|
|
66
|
+
const converted = toRect(rect);
|
|
67
|
+
if (isVisible(converted)) textRects.push(converted);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
72
|
+
const el = node;
|
|
73
|
+
if (el.getAttribute("data-bones-auto") === "off") return;
|
|
74
|
+
if (BLOCK_TAGS.has(el.localName)) {
|
|
75
|
+
const rect = toRect(el.getBoundingClientRect());
|
|
76
|
+
if (isVisible(rect)) blocks.push({
|
|
77
|
+
kind: "block",
|
|
78
|
+
...rect
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const child of node.childNodes) visit(child);
|
|
83
|
+
};
|
|
84
|
+
for (const child of root.childNodes) visit(child);
|
|
85
|
+
const bones = [...blocks];
|
|
86
|
+
for (const line of mergeLineRects(textRects)) {
|
|
87
|
+
const height = line.height * TEXT_BAR_SCALE;
|
|
88
|
+
bones.push({
|
|
89
|
+
kind: "text",
|
|
90
|
+
left: line.left,
|
|
91
|
+
top: line.top + (line.height - height) / 2,
|
|
92
|
+
width: line.width,
|
|
93
|
+
height
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return bones;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
export { measureBones };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { measureBones } from "./measure.mjs";
|
|
2
|
+
//#region src/element/overlay.ts
|
|
3
|
+
const OVERLAY_CSS = `
|
|
4
|
+
:host([precision="measured"]) {
|
|
5
|
+
display: block;
|
|
6
|
+
position: relative;
|
|
7
|
+
}
|
|
8
|
+
/* Hidden content still lays out, so re-measurement stays valid. Deliberately
|
|
9
|
+
not !important: outer-tree rules beat ::slotted, which is what lets the
|
|
10
|
+
auto.css opt-out rule re-show exempt subtrees (and lets an author
|
|
11
|
+
visibility rule on a direct child win — a documented edge). */
|
|
12
|
+
:host([data-bones-measured]) ::slotted(*) {
|
|
13
|
+
visibility: hidden;
|
|
14
|
+
}
|
|
15
|
+
[part~="overlay"] {
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset: 0;
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
}
|
|
20
|
+
[part~="bone"] {
|
|
21
|
+
position: absolute;
|
|
22
|
+
background: var(--bone-base, color-mix(in srgb, rgb(from currentcolor r g b / 1) 12%, transparent));
|
|
23
|
+
border-radius: var(--bone-radius, 4px);
|
|
24
|
+
}
|
|
25
|
+
@keyframes bone-shimmer {
|
|
26
|
+
0% { background-position: 200% 0; }
|
|
27
|
+
100% { background-position: -200% 0; }
|
|
28
|
+
}
|
|
29
|
+
@keyframes bone-pulse {
|
|
30
|
+
0%, 100% { opacity: 1; }
|
|
31
|
+
50% { opacity: 0.5; }
|
|
32
|
+
}
|
|
33
|
+
[part~="overlay"]:not([data-bone-animate]) [part~="bone"],
|
|
34
|
+
[part~="overlay"][data-bone-animate="shimmer"] [part~="bone"] {
|
|
35
|
+
animation: bone-shimmer var(--bone-duration, 1.5s) ease-in-out infinite;
|
|
36
|
+
background: linear-gradient(
|
|
37
|
+
90deg,
|
|
38
|
+
var(--bone-base, color-mix(in srgb, rgb(from currentcolor r g b / 1) 12%, transparent)) 25%,
|
|
39
|
+
var(--bone-highlight, color-mix(in srgb, rgb(from currentcolor r g b / 1) 6%, transparent)) 50%,
|
|
40
|
+
var(--bone-base, color-mix(in srgb, rgb(from currentcolor r g b / 1) 12%, transparent)) 75%
|
|
41
|
+
);
|
|
42
|
+
background-size: 200% 100%;
|
|
43
|
+
}
|
|
44
|
+
[part~="overlay"][data-bone-animate="pulse"] [part~="bone"] {
|
|
45
|
+
animation: bone-pulse var(--bone-duration, 1.5s) ease-in-out infinite;
|
|
46
|
+
}
|
|
47
|
+
[part~="overlay"][data-bone-animate="none"] [part~="bone"] {
|
|
48
|
+
animation: none;
|
|
49
|
+
}
|
|
50
|
+
/* Matches the shimmer/pulse selectors above at equal (0,3,0) specificity so
|
|
51
|
+
this override always wins the cascade instead of losing to source order.
|
|
52
|
+
data-bone-animate="none" is deliberately excluded: none still means none. */
|
|
53
|
+
@media (prefers-reduced-motion: reduce) {
|
|
54
|
+
[part~="overlay"]:not([data-bone-animate]) [part~="bone"],
|
|
55
|
+
[part~="overlay"][data-bone-animate="shimmer"] [part~="bone"],
|
|
56
|
+
[part~="overlay"][data-bone-animate="pulse"] [part~="bone"] {
|
|
57
|
+
animation: bone-pulse 2s ease-in-out infinite;
|
|
58
|
+
background: var(--bone-base, color-mix(in srgb, rgb(from currentcolor r g b / 1) 12%, transparent));
|
|
59
|
+
background-size: auto;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
let sharedSheet;
|
|
64
|
+
function applyStyles(root) {
|
|
65
|
+
if ("adoptedStyleSheets" in root && typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype) {
|
|
66
|
+
if (sharedSheet === void 0) {
|
|
67
|
+
sharedSheet = new CSSStyleSheet();
|
|
68
|
+
sharedSheet.replaceSync(OVERLAY_CSS);
|
|
69
|
+
}
|
|
70
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sharedSheet];
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const style = root.ownerDocument.createElement("style");
|
|
74
|
+
style.textContent = OVERLAY_CSS;
|
|
75
|
+
root.append(style);
|
|
76
|
+
}
|
|
77
|
+
var MeasuredOverlay = class {
|
|
78
|
+
#host;
|
|
79
|
+
#container;
|
|
80
|
+
#observer;
|
|
81
|
+
#mutations;
|
|
82
|
+
#active = false;
|
|
83
|
+
#ownsAutoOff = false;
|
|
84
|
+
constructor(host) {
|
|
85
|
+
this.#host = host;
|
|
86
|
+
}
|
|
87
|
+
get active() {
|
|
88
|
+
return this.#active;
|
|
89
|
+
}
|
|
90
|
+
prepare() {
|
|
91
|
+
if (this.#host.shadowRoot) return;
|
|
92
|
+
const root = this.#host.attachShadow({ mode: "open" });
|
|
93
|
+
root.append(this.#host.ownerDocument.createElement("slot"));
|
|
94
|
+
applyStyles(root);
|
|
95
|
+
}
|
|
96
|
+
activate() {
|
|
97
|
+
this.prepare();
|
|
98
|
+
if (!this.#active) {
|
|
99
|
+
this.#active = true;
|
|
100
|
+
if (!this.#host.hasAttribute("data-bones-auto")) {
|
|
101
|
+
this.#host.setAttribute("data-bones-auto", "off");
|
|
102
|
+
this.#ownsAutoOff = true;
|
|
103
|
+
}
|
|
104
|
+
this.#host.setAttribute("data-bones-measured", "");
|
|
105
|
+
}
|
|
106
|
+
if (!this.#renderBars()) {
|
|
107
|
+
this.deactivate();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.#observe();
|
|
111
|
+
}
|
|
112
|
+
deactivate() {
|
|
113
|
+
this.#unobserve();
|
|
114
|
+
this.#container?.remove();
|
|
115
|
+
this.#container = void 0;
|
|
116
|
+
if (!this.#active) return;
|
|
117
|
+
this.#active = false;
|
|
118
|
+
this.#host.removeAttribute("data-bones-measured");
|
|
119
|
+
if (this.#ownsAutoOff) {
|
|
120
|
+
this.#host.removeAttribute("data-bones-auto");
|
|
121
|
+
this.#ownsAutoOff = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
pause() {
|
|
125
|
+
this.#unobserve();
|
|
126
|
+
}
|
|
127
|
+
#observe() {
|
|
128
|
+
if (typeof ResizeObserver !== "undefined" && !this.#observer) {
|
|
129
|
+
this.#observer = new ResizeObserver(() => this.#invalidate());
|
|
130
|
+
this.#observer.observe(this.#host);
|
|
131
|
+
}
|
|
132
|
+
if (typeof MutationObserver !== "undefined" && !this.#mutations) {
|
|
133
|
+
this.#mutations = new MutationObserver(() => this.#invalidate());
|
|
134
|
+
this.#mutations.observe(this.#host, {
|
|
135
|
+
childList: true,
|
|
136
|
+
subtree: true,
|
|
137
|
+
characterData: true
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
#unobserve() {
|
|
142
|
+
this.#observer?.disconnect();
|
|
143
|
+
this.#observer = void 0;
|
|
144
|
+
this.#mutations?.disconnect();
|
|
145
|
+
this.#mutations = void 0;
|
|
146
|
+
}
|
|
147
|
+
#invalidate() {
|
|
148
|
+
if (this.#active && !this.#renderBars()) this.deactivate();
|
|
149
|
+
}
|
|
150
|
+
#renderBars() {
|
|
151
|
+
const root = this.#host.shadowRoot;
|
|
152
|
+
if (!root) return false;
|
|
153
|
+
const bones = measureBones(this.#host);
|
|
154
|
+
if (bones.length === 0) return false;
|
|
155
|
+
const doc = this.#host.ownerDocument;
|
|
156
|
+
if (!this.#container) {
|
|
157
|
+
this.#container = doc.createElement("div");
|
|
158
|
+
this.#container.setAttribute("part", "overlay");
|
|
159
|
+
this.#container.setAttribute("aria-hidden", "true");
|
|
160
|
+
root.append(this.#container);
|
|
161
|
+
}
|
|
162
|
+
const animate = this.#host.closest("[data-bone-animate]")?.getAttribute("data-bone-animate");
|
|
163
|
+
if (animate) this.#container.setAttribute("data-bone-animate", animate);
|
|
164
|
+
else this.#container.removeAttribute("data-bone-animate");
|
|
165
|
+
const origin = this.#container.getBoundingClientRect();
|
|
166
|
+
const style = getComputedStyle(this.#container);
|
|
167
|
+
const layoutWidth = Number.parseFloat(style.width);
|
|
168
|
+
const layoutHeight = Number.parseFloat(style.height);
|
|
169
|
+
const scaleX = layoutWidth > 0 ? origin.width / layoutWidth : 1;
|
|
170
|
+
const scaleY = layoutHeight > 0 ? origin.height / layoutHeight : 1;
|
|
171
|
+
this.#container.replaceChildren(...bones.map((bone) => {
|
|
172
|
+
const bar = doc.createElement("div");
|
|
173
|
+
bar.setAttribute("part", `bone bone-${bone.kind}`);
|
|
174
|
+
bar.style.left = `${(bone.left - origin.left) / scaleX}px`;
|
|
175
|
+
bar.style.top = `${(bone.top - origin.top) / scaleY}px`;
|
|
176
|
+
bar.style.width = `${bone.width / scaleX}px`;
|
|
177
|
+
bar.style.height = `${bone.height / scaleY}px`;
|
|
178
|
+
return bar;
|
|
179
|
+
}));
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
export { MeasuredOverlay };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BonesBoundary as BonesBoundary$1 } from "../element/boundary.mjs";
|
|
2
|
+
import { HTMLAttributes, ReactNode, Ref } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/boundary.d.ts
|
|
5
|
+
interface ElementAttributes extends HTMLAttributes<HTMLElement> {
|
|
6
|
+
busy?: boolean;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
delay?: number;
|
|
9
|
+
"min-duration"?: number;
|
|
10
|
+
transition?: "auto" | "none";
|
|
11
|
+
precision?: "css" | "measured";
|
|
12
|
+
class?: string;
|
|
13
|
+
ref?: Ref<BonesBoundary$1>;
|
|
14
|
+
[dataAttribute: `data-${string}`]: string | number | boolean | undefined;
|
|
15
|
+
}
|
|
16
|
+
declare module "react" {
|
|
17
|
+
namespace JSX {
|
|
18
|
+
interface IntrinsicElements {
|
|
19
|
+
"bones-boundary": ElementAttributes;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
interface BonesBoundaryProps extends Omit<ElementAttributes, "min-duration" | "inert" | "aria-busy" | "class"> {
|
|
24
|
+
minDuration?: number;
|
|
25
|
+
onShow?: (event: CustomEvent) => void;
|
|
26
|
+
onHide?: (event: CustomEvent) => void;
|
|
27
|
+
}
|
|
28
|
+
declare function BonesBoundary({
|
|
29
|
+
busy,
|
|
30
|
+
force,
|
|
31
|
+
delay,
|
|
32
|
+
minDuration,
|
|
33
|
+
transition,
|
|
34
|
+
onShow,
|
|
35
|
+
onHide,
|
|
36
|
+
children,
|
|
37
|
+
...rest
|
|
38
|
+
}: BonesBoundaryProps): ReactNode;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { BonesBoundary, BonesBoundaryProps };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
//#region src/react/boundary.ts
|
|
3
|
+
function BonesBoundary({ busy, force, delay, minDuration, transition, onShow, onHide, children, ...rest }) {
|
|
4
|
+
return createElement("bones-boundary", {
|
|
5
|
+
...rest,
|
|
6
|
+
busy: busy ? true : void 0,
|
|
7
|
+
force: force ? true : void 0,
|
|
8
|
+
delay,
|
|
9
|
+
"min-duration": minDuration,
|
|
10
|
+
transition,
|
|
11
|
+
...force ? {
|
|
12
|
+
"aria-busy": "true",
|
|
13
|
+
inert: true
|
|
14
|
+
} : {},
|
|
15
|
+
suppressHydrationWarning: true,
|
|
16
|
+
"onbones:show": onShow,
|
|
17
|
+
"onbones:hide": onHide
|
|
18
|
+
}, children);
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { BonesBoundary };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { boneAttributes } from "../core/attributes.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { cloneElement, createElement, isValidElement } from "react";
|
|
3
3
|
//#region src/react/create-bones.ts
|
|
4
|
-
const getBonesContext = cache(() => ({ loading: false }));
|
|
5
4
|
function withKey(node, key) {
|
|
6
5
|
return isValidElement(node) ? cloneElement(node, { key }) : node;
|
|
7
6
|
}
|
|
@@ -40,9 +39,6 @@ function createBones(dataOrOptions, maybeOptions) {
|
|
|
40
39
|
} else if (data != null && data === forceBones) {
|
|
41
40
|
isLoading = true;
|
|
42
41
|
resolved = void 0;
|
|
43
|
-
} else if (getBonesContext().loading) {
|
|
44
|
-
isLoading = true;
|
|
45
|
-
resolved = void 0;
|
|
46
42
|
} else if (data != null && data instanceof Promise) resolved = readPromise(data);
|
|
47
43
|
else resolved = data;
|
|
48
44
|
let boneCallIndex = 0;
|
|
@@ -70,4 +66,4 @@ function createBones(dataOrOptions, maybeOptions) {
|
|
|
70
66
|
};
|
|
71
67
|
}
|
|
72
68
|
//#endregion
|
|
73
|
-
export { createBones, forceBones,
|
|
69
|
+
export { createBones, forceBones, readPromise };
|
package/dist/react/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { BoneOptions, BoneType, MinMax, isMinMax, minMax } from "../core/attributes.mjs";
|
|
2
2
|
import { CreateBonesOptions, CreateBonesReturn, createBones, forceBones, readPromise } from "./create-bones.mjs";
|
|
3
|
-
import {
|
|
4
|
-
export { type BoneOptions, type BoneType,
|
|
3
|
+
import { BonesBoundary, BonesBoundaryProps } from "./boundary.mjs";
|
|
4
|
+
export { type BoneOptions, type BoneType, BonesBoundary, type BonesBoundaryProps, type CreateBonesOptions, type CreateBonesReturn, type MinMax, createBones, forceBones, isMinMax, minMax, readPromise };
|
package/dist/react/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { isMinMax, minMax } from "../core/attributes.mjs";
|
|
2
2
|
import { createBones, forceBones, readPromise } from "./create-bones.mjs";
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
3
|
+
import { BonesBoundary } from "./boundary.mjs";
|
|
4
|
+
export { BonesBoundary, createBones, forceBones, isMinMax, minMax, readPromise };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/server/bootstrap.d.ts
|
|
2
|
+
declare const BOOTSTRAP_SCRIPT = "<script>function __bonesSwap(e,r){var t=document.querySelector('template[data-bones-chunk=\"'+e+'\"]'),n=document.querySelector('[data-bones-slot=\"'+e+'\"]');n?(t&&(n.replaceChildren(t.content),t.remove()),r&&n.setAttribute(\"data-bones-error\",\"\"),\"boolean\"==typeof n.busy?n.busy=!1:(n.removeAttribute(\"busy\"),n.removeAttribute(\"aria-busy\"),n.removeAttribute(\"inert\"))):t&&t.remove()}</script>";
|
|
3
|
+
//#endregion
|
|
4
|
+
export { BOOTSTRAP_SCRIPT };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const BOOTSTRAP_SCRIPT = `<script>function __bonesSwap(e,r){var t=document.querySelector('template[data-bones-chunk="'+e+'"]'),n=document.querySelector('[data-bones-slot="'+e+'"]');n?(t&&(n.replaceChildren(t.content),t.remove()),r&&n.setAttribute("data-bones-error",""),"boolean"==typeof n.busy?n.busy=!1:(n.removeAttribute("busy"),n.removeAttribute("aria-busy"),n.removeAttribute("inert"))):t&&t.remove()}<\/script>`;
|
|
2
|
+
//#endregion
|
|
3
|
+
export { BOOTSTRAP_SCRIPT };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BOOTSTRAP_SCRIPT } from "./bootstrap.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/server/index.d.ts
|
|
4
|
+
declare function renderBoundary(id: string, fallbackHtml: string, attrs?: string): string;
|
|
5
|
+
declare function renderChunk(id: string, html: string): string;
|
|
6
|
+
declare function renderErrorChunk(id: string, html?: string): string;
|
|
7
|
+
interface StreamBonesOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Renders the error HTML for a rejected slot. Returning undefined (and
|
|
10
|
+
* throwing) falls back to the bare error chunk, which keeps the boundary's
|
|
11
|
+
* fallback children.
|
|
12
|
+
*/
|
|
13
|
+
onError?: (id: string, error: unknown) => string | undefined;
|
|
14
|
+
}
|
|
15
|
+
declare function streamBones(shell: string, slots: Record<string, Promise<string>>, options?: StreamBonesOptions): ReadableStream<Uint8Array>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { BOOTSTRAP_SCRIPT, StreamBonesOptions, renderBoundary, renderChunk, renderErrorChunk, streamBones };
|