@caltho/site-kit 0.2.9 → 0.2.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.
- package/README.md +63 -8
- package/dist/cli/check.js +31 -1
- package/dist/content/index.d.ts +318 -23
- package/dist/content/index.js +148 -15
- package/dist/content/look.d.ts +260 -9
- package/dist/content/look.js +386 -22
- package/dist/finish.js +9 -1
- package/dist/fonts/families/dm-sans/LICENSE +93 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/LICENSE +93 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/LICENSE +93 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/LICENSE +93 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/LICENSE +93 -0
- package/dist/fonts/families/manrope/manrope-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/LICENSE +93 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/LICENSE +93 -0
- package/dist/fonts/families/outfit/outfit-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/outfit-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/LICENSE +93 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/LICENSE +93 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/LICENSE +93 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/google.json +1 -0
- package/dist/fonts-google.d.ts +35 -0
- package/dist/fonts-google.js +70 -0
- package/dist/fonts.css +365 -0
- package/dist/frame.test.d.ts +1 -0
- package/dist/frame.test.js +33 -0
- package/dist/migrate.d.ts +45 -0
- package/dist/migrate.js +284 -0
- package/dist/next/block-mark.d.ts +7 -0
- package/dist/next/block-mark.js +16 -0
- package/dist/next/blocks.d.ts +11 -0
- package/dist/next/blocks.js +35 -0
- package/dist/next/data.d.ts +4 -0
- package/dist/next/data.js +4 -0
- package/dist/next/frame.d.ts +34 -0
- package/dist/next/frame.js +70 -0
- package/dist/next/index.d.ts +6 -2
- package/dist/next/index.js +7 -3
- package/dist/next/menu-group.d.ts +11 -0
- package/dist/next/menu-group.js +33 -0
- package/dist/next/parts/LookStyle.js +3 -1
- package/dist/next/preview.js +282 -25
- package/dist/next/sections.d.ts +21 -6
- package/dist/next/sections.js +167 -6
- package/dist/prepare.d.ts +5 -1
- package/dist/prepare.js +59 -1
- package/dist/preview-match.d.ts +8 -0
- package/dist/preview-match.js +33 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +26 -0
- package/dist/sections/icons.d.ts +47 -0
- package/dist/sections/icons.js +51 -0
- package/dist/sections/index.d.ts +284 -2
- package/dist/sections/index.js +156 -2
- package/dist/sections/presets.d.ts +13 -0
- package/dist/sections/presets.js +300 -0
- package/dist/sections.css +1393 -9
- package/dist/sections.test.js +310 -8
- package/package.json +8 -3
package/dist/next/preview.js
CHANGED
|
@@ -8,16 +8,55 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
//
|
|
9
9
|
// CalOS → page: { type: "calos:show", site, images, path, selected?, scroll?, labels? }
|
|
10
10
|
// page → CalOS: { type: "calos:ready" }, { type: "calos:pick", path, block },
|
|
11
|
-
// { type: "calos:error", message }
|
|
11
|
+
// { type: "calos:error", message }, { type: "calos:overflow", blocks } (the page's
|
|
12
|
+
// blocks with something past the screen's edge at this width, by index)
|
|
12
13
|
import { Component, useEffect, useRef, useState } from "react";
|
|
13
14
|
import { lookVars } from "../content/look.js";
|
|
14
|
-
import {
|
|
15
|
+
import { fontFiles, googleFont } from "../fonts-google.js";
|
|
16
|
+
import { BUILT_IMAGES, fieldForImage, fieldForText, matchBlocks } from "../preview-match.js";
|
|
17
|
+
import { BLOCK_MARK, InPreview } from "./block-mark.js";
|
|
15
18
|
import { setPrepared } from "./data.js";
|
|
19
|
+
/** `labels`: what CalOS calls each of the page's blocks, shown on its outline. */
|
|
20
|
+
/** The Google fonts this preview has loaded for a draft (by name), once each. */
|
|
21
|
+
const loadedFonts = new Set();
|
|
16
22
|
/** `look`: the same Look options the site's defineSite has, for the draft's Look to be drawn as it's chosen. */
|
|
17
23
|
export function SitePreview({ origins, render, look }) {
|
|
18
24
|
const [shown, setShown] = useState(null);
|
|
25
|
+
// A Google font the draft's Look uses and this build doesn't serve yet: loaded from Fontsource's
|
|
26
|
+
// CDN (the browser's FontFace, which only the font policy governs), so the draft shows in it.
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const l = shown?.site.look;
|
|
29
|
+
for (const name of [l?.headingFont, l?.bodyFont]) {
|
|
30
|
+
if (typeof name !== "string" || (look?.fonts && name in look.fonts) || loadedFonts.has(name))
|
|
31
|
+
continue;
|
|
32
|
+
const f = googleFont(name);
|
|
33
|
+
if (!f)
|
|
34
|
+
continue;
|
|
35
|
+
loadedFonts.add(name);
|
|
36
|
+
for (const x of fontFiles(f)) {
|
|
37
|
+
const face = new FontFace(f.family, `url(${x.url}) format("woff2")`, { weight: x.weight, style: x.style, unicodeRange: x.range, display: "swap" });
|
|
38
|
+
document.fonts.add(face);
|
|
39
|
+
face.load().catch(() => document.fonts.delete(face));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Fonts the business uploaded: from CalOS, where the editor says each file is (with the pictures).
|
|
43
|
+
const uploaded = shown?.site.look?.uploadedFonts;
|
|
44
|
+
for (const f of uploaded ?? [])
|
|
45
|
+
for (const x of f.files) {
|
|
46
|
+
const url = shown?.images[x.src]?.src;
|
|
47
|
+
const key = `${f.family}|${x.src}`;
|
|
48
|
+
if (!url || loadedFonts.has(key) || !/^https?:\/\//.test(url))
|
|
49
|
+
continue;
|
|
50
|
+
loadedFonts.add(key);
|
|
51
|
+
const face = new FontFace(f.family, `url(${JSON.stringify(url)})`, { weight: x.weight, style: x.style, display: "swap" });
|
|
52
|
+
document.fonts.add(face);
|
|
53
|
+
face.load().catch(() => document.fonts.delete(face));
|
|
54
|
+
}
|
|
55
|
+
}, [shown, look]);
|
|
19
56
|
const [round, setRound] = useState(0);
|
|
20
57
|
const latest = useRef(null);
|
|
58
|
+
const lookRef = useRef(look);
|
|
59
|
+
lookRef.current = look;
|
|
21
60
|
// Answers go to the address CalOS is open at; until it's spoken, "ready" goes to each it may be.
|
|
22
61
|
const replyTo = useRef(null);
|
|
23
62
|
const post = (msg) => {
|
|
@@ -38,7 +77,8 @@ export function SitePreview({ origins, render, look }) {
|
|
|
38
77
|
return;
|
|
39
78
|
const m = { ...sent, images: asBuilt(sent.images) };
|
|
40
79
|
replyTo.current = e.origin;
|
|
41
|
-
|
|
80
|
+
// The draft's Look, its colours read by KitBlocks to know a dark background as it's chosen.
|
|
81
|
+
setPrepared({ site: m.site, images: m.images, csp: {}, ...(lookRef.current ? { look: lookVars(m.site.look, lookRef.current) } : {}) });
|
|
42
82
|
latest.current = m;
|
|
43
83
|
setShown(m);
|
|
44
84
|
setRound((r) => r + 1);
|
|
@@ -70,7 +110,7 @@ export function SitePreview({ origins, render, look }) {
|
|
|
70
110
|
e.stopPropagation();
|
|
71
111
|
// A link anywhere (a section's, the menu's, the footer's) is edited, never followed: CalOS's page picker moves between pages.
|
|
72
112
|
// A picture under the pointer counts, even when something over it (a frame, a caption's box) took the click.
|
|
73
|
-
const under = document.elementsFromPoint(e.clientX, e.clientY).find((el) => el.tagName === "IMG") ?? target;
|
|
113
|
+
const under = document.elementsFromPoint(e.clientX, e.clientY).find((el) => el.tagName === "IMG" && seen(el)) ?? target;
|
|
74
114
|
post({ type: "calos:pick", ...pick(under, m, wordsAt(e.clientX, e.clientY)) });
|
|
75
115
|
};
|
|
76
116
|
const onSubmit = (e) => e.preventDefault();
|
|
@@ -79,10 +119,10 @@ export function SitePreview({ origins, render, look }) {
|
|
|
79
119
|
const m = latest.current;
|
|
80
120
|
const target = e.target;
|
|
81
121
|
const blocks = m ? blockElements(m) : null;
|
|
82
|
-
const n = blocks && target ? blocks.findIndex((
|
|
122
|
+
const n = blocks && target ? blocks.findIndex((d) => holds(d, target)) : -1;
|
|
83
123
|
const selected = m ? blockElement(m) : null;
|
|
84
124
|
hovered = n >= 0 ? blocks[n] : null;
|
|
85
|
-
outline("section", hovered && hovered !== selected ? hovered : null, n >= 0 ? m?.labels?.[n] : undefined);
|
|
125
|
+
outline("section", hovered && hovered[0] !== selected?.[0] ? hovered : null, n >= 0 ? m?.labels?.[n] : undefined);
|
|
86
126
|
outline("hover", hoverable(target));
|
|
87
127
|
};
|
|
88
128
|
const leave = () => {
|
|
@@ -103,6 +143,56 @@ export function SitePreview({ origins, render, look }) {
|
|
|
103
143
|
};
|
|
104
144
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
145
|
}, [key]);
|
|
146
|
+
// The footer's own colour, as designed (the site's CSS may set it, not the Look): said to CalOS so
|
|
147
|
+
// its "as designed" choice shows the colour it is. Only while the draft leaves it as designed.
|
|
148
|
+
const footerSaid = useRef(null);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!shown)
|
|
151
|
+
return;
|
|
152
|
+
const bg = shown.site.look?.footer?.background;
|
|
153
|
+
if (bg && bg !== "dark")
|
|
154
|
+
return;
|
|
155
|
+
const frame = requestAnimationFrame(() => {
|
|
156
|
+
const footer = document.querySelector("footer.kit-footer");
|
|
157
|
+
const color = footer ? getComputedStyle(footer).backgroundColor : "";
|
|
158
|
+
if (color && color !== footerSaid.current)
|
|
159
|
+
post({ type: "calos:footer", color });
|
|
160
|
+
footerSaid.current = color;
|
|
161
|
+
});
|
|
162
|
+
return () => cancelAnimationFrame(frame);
|
|
163
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
164
|
+
}, [shown]);
|
|
165
|
+
// What runs past the edge of the screen, after each drawing and as the width changes: said to
|
|
166
|
+
// CalOS by block, so the editor can point at the section to fix at this size.
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!shown)
|
|
169
|
+
return;
|
|
170
|
+
// Nothing said yet for this drawing: the first measure is always said, so a section fixed since
|
|
171
|
+
// (nothing past the edge any more) is cleared, not left flagged.
|
|
172
|
+
let last = null;
|
|
173
|
+
const measure = () => {
|
|
174
|
+
const { blocks, what } = overflowing(latest.current ?? shown);
|
|
175
|
+
const said = blocks.join(",");
|
|
176
|
+
if (said !== last)
|
|
177
|
+
post({ type: "calos:overflow", blocks, what });
|
|
178
|
+
last = said;
|
|
179
|
+
};
|
|
180
|
+
let frame = 0;
|
|
181
|
+
const soon = () => {
|
|
182
|
+
cancelAnimationFrame(frame);
|
|
183
|
+
frame = requestAnimationFrame(() => requestAnimationFrame(measure));
|
|
184
|
+
};
|
|
185
|
+
soon();
|
|
186
|
+
void document.fonts?.ready.then(soon);
|
|
187
|
+
window.addEventListener("resize", soon);
|
|
188
|
+
window.addEventListener("load", soon, true);
|
|
189
|
+
return () => {
|
|
190
|
+
cancelAnimationFrame(frame);
|
|
191
|
+
window.removeEventListener("resize", soon);
|
|
192
|
+
window.removeEventListener("load", soon, true);
|
|
193
|
+
};
|
|
194
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
195
|
+
}, [shown, round]);
|
|
106
196
|
// The selected block outlined (and brought into view when CalOS asks), after each drawing.
|
|
107
197
|
useEffect(() => {
|
|
108
198
|
if (!shown)
|
|
@@ -120,32 +210,77 @@ export function SitePreview({ origins, render, look }) {
|
|
|
120
210
|
drawnPath = shown.path;
|
|
121
211
|
// Scrolled here only: scrollIntoView would scroll CalOS's page around the frame too.
|
|
122
212
|
if (el && shown.scroll)
|
|
123
|
-
window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
213
|
+
window.scrollTo({ top: el[0].getBoundingClientRect().top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
214
|
+
// On a resize, once a frame at most: measured on every event, the page was re-laid out mid-change.
|
|
215
|
+
let frame = 0;
|
|
124
216
|
const keep = () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
217
|
+
if (frame)
|
|
218
|
+
return;
|
|
219
|
+
frame = requestAnimationFrame(() => {
|
|
220
|
+
frame = 0;
|
|
221
|
+
outline("selected", blockElement(latest.current), label(latest.current));
|
|
222
|
+
outline("section", null);
|
|
223
|
+
outline("hover", null);
|
|
224
|
+
});
|
|
128
225
|
};
|
|
129
|
-
|
|
226
|
+
// The outlines are in the page's coordinates, so scrolling moves them with it; the hover ones go.
|
|
227
|
+
let hidden = false;
|
|
228
|
+
const scrolled = () => {
|
|
229
|
+
if (hidden)
|
|
230
|
+
return;
|
|
231
|
+
hidden = true;
|
|
232
|
+
requestAnimationFrame(() => {
|
|
233
|
+
hidden = false;
|
|
234
|
+
outline("section", null);
|
|
235
|
+
outline("hover", null);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
window.addEventListener("scroll", scrolled, { passive: true });
|
|
130
239
|
window.addEventListener("resize", keep);
|
|
131
240
|
return () => {
|
|
132
|
-
|
|
241
|
+
cancelAnimationFrame(frame);
|
|
242
|
+
window.removeEventListener("scroll", scrolled);
|
|
133
243
|
window.removeEventListener("resize", keep);
|
|
134
244
|
};
|
|
135
245
|
}, [shown, round]);
|
|
136
246
|
// The draft's Look, set on the page (LookStyle drew the published one into the build).
|
|
247
|
+
// One the draft no longer sets (a colour removed, the footer back as designed) is taken off again.
|
|
248
|
+
const setVars = useRef(new Set());
|
|
137
249
|
useEffect(() => {
|
|
138
250
|
if (!shown || !look)
|
|
139
251
|
return;
|
|
140
|
-
|
|
141
|
-
|
|
252
|
+
const vars = lookVars(shown.site.look, look);
|
|
253
|
+
const style = document.documentElement.style;
|
|
254
|
+
// The published Look's values (LookStyle's :root rule) the draft no longer has (a colour it
|
|
255
|
+
// removed): made invalid here, so what reads them falls back as the site will once published.
|
|
256
|
+
const published = new Set();
|
|
257
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
258
|
+
let rules;
|
|
259
|
+
try {
|
|
260
|
+
rules = sheet.cssRules;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
for (const r of Array.from(rules))
|
|
266
|
+
if (r instanceof CSSStyleRule && r.selectorText === ":root")
|
|
267
|
+
for (const name of Array.from(r.style))
|
|
268
|
+
if (name.startsWith("--look-") || name.startsWith("--kit-footer-"))
|
|
269
|
+
published.add(name);
|
|
270
|
+
}
|
|
271
|
+
for (const k of new Set([...setVars.current, ...published]))
|
|
272
|
+
if (!(k in vars))
|
|
273
|
+
style.setProperty(k, "initial");
|
|
274
|
+
for (const [k, v] of Object.entries(vars))
|
|
275
|
+
style.setProperty(k, v);
|
|
276
|
+
setVars.current = new Set(Object.keys(vars));
|
|
142
277
|
}, [shown, look]);
|
|
143
278
|
if (!shown)
|
|
144
279
|
return null;
|
|
145
280
|
const page = shown.site.pages.find((p) => p.path === shown.path) ?? shown.site.pages[0];
|
|
146
281
|
if (!page)
|
|
147
282
|
return null;
|
|
148
|
-
return (_jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round));
|
|
283
|
+
return (_jsx(InPreview.Provider, { value: true, children: _jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round) }));
|
|
149
284
|
}
|
|
150
285
|
/** The pictures this build made (finish writes the list into this page), by path. */
|
|
151
286
|
let built = null;
|
|
@@ -174,35 +309,100 @@ function asBuilt(images) {
|
|
|
174
309
|
const pageIndex = (m) => m.site.pages.findIndex((p) => p.path === m.path);
|
|
175
310
|
/** What's in `main` without being drawn: a page's structured data (JSON-LD) and the like. */
|
|
176
311
|
const UNDRAWN = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "LINK", "META"]);
|
|
177
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* What each of the page's blocks drew, one list for each block (a block may draw nothing, or more than
|
|
314
|
+
* one). Exactly, where the site marks where each block starts (BlockMark); otherwise `main`'s
|
|
315
|
+
* children, found by their words when they aren't one for each block.
|
|
316
|
+
*/
|
|
178
317
|
function blockElements(m) {
|
|
179
318
|
const main = document.querySelector("main");
|
|
180
319
|
const page = m.site.pages[pageIndex(m)];
|
|
181
320
|
if (!main || !page)
|
|
182
321
|
return null;
|
|
322
|
+
const marked = marks(main, page.blocks.length);
|
|
323
|
+
if (marked)
|
|
324
|
+
return marked;
|
|
183
325
|
const kids = [...main.children].filter((e) => !UNDRAWN.has(e.tagName));
|
|
184
|
-
|
|
326
|
+
const words = (el) => [el.innerText ?? el.textContent ?? "", ...[...el.querySelectorAll("img[alt]")].map((i) => i.getAttribute("alt") ?? "")].join(" ");
|
|
327
|
+
const owner = matchBlocks(page.blocks, kids.length === page.blocks.length ? kids.map(() => "") : kids.map(words));
|
|
328
|
+
return page.blocks.map((_, i) => kids.filter((_, j) => owner[j] === i));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The page's blocks with something drawn past the left or right edge of the screen (words running
|
|
332
|
+
* out of a card, a row too wide for a phone), by index. Something inside a box that clips what it
|
|
333
|
+
* holds on purpose (a slideshow's other slides, a scrolling row) doesn't count.
|
|
334
|
+
*/
|
|
335
|
+
function overflowing(m) {
|
|
336
|
+
const blocks = blockElements(m);
|
|
337
|
+
if (!blocks)
|
|
338
|
+
return { blocks: [], what: [] };
|
|
339
|
+
const edge = document.documentElement.clientWidth;
|
|
340
|
+
const clips = (el, root) => {
|
|
341
|
+
for (let at = el.parentElement; at && at !== root.parentElement; at = at.parentElement) {
|
|
342
|
+
const c = getComputedStyle(at);
|
|
343
|
+
if (c.overflowX !== "visible" || c.overflow !== "visible")
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
};
|
|
348
|
+
// What it was, said with the block (the element and its words), for whoever's looking into it.
|
|
349
|
+
const what = [];
|
|
350
|
+
const out = (root) => [root, ...root.querySelectorAll("*")].some((el) => {
|
|
351
|
+
const r = el.getBoundingClientRect();
|
|
352
|
+
if (!r.width || !r.height || getComputedStyle(el).visibility === "hidden")
|
|
353
|
+
return false;
|
|
354
|
+
// Across the edge, not wholly off the screen (a field kept out of sight on purpose).
|
|
355
|
+
const over = ((r.right > edge + 1 && r.left < edge) || (r.left < -1 && r.right > 0)) && !clips(el, root);
|
|
356
|
+
if (over)
|
|
357
|
+
what.push(`${el.tagName.toLowerCase()}${typeof el.className === "string" && el.className ? `.${el.className.trim().split(/\s+/).join(".")}` : ""} ${Math.round(r.left)}–${Math.round(r.right)} of ${edge}`);
|
|
358
|
+
return over;
|
|
359
|
+
});
|
|
360
|
+
return { blocks: blocks.flatMap((els, i) => (els.some(out) ? [i] : [])), what };
|
|
361
|
+
}
|
|
362
|
+
/** Each block's elements by the marks before them: what's drawn after a block's mark, up to the next. Null where there are none. */
|
|
363
|
+
function marks(main, count) {
|
|
364
|
+
const first = main.querySelector(`template[${BLOCK_MARK}]`);
|
|
365
|
+
if (!first?.parentElement)
|
|
366
|
+
return null;
|
|
367
|
+
const out = Array.from({ length: count }, () => []);
|
|
368
|
+
let at = -1;
|
|
369
|
+
for (const el of first.parentElement.children) {
|
|
370
|
+
const mark = el.tagName === "TEMPLATE" ? el.getAttribute(BLOCK_MARK) : null;
|
|
371
|
+
if (mark !== null)
|
|
372
|
+
at = Number(mark);
|
|
373
|
+
else if (at >= 0 && at < count && !UNDRAWN.has(el.tagName))
|
|
374
|
+
out[at].push(el);
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
185
377
|
}
|
|
378
|
+
const holds = (d, target) => !!d?.some((el) => el.contains(target));
|
|
186
379
|
function blockElement(m) {
|
|
187
380
|
if (!m?.selected)
|
|
188
381
|
return null;
|
|
189
382
|
const [k, p, b, n] = m.selected;
|
|
190
383
|
if (k !== "pages" || p !== pageIndex(m) || b !== "blocks" || typeof n !== "number")
|
|
191
384
|
return null;
|
|
192
|
-
|
|
385
|
+
const d = blockElements(m)?.[n];
|
|
386
|
+
return d?.length ? d : null;
|
|
193
387
|
}
|
|
194
388
|
/** The words of the text under the pointer: a name set beside other things in one element (a caption with an email and a phone) is still just the name. */
|
|
195
389
|
function wordsAt(x, y) {
|
|
196
390
|
const doc = document;
|
|
197
391
|
const node = doc.caretPositionFromPoint?.(x, y)?.offsetNode ?? doc.caretRangeFromPoint?.(x, y)?.startContainer;
|
|
198
|
-
|
|
392
|
+
if (node?.nodeType !== Node.TEXT_NODE)
|
|
393
|
+
return null;
|
|
394
|
+
// The caret snaps to the nearest text, even across empty space: only words actually under the pointer count.
|
|
395
|
+
const range = document.createRange();
|
|
396
|
+
range.selectNodeContents(node);
|
|
397
|
+
const under = [...range.getClientRects()].some((r) => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom);
|
|
398
|
+
return under ? (node.textContent?.trim() ?? null) || null : null;
|
|
199
399
|
}
|
|
200
400
|
/** What a click on `target` meant: the field, and the block it's in. `words`: the text right under the pointer, tried first. */
|
|
201
401
|
function pick(target, m, words = null) {
|
|
202
402
|
const pi = pageIndex(m);
|
|
203
403
|
const page = m.site.pages[pi];
|
|
204
404
|
const blocks = blockElements(m);
|
|
205
|
-
const n = blocks ? blocks.findIndex((
|
|
405
|
+
const n = blocks ? blocks.findIndex((d) => holds(d, target)) : -1;
|
|
206
406
|
const block = n >= 0 ? ["pages", pi, "blocks", n] : null;
|
|
207
407
|
const scope = n >= 0 ? page.blocks[n] : null;
|
|
208
408
|
// What the whole site shares (its founders, its case studies), which a section may show too; the
|
|
@@ -216,6 +416,16 @@ function pick(target, m, words = null) {
|
|
|
216
416
|
if (found)
|
|
217
417
|
return { path: found, block: blockOf(found) ?? block };
|
|
218
418
|
}
|
|
419
|
+
// An icon part has no words to find it by: its name, and which of the section's icons of that name it is.
|
|
420
|
+
const icon = target.closest("svg[data-icon]");
|
|
421
|
+
if (icon && scope && n >= 0) {
|
|
422
|
+
const name = icon.getAttribute("data-icon");
|
|
423
|
+
const found = iconParts(scope, block).filter((x) => x.name === name);
|
|
424
|
+
const drawn = blocks[n].flatMap((el) => [...(el.matches("svg[data-icon]") ? [el] : []), ...el.querySelectorAll("svg[data-icon]")]).filter((el) => el.getAttribute("data-icon") === name);
|
|
425
|
+
const at = found[Math.max(0, drawn.indexOf(icon))] ?? found[0];
|
|
426
|
+
if (at)
|
|
427
|
+
return { path: at.path, block };
|
|
428
|
+
}
|
|
219
429
|
if (words) {
|
|
220
430
|
// Outside a section, a menu made from the pages' menu labels is any page's label.
|
|
221
431
|
const labels = { pages: m.site.pages.map((p) => ({ menu: p.menu })) };
|
|
@@ -223,6 +433,16 @@ function pick(target, m, words = null) {
|
|
|
223
433
|
if (exact)
|
|
224
434
|
return { path: exact, block: blockOf(exact) ?? block };
|
|
225
435
|
}
|
|
436
|
+
// The header (outside every section) away from its words and pictures, its own empty space: its
|
|
437
|
+
// settings in the Look, not whatever words its parents hold (the business's name).
|
|
438
|
+
// The phone's menu button: its own settings (what it shows, its frame), with the header's.
|
|
439
|
+
if (n < 0 && target.closest(".kit-menu-narrow > summary"))
|
|
440
|
+
return { path: ["look", "header", "menuShows"], block: null };
|
|
441
|
+
if (n < 0 && target.closest("header") && !words && !target.closest("a, button, summary, img"))
|
|
442
|
+
return { path: ["look", "header"], block: null };
|
|
443
|
+
// The kit's footer the same: its own settings (its colour), where the site's Look has them.
|
|
444
|
+
if (n < 0 && target.closest("footer.kit-footer") && !words && !target.closest("a, button, summary, img") && m.site.look?.footer)
|
|
445
|
+
return { path: ["look", "footer"], block: null };
|
|
226
446
|
// The clicked element's words, then its parents' until the block's edge: at each, the section's
|
|
227
447
|
// own fields first, then the site's shared ones, so a parent holding more of the section isn't
|
|
228
448
|
// mistaken for what was clicked.
|
|
@@ -236,12 +456,44 @@ function pick(target, m, words = null) {
|
|
|
236
456
|
if (found)
|
|
237
457
|
return { path: found, block: blockOf(found) ?? block };
|
|
238
458
|
}
|
|
239
|
-
if (n >= 0 &&
|
|
459
|
+
if (n >= 0 && blocks[n].includes(el))
|
|
240
460
|
break;
|
|
241
461
|
}
|
|
242
462
|
return { path: block, block };
|
|
243
463
|
}
|
|
464
|
+
/** A section's icon parts, in the order they're drawn: each one's path and name. */
|
|
465
|
+
function iconParts(value, base) {
|
|
466
|
+
const out = [];
|
|
467
|
+
const visit = (v, path) => {
|
|
468
|
+
if (Array.isArray(v))
|
|
469
|
+
return v.forEach((x, i) => visit(x, [...path, i]));
|
|
470
|
+
if (!v || typeof v !== "object")
|
|
471
|
+
return;
|
|
472
|
+
const o = v;
|
|
473
|
+
if (o.type === "icon" && typeof o.name === "string")
|
|
474
|
+
out.push({ path, name: o.name });
|
|
475
|
+
for (const [k, x] of Object.entries(o))
|
|
476
|
+
if (x && typeof x === "object")
|
|
477
|
+
visit(x, [...path, k]);
|
|
478
|
+
};
|
|
479
|
+
visit(value, base);
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
244
482
|
const blockOf = (path) => (path[0] === "pages" && path[2] === "blocks" && typeof path[3] === "number" ? path.slice(0, 4) : null);
|
|
483
|
+
/**
|
|
484
|
+
* Whether a picture is one a person sees: not in something hidden from them (a slideshow's other
|
|
485
|
+
* slides, stacked in the same place) and not faded out to nothing.
|
|
486
|
+
*/
|
|
487
|
+
function seen(el) {
|
|
488
|
+
for (let at = el; at && at !== document.body; at = at.parentElement) {
|
|
489
|
+
if (at.getAttribute("aria-hidden") === "true")
|
|
490
|
+
return false;
|
|
491
|
+
const c = getComputedStyle(at);
|
|
492
|
+
if (c.visibility === "hidden" || c.display === "none" || Number(c.opacity) < 0.05)
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
245
497
|
/** The smallest thing under the pointer that's words or a picture. */
|
|
246
498
|
function hoverable(el) {
|
|
247
499
|
for (let at = el; at && at !== document.body; at = at.parentElement) {
|
|
@@ -269,7 +521,8 @@ function outline(which, el, label) {
|
|
|
269
521
|
let made = boxes[which];
|
|
270
522
|
if (!made) {
|
|
271
523
|
const box = document.createElement("div");
|
|
272
|
-
|
|
524
|
+
// In the page's own coordinates: it scrolls with the page, as the page does, with nothing to redraw.
|
|
525
|
+
Object.assign(box.style, { position: "absolute", pointerEvents: "none", zIndex: "2147483647", ...looks[which] });
|
|
273
526
|
const tag = document.createElement("span");
|
|
274
527
|
Object.assign(tag.style, {
|
|
275
528
|
position: "absolute",
|
|
@@ -286,10 +539,14 @@ function outline(which, el, label) {
|
|
|
286
539
|
document.body.appendChild(box);
|
|
287
540
|
made = boxes[which] = { box, tag };
|
|
288
541
|
}
|
|
289
|
-
|
|
542
|
+
const els = el === null ? [] : Array.isArray(el) ? el : [el];
|
|
543
|
+
if (!els.length)
|
|
290
544
|
return void (made.box.style.display = "none");
|
|
291
|
-
|
|
292
|
-
|
|
545
|
+
// Around all of it: a block that drew more than one thing is outlined as one.
|
|
546
|
+
const rs = els.map((e) => e.getBoundingClientRect());
|
|
547
|
+
const top = Math.min(...rs.map((x) => x.top)), left = Math.min(...rs.map((x) => x.left));
|
|
548
|
+
const r = { top, left, width: Math.max(...rs.map((x) => x.right)) - left, height: Math.max(...rs.map((x) => x.bottom)) - top };
|
|
549
|
+
Object.assign(made.box.style, { display: "block", top: `${r.top + window.scrollY}px`, left: `${r.left + window.scrollX}px`, width: `${r.width}px`, height: `${r.height}px` });
|
|
293
550
|
made.tag.textContent = label ?? "";
|
|
294
551
|
made.tag.style.display = label ? "block" : "none";
|
|
295
552
|
}
|
package/dist/next/sections.d.ts
CHANGED
|
@@ -3,15 +3,30 @@ export type KitBlock = {
|
|
|
3
3
|
id?: string;
|
|
4
4
|
placement?: Record<string, unknown>;
|
|
5
5
|
} & Record<string, unknown>;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* The styles for each thing a kit section draws. A kit section follows the site's Look (its theme:
|
|
8
|
+
* type scale, colours, buttons, width and spacing, as --look-* variables) with the kit's own classes
|
|
9
|
+
* (sections.css), so it looks right on any site with none of these given. A site may still name its
|
|
10
|
+
* own for any of them, in place of the kit's: `headingLarge` a section's own heading, `headingMedium`
|
|
11
|
+
* a heading under it, `headingSmall` the smallest; `text` body text, `muted` quieter text beside it (a
|
|
12
|
+
* price's "per month", a fact's label, who a quote is from).
|
|
13
|
+
*/
|
|
7
14
|
export type KitClasses = {
|
|
8
|
-
section
|
|
9
|
-
wrap
|
|
10
|
-
|
|
15
|
+
section: string;
|
|
16
|
+
wrap: string;
|
|
17
|
+
headingLarge: string;
|
|
18
|
+
headingMedium: string;
|
|
19
|
+
headingSmall: string;
|
|
20
|
+
text: string;
|
|
21
|
+
muted: string;
|
|
22
|
+
button: string;
|
|
23
|
+
buttonQuiet: string;
|
|
11
24
|
};
|
|
25
|
+
/** The kit's own classes, drawn from the Look by sections.css. */
|
|
26
|
+
export declare const KIT_CLASSES: KitClasses;
|
|
12
27
|
/** A block of one of the kit's types, drawn; null for any other (the site's own to draw). */
|
|
13
|
-
export declare function KitSection({ s, level, classes }: {
|
|
28
|
+
export declare function KitSection({ s, level, classes: given }: {
|
|
14
29
|
s: KitBlock;
|
|
15
30
|
level?: number;
|
|
16
|
-
classes?: KitClasses
|
|
31
|
+
classes?: Partial<KitClasses>;
|
|
17
32
|
}): import("react").JSX.Element | null;
|