@caltho/site-kit 0.2.11 → 0.3.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 +84 -3
- package/dist/behaviors/accordion.d.ts +4 -0
- package/dist/behaviors/accordion.js +56 -0
- package/dist/behaviors/carousel.d.ts +4 -0
- package/dist/behaviors/carousel.js +127 -0
- package/dist/behaviors/filter.d.ts +1 -0
- package/dist/behaviors/filter.js +25 -0
- package/dist/behaviors/index.d.ts +14 -0
- package/dist/behaviors/index.js +78 -0
- package/dist/behaviors/motion.d.ts +13 -0
- package/dist/behaviors/motion.js +88 -0
- package/dist/behaviors/tabs.d.ts +3 -0
- package/dist/behaviors/tabs.js +55 -0
- package/dist/built/css.d.ts +2 -0
- package/dist/built/css.js +26 -0
- package/dist/built/theme.d.ts +19 -0
- package/dist/built/theme.js +86 -0
- package/dist/built/tree.d.ts +24 -0
- package/dist/built/tree.js +96 -0
- package/dist/built.test.d.ts +1 -0
- package/dist/built.test.js +257 -0
- package/dist/consent.d.ts +33 -0
- package/dist/consent.js +36 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +61 -0
- package/dist/content/index.d.ts +170 -40
- package/dist/content/index.js +155 -0
- package/dist/content/lists.d.ts +69 -0
- package/dist/content/lists.js +77 -0
- package/dist/content/lists.test.d.ts +1 -0
- package/dist/content/lists.test.js +257 -0
- package/dist/content/look.d.ts +13 -13
- package/dist/content-migrate.d.ts +51 -0
- package/dist/content-migrate.js +69 -0
- package/dist/finish.d.ts +1 -1
- package/dist/finish.js +18 -5
- package/dist/frame.test.js +25 -0
- package/dist/next/built.d.ts +40 -0
- package/dist/next/built.js +318 -0
- package/dist/next/consent.d.ts +4 -0
- package/dist/next/consent.js +13 -0
- package/dist/next/data.d.ts +2 -2
- package/dist/next/data.js +6 -4
- package/dist/next/frame.d.ts +9 -1
- package/dist/next/frame.js +5 -2
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +2 -0
- package/dist/next/metadata.js +3 -2
- package/dist/next/parts/Img.d.ts +6 -1
- package/dist/next/parts/Img.js +11 -3
- package/dist/next/preview.js +242 -15
- package/dist/next/sections.js +5 -0
- package/dist/policies.js +22 -4
- package/dist/prepare.d.ts +10 -0
- package/dist/prepare.js +67 -3
- package/dist/preview-match.d.ts +21 -0
- package/dist/preview-match.js +25 -0
- package/dist/schema.js +3 -1
- package/dist/sections/icons.d.ts +2 -0
- package/dist/sections/icons.js +4 -0
- package/dist/sections/index.d.ts +38 -16
- package/dist/sections/index.js +7 -2
- package/dist/sections.css +324 -0
- package/dist/seo.js +2 -1
- package/dist/tree/behaviors.d.ts +50 -0
- package/dist/tree/behaviors.js +67 -0
- package/dist/tree/classes.d.ts +24 -0
- package/dist/tree/classes.js +94 -0
- package/dist/tree/index.d.ts +145 -0
- package/dist/tree/index.js +196 -0
- package/dist/tree/messages.d.ts +45 -0
- package/dist/tree/messages.js +7 -0
- package/package.json +15 -1
package/dist/next/parts/Img.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// A picture from the site's images, at the widths `site-kit prepare` made: `sizes` says how wide
|
|
3
3
|
// it shows, the browser picks the copy. Cropped to a shape by class (`ratio-4-3`), not by a
|
|
4
4
|
// style attribute, which the content security policy would block. Above the fold (`eager`) it
|
|
5
5
|
// loads first; everywhere else it waits until it's near.
|
|
6
6
|
import { image } from "../data.js";
|
|
7
|
-
|
|
7
|
+
/** `node`: a built section's node id, marked on the picture in CalOS's preview (data-node) so a click on it names the node. */
|
|
8
|
+
/** Below this width a picture's phone version (`phoneSrc`) is shown in its place. */
|
|
9
|
+
export const PHONE_WIDTH = 700;
|
|
10
|
+
export function Img({ src, alt, sizes, ratio, eager = false, className, node, phoneSrc }) {
|
|
8
11
|
const m = image(src);
|
|
9
12
|
const [rw, rh] = ratio ? ratio.split("/").map(Number) : [m.width, m.height];
|
|
10
13
|
const width = m.width;
|
|
11
14
|
const height = Math.round((width * rh) / rw);
|
|
12
15
|
const classes = [ratio ? `ratio-${ratio.replace("/", "-")}` : null, className].filter(Boolean).join(" ") || undefined;
|
|
13
|
-
|
|
16
|
+
const img = (_jsx("img", { src: m.src, srcSet: m.srcset.length ? m.srcset.map((s) => `${s.url} ${s.w}w`).join(", ") : undefined, sizes: m.srcset.length ? sizes : undefined, width: width, height: height, alt: alt, loading: eager ? "eager" : "lazy", decoding: eager ? "sync" : "async", fetchPriority: eager ? "high" : undefined, className: classes, "data-node": node }));
|
|
17
|
+
if (!phoneSrc)
|
|
18
|
+
return img;
|
|
19
|
+
// The phone version, at its own widths, chosen by the browser on a small screen (the <img> stays the picture: its class and alt).
|
|
20
|
+
const p = image(phoneSrc);
|
|
21
|
+
return (_jsxs("picture", { "data-node": node, children: [_jsx("source", { media: `(max-width: ${PHONE_WIDTH}px)`, srcSet: p.srcset.length ? p.srcset.map((x) => `${x.url} ${x.w}w`).join(", ") : p.src, sizes: "100vw", width: p.width, height: p.height }), img] }));
|
|
14
22
|
}
|
package/dist/next/preview.js
CHANGED
|
@@ -10,10 +10,17 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
// page → CalOS: { type: "calos:ready" }, { type: "calos:pick", path, block },
|
|
11
11
|
// { type: "calos:error", message }, { type: "calos:overflow", blocks } (the page's
|
|
12
12
|
// blocks with something past the screen's edge at this width, by index)
|
|
13
|
+
// Built sections (../tree/messages.ts): calos:built-select, -hover and -boxes to CalOS;
|
|
14
|
+
// calos:built-highlight and -state from it.
|
|
15
|
+
import { allPages } from "../content/lists.js";
|
|
13
16
|
import { Component, useEffect, useRef, useState } from "react";
|
|
14
17
|
import { lookVars } from "../content/look.js";
|
|
15
18
|
import { fontFiles, googleFont } from "../fonts-google.js";
|
|
16
|
-
import { BUILT_IMAGES, fieldForImage, fieldForText, matchBlocks } from "../preview-match.js";
|
|
19
|
+
import { BUILT_IMAGES, fieldForImage, fieldForText, isShown, matchBlocks, withoutVariant } from "../preview-match.js";
|
|
20
|
+
import { showWithin, startBehaviors } from "../behaviors/index.js";
|
|
21
|
+
import { swatchSlugs, tailwindInput } from "../built/theme.js";
|
|
22
|
+
import { hasBuilt } from "../built/tree.js";
|
|
23
|
+
import { NODE_ATTR } from "../tree/index.js";
|
|
17
24
|
import { BLOCK_MARK, InPreview } from "./block-mark.js";
|
|
18
25
|
import { setPrepared } from "./data.js";
|
|
19
26
|
/** `labels`: what CalOS calls each of the page's blocks, shown on its outline. */
|
|
@@ -72,6 +79,15 @@ export function SitePreview({ origins, render, look }) {
|
|
|
72
79
|
const onMessage = (e) => {
|
|
73
80
|
if (!origins.includes(e.origin) || e.source !== window.parent)
|
|
74
81
|
return;
|
|
82
|
+
const said = e.data;
|
|
83
|
+
if (said?.type === "calos:built-highlight" && latest.current) {
|
|
84
|
+
highlight = { block: said.block, nodes: Array.isArray(said.nodes) ? said.nodes.filter((x) => typeof x === "string") : [] };
|
|
85
|
+
return drawHighlight(latest.current, true);
|
|
86
|
+
}
|
|
87
|
+
if (said?.type === "calos:built-state" && latest.current) {
|
|
88
|
+
forced = said.state === "normal" ? null : { block: said.block, node: said.node, state: said.state };
|
|
89
|
+
return applyState(latest.current);
|
|
90
|
+
}
|
|
75
91
|
const sent = e.data;
|
|
76
92
|
if (sent?.type !== "calos:show")
|
|
77
93
|
return;
|
|
@@ -106,11 +122,29 @@ export function SitePreview({ origins, render, look }) {
|
|
|
106
122
|
const target = e.target;
|
|
107
123
|
if (!m || !target)
|
|
108
124
|
return;
|
|
125
|
+
// A built section's own controls (a carousel's dots and arrows, which aren't nodes) work as on
|
|
126
|
+
// the site, so a slide can be brought up to edit.
|
|
127
|
+
if (target.closest(".kit-carousel-controls button, button[data-kit-to]"))
|
|
128
|
+
return;
|
|
109
129
|
e.preventDefault();
|
|
110
130
|
e.stopPropagation();
|
|
131
|
+
// Only what a person sees is what they clicked: the top element under the pointer that's shown,
|
|
132
|
+
// not a slide faded out over it (a carousel's slides are stacked in one place, the hidden ones
|
|
133
|
+
// still under the pointer), nor the words of one.
|
|
134
|
+
const stack = document.elementsFromPoint(e.clientX, e.clientY);
|
|
135
|
+
const shownEl = stack.find(seen) ?? target;
|
|
136
|
+
// A built section's element names its node: exact, nothing guessed.
|
|
137
|
+
const node = shownEl.closest(`[${NODE_ATTR}]`);
|
|
138
|
+
if (node) {
|
|
139
|
+
const n = blockElements(m)?.findIndex((d) => holds(d, node)) ?? -1;
|
|
140
|
+
// Or in the site's own header or footer (built): -1 or -2, as the editor numbers them.
|
|
141
|
+
const at = n >= 0 ? n : frameOf(node);
|
|
142
|
+
if (at !== null)
|
|
143
|
+
return post({ type: "calos:built-select", block: at, node: node.getAttribute(NODE_ATTR), shift: e.shiftKey });
|
|
144
|
+
}
|
|
111
145
|
// A link anywhere (a section's, the menu's, the footer's) is edited, never followed: CalOS's page picker moves between pages.
|
|
112
146
|
// A picture under the pointer counts, even when something over it (a frame, a caption's box) took the click.
|
|
113
|
-
const under =
|
|
147
|
+
const under = stack.find((el) => el.tagName === "IMG" && seen(el)) ?? shownEl;
|
|
114
148
|
post({ type: "calos:pick", ...pick(under, m, wordsAt(e.clientX, e.clientY)) });
|
|
115
149
|
};
|
|
116
150
|
const onSubmit = (e) => e.preventDefault();
|
|
@@ -123,9 +157,26 @@ export function SitePreview({ origins, render, look }) {
|
|
|
123
157
|
const selected = m ? blockElement(m) : null;
|
|
124
158
|
hovered = n >= 0 ? blocks[n] : null;
|
|
125
159
|
outline("section", hovered && hovered[0] !== selected?.[0] ? hovered : null, n >= 0 ? m?.labels?.[n] : undefined);
|
|
126
|
-
|
|
160
|
+
// Over a built section, the element itself (said to CalOS, for its layers), not the words in it.
|
|
161
|
+
const shownEl = document.elementsFromPoint(e.clientX, e.clientY).find(seen) ?? target;
|
|
162
|
+
// In the site's own header or footer (built): -1 or -2, as the editor knows them.
|
|
163
|
+
const framed = n < 0 && shownEl ? shownEl.closest(`[${NODE_ATTR}]`) : null;
|
|
164
|
+
const f = framed ? frameOf(framed) : null;
|
|
165
|
+
const node = n >= 0 ? (shownEl?.closest(`[${NODE_ATTR}]`) ?? null) : f !== null ? framed : null;
|
|
166
|
+
const id = node && (f !== null || holds(blocks[n], node)) ? node.getAttribute(NODE_ATTR) : null;
|
|
167
|
+
const at = f ?? n;
|
|
168
|
+
const said = id ? `${at}:${id}` : null;
|
|
169
|
+
if (said !== hoverSaid) {
|
|
170
|
+
if (id || hoverSaid)
|
|
171
|
+
post({ type: "calos:built-hover", block: id ? at : Number(hoverSaid.split(":")[0]), node: id });
|
|
172
|
+
hoverSaid = said;
|
|
173
|
+
}
|
|
174
|
+
outline("hover", id ? node : hoverable(target));
|
|
127
175
|
};
|
|
128
176
|
const leave = () => {
|
|
177
|
+
if (hoverSaid)
|
|
178
|
+
post({ type: "calos:built-hover", block: Number(hoverSaid.split(":")[0]), node: null });
|
|
179
|
+
hoverSaid = null;
|
|
129
180
|
hovered = null;
|
|
130
181
|
outline("section", null);
|
|
131
182
|
outline("hover", null);
|
|
@@ -243,6 +294,47 @@ export function SitePreview({ origins, render, look }) {
|
|
|
243
294
|
window.removeEventListener("resize", keep);
|
|
244
295
|
};
|
|
245
296
|
}, [shown, round]);
|
|
297
|
+
// Built sections: their classes drawn as they're typed (Tailwind's browser build, given the same
|
|
298
|
+
// input as the build's compiler), their behaviors running (nothing moving by itself, so what's
|
|
299
|
+
// being edited stays put), where each element is said to CalOS after every drawing (and as the
|
|
300
|
+
// page scrolls, resizes or the classes' CSS changes), and the editor's selection and state kept.
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (!shown)
|
|
303
|
+
return;
|
|
304
|
+
const page = allPages(shown.site).find((p) => p.path === shown.path) ?? shown.site.pages[0];
|
|
305
|
+
if (!page || !hasBuilt(page.blocks))
|
|
306
|
+
return;
|
|
307
|
+
loadTailwind(tailwindInput(swatchSlugs(look ? lookVars(shown.site.look, look) : undefined)));
|
|
308
|
+
if (!behaving) {
|
|
309
|
+
behaving = true;
|
|
310
|
+
startBehaviors(true);
|
|
311
|
+
}
|
|
312
|
+
let frame = 0;
|
|
313
|
+
const soon = () => {
|
|
314
|
+
cancelAnimationFrame(frame);
|
|
315
|
+
frame = requestAnimationFrame(() => {
|
|
316
|
+
const m = latest.current ?? shown;
|
|
317
|
+
applyState(m);
|
|
318
|
+
drawHighlight(m, false);
|
|
319
|
+
for (const [block, boxes] of builtBoxes(m))
|
|
320
|
+
post({ type: "calos:built-boxes", block, boxes });
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
soon();
|
|
324
|
+
// The browser build writes the classes' CSS a moment after they're drawn: the boxes are said again then.
|
|
325
|
+
const css = new MutationObserver(soon);
|
|
326
|
+
css.observe(document.head, { childList: true, subtree: true, characterData: true });
|
|
327
|
+
void document.fonts?.ready.then(soon);
|
|
328
|
+
window.addEventListener("resize", soon);
|
|
329
|
+
window.addEventListener("scroll", soon, { passive: true });
|
|
330
|
+
return () => {
|
|
331
|
+
cancelAnimationFrame(frame);
|
|
332
|
+
css.disconnect();
|
|
333
|
+
window.removeEventListener("resize", soon);
|
|
334
|
+
window.removeEventListener("scroll", soon);
|
|
335
|
+
};
|
|
336
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
337
|
+
}, [shown, round, look]);
|
|
246
338
|
// The draft's Look, set on the page (LookStyle drew the published one into the build).
|
|
247
339
|
// One the draft no longer sets (a colour removed, the footer back as designed) is taken off again.
|
|
248
340
|
const setVars = useRef(new Set());
|
|
@@ -277,7 +369,7 @@ export function SitePreview({ origins, render, look }) {
|
|
|
277
369
|
}, [shown, look]);
|
|
278
370
|
if (!shown)
|
|
279
371
|
return null;
|
|
280
|
-
const page = shown.site.
|
|
372
|
+
const page = allPages(shown.site).find((p) => p.path === shown.path) ?? shown.site.pages[0];
|
|
281
373
|
if (!page)
|
|
282
374
|
return null;
|
|
283
375
|
return (_jsx(InPreview.Provider, { value: true, children: _jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round) }));
|
|
@@ -389,13 +481,26 @@ function blockElement(m) {
|
|
|
389
481
|
function wordsAt(x, y) {
|
|
390
482
|
const doc = document;
|
|
391
483
|
const node = doc.caretPositionFromPoint?.(x, y)?.offsetNode ?? doc.caretRangeFromPoint?.(x, y)?.startContainer;
|
|
392
|
-
if (node?.nodeType !== Node.TEXT_NODE)
|
|
393
|
-
return null;
|
|
394
484
|
// The caret snaps to the nearest text, even across empty space: only words actually under the pointer count.
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
485
|
+
const under = (t) => {
|
|
486
|
+
const range = document.createRange();
|
|
487
|
+
range.selectNodeContents(t);
|
|
488
|
+
return [...range.getClientRects()].some((r) => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom);
|
|
489
|
+
};
|
|
490
|
+
const words = (t) => (t.textContent?.trim() ?? null) || null;
|
|
491
|
+
// Words a person sees: the caret finds text in whatever's on top, a faded-out slide stacked over
|
|
492
|
+
// the one showing as much as the one showing.
|
|
493
|
+
if (node?.nodeType === Node.TEXT_NODE && node.parentElement && seen(node.parentElement))
|
|
494
|
+
return under(node) ? words(node) : null;
|
|
495
|
+
// Then the words of the shown elements under the pointer, the top one first.
|
|
496
|
+
for (const el of document.elementsFromPoint(x, y)) {
|
|
497
|
+
if (!seen(el))
|
|
498
|
+
continue;
|
|
499
|
+
const t = [...el.childNodes].find((c) => c.nodeType === Node.TEXT_NODE && c.textContent?.trim() && under(c));
|
|
500
|
+
if (t)
|
|
501
|
+
return words(t);
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
399
504
|
}
|
|
400
505
|
/** What a click on `target` meant: the field, and the block it's in. `words`: the text right under the pointer, tried first. */
|
|
401
506
|
function pick(target, m, words = null) {
|
|
@@ -485,14 +590,12 @@ const blockOf = (path) => (path[0] === "pages" && path[2] === "blocks" && typeof
|
|
|
485
590
|
* slides, stacked in the same place) and not faded out to nothing.
|
|
486
591
|
*/
|
|
487
592
|
function seen(el) {
|
|
593
|
+
const chain = [];
|
|
488
594
|
for (let at = el; at && at !== document.body; at = at.parentElement) {
|
|
489
|
-
if (at.getAttribute("aria-hidden") === "true")
|
|
490
|
-
return false;
|
|
491
595
|
const c = getComputedStyle(at);
|
|
492
|
-
|
|
493
|
-
return false;
|
|
596
|
+
chain.push({ ariaHidden: at.getAttribute("aria-hidden"), inert: at.hasAttribute("inert"), visibility: c.visibility, display: c.display, opacity: c.opacity });
|
|
494
597
|
}
|
|
495
|
-
return
|
|
598
|
+
return isShown(chain);
|
|
496
599
|
}
|
|
497
600
|
/** The smallest thing under the pointer that's words or a picture. */
|
|
498
601
|
function hoverable(el) {
|
|
@@ -504,6 +607,130 @@ function hoverable(el) {
|
|
|
504
607
|
}
|
|
505
608
|
return null;
|
|
506
609
|
}
|
|
610
|
+
// ---- built sections ----
|
|
611
|
+
/** What the editor has selected in a built section (calos:built-highlight), outlined after every drawing. */
|
|
612
|
+
let highlight = null;
|
|
613
|
+
/** A node drawn as hovered or focused while those classes are edited (calos:built-state). */
|
|
614
|
+
let forced = null;
|
|
615
|
+
/** The built node last said to be under the pointer ("block:node"). */
|
|
616
|
+
let hoverSaid = null;
|
|
617
|
+
let behaving = false;
|
|
618
|
+
let tailwind = null;
|
|
619
|
+
/**
|
|
620
|
+
* Tailwind's browser build, loaded once (a chunk of its own: only this page carries it, never a
|
|
621
|
+
* visitor's), compiling the classes on the page as they change. Its input is set first, so it
|
|
622
|
+
* never compiles its default (which resets the page's own styles): the theme and utilities only,
|
|
623
|
+
* prefixed as built sections draw them, with the draft's Look as the theme.
|
|
624
|
+
*/
|
|
625
|
+
function loadTailwind(input) {
|
|
626
|
+
let style = document.getElementById("kit-tailwind");
|
|
627
|
+
if (!style) {
|
|
628
|
+
style = document.createElement("style");
|
|
629
|
+
style.id = "kit-tailwind";
|
|
630
|
+
style.setAttribute("type", "text/tailwindcss");
|
|
631
|
+
document.head.appendChild(style);
|
|
632
|
+
}
|
|
633
|
+
if (style.textContent !== input)
|
|
634
|
+
style.textContent = input;
|
|
635
|
+
tailwind ??= import("@tailwindcss/browser").catch((err) => {
|
|
636
|
+
tailwind = null;
|
|
637
|
+
console.error("site-kit: Tailwind's browser build didn't load", err);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/** The site's own header (-1) or footer (-2), as the editor numbers them: the one a built element is in, or null. */
|
|
641
|
+
const FRAMES = { "-1": "header", "-2": "footer" };
|
|
642
|
+
function frameOf(el) {
|
|
643
|
+
const f = el.closest("[data-kit-frame]")?.getAttribute("data-kit-frame");
|
|
644
|
+
return f === "header" ? -1 : f === "footer" ? -2 : null;
|
|
645
|
+
}
|
|
646
|
+
function frameEls(block) {
|
|
647
|
+
const which = FRAMES[String(block)];
|
|
648
|
+
const el = which ? document.querySelector(`[data-kit-frame="${which}"]`) : null;
|
|
649
|
+
return el ? [el] : [];
|
|
650
|
+
}
|
|
651
|
+
/** The elements drawn for a node of a block (a repeated node has one for each item). */
|
|
652
|
+
function nodeEls(m, block, node) {
|
|
653
|
+
const els = block < 0 ? frameEls(block) : (blockElements(m)?.[block] ?? []);
|
|
654
|
+
const sel = `[${NODE_ATTR}="${CSS.escape(node)}"]`;
|
|
655
|
+
return els.flatMap((el) => [...(el.matches(sel) ? [el] : []), ...el.querySelectorAll(sel)]);
|
|
656
|
+
}
|
|
657
|
+
/** Where each built element is on screen, by block, for CalOS's overlays: only what's shown (not a slide faded out). */
|
|
658
|
+
function builtBoxes(m) {
|
|
659
|
+
const page = m.site.pages[pageIndex(m)];
|
|
660
|
+
const blocks = blockElements(m);
|
|
661
|
+
if (!page || !blocks)
|
|
662
|
+
return [];
|
|
663
|
+
const out = [];
|
|
664
|
+
page.blocks.forEach((b, i) => {
|
|
665
|
+
if (b.type !== "built" && b.type !== "use")
|
|
666
|
+
return;
|
|
667
|
+
const boxes = (blocks[i] ?? [])
|
|
668
|
+
.flatMap((el) => [...(el.hasAttribute(NODE_ATTR) ? [el] : []), ...el.querySelectorAll(`[${NODE_ATTR}]`)])
|
|
669
|
+
.filter(seen)
|
|
670
|
+
.map((el) => {
|
|
671
|
+
const r = el.getBoundingClientRect();
|
|
672
|
+
return { node: el.getAttribute(NODE_ATTR), x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
|
|
673
|
+
});
|
|
674
|
+
out.push([i, boxes]);
|
|
675
|
+
});
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
/** The selected nodes outlined; brought into view when CalOS has just selected them, and shown where a behavior hides them (another slide, tab or closed item). */
|
|
679
|
+
const highlightBoxes = [];
|
|
680
|
+
function drawHighlight(m, scroll) {
|
|
681
|
+
const els = highlight ? highlight.nodes.flatMap((id) => nodeEls(m, highlight.block, id).slice(0, 1)) : [];
|
|
682
|
+
if (scroll)
|
|
683
|
+
els.forEach((el) => showWithin(el));
|
|
684
|
+
while (highlightBoxes.length < els.length) {
|
|
685
|
+
const box = document.createElement("div");
|
|
686
|
+
Object.assign(box.style, { position: "absolute", pointerEvents: "none", zIndex: "2147483647", ...looks.selected });
|
|
687
|
+
document.body.appendChild(box);
|
|
688
|
+
highlightBoxes.push(box);
|
|
689
|
+
}
|
|
690
|
+
highlightBoxes.forEach((box, i) => {
|
|
691
|
+
const el = els[i];
|
|
692
|
+
if (!el)
|
|
693
|
+
return void (box.style.display = "none");
|
|
694
|
+
const r = el.getBoundingClientRect();
|
|
695
|
+
Object.assign(box.style, { display: "block", top: `${r.top + window.scrollY}px`, left: `${r.left + window.scrollX}px`, width: `${r.width}px`, height: `${r.height}px` });
|
|
696
|
+
});
|
|
697
|
+
const first = els[0];
|
|
698
|
+
if (scroll && first) {
|
|
699
|
+
const r = first.getBoundingClientRect();
|
|
700
|
+
if (r.top < 0 || r.bottom > window.innerHeight)
|
|
701
|
+
window.scrollTo({ top: r.top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* A node drawn as hovered or focused: each of its classes for that state (hover:, focus:; and
|
|
706
|
+
* group-hover: and the like on what's inside it when it's a group) added without the state, so it
|
|
707
|
+
* shows while its hover classes are edited. Whatever was added before is taken off first.
|
|
708
|
+
*/
|
|
709
|
+
const STATE_VARIANTS = { hover: ["hover"], focus: ["focus", "focus-visible", "focus-within"] };
|
|
710
|
+
const GROUP_VARIANTS = { hover: ["group-hover"], focus: ["group-focus", "group-focus-within"] };
|
|
711
|
+
let forcedClasses = [];
|
|
712
|
+
function applyState(m) {
|
|
713
|
+
for (const f of forcedClasses)
|
|
714
|
+
f.el.classList.remove(f.cls);
|
|
715
|
+
forcedClasses = [];
|
|
716
|
+
if (!forced)
|
|
717
|
+
return;
|
|
718
|
+
const force = (el, variants) => {
|
|
719
|
+
for (const c of [...el.classList]) {
|
|
720
|
+
const plain = withoutVariant(c, variants);
|
|
721
|
+
if (plain && !el.classList.contains(plain)) {
|
|
722
|
+
el.classList.add(plain);
|
|
723
|
+
forcedClasses.push({ el, cls: plain });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
for (const el of nodeEls(m, forced.block, forced.node)) {
|
|
728
|
+
force(el, STATE_VARIANTS[forced.state]);
|
|
729
|
+
if ([...el.classList].some((c) => /:group$/.test(c)))
|
|
730
|
+
for (const inner of el.querySelectorAll(`[${NODE_ATTR}]`))
|
|
731
|
+
force(inner, GROUP_VARIANTS[forced.state]);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
507
734
|
// Outlines drawn over the page, sized through the style object (allowed by the content security
|
|
508
735
|
// policy, where a style attribute in the markup isn't), never catching the pointer. A section's
|
|
509
736
|
// outline carries its name.
|
package/dist/next/sections.js
CHANGED
|
@@ -6,6 +6,7 @@ import { image, isDraft, lookValues } from "./data.js";
|
|
|
6
6
|
import { Img } from "./parts/Img.js";
|
|
7
7
|
import { Md } from "./parts/Md.js";
|
|
8
8
|
import { Slideshow } from "./slideshow.js";
|
|
9
|
+
import { BuiltSection, UseSection } from "./built.js";
|
|
9
10
|
/** The kit's own classes, drawn from the Look by sections.css. */
|
|
10
11
|
export const KIT_CLASSES = {
|
|
11
12
|
section: "kit-section",
|
|
@@ -28,6 +29,10 @@ export function KitSection({ s, level = 2, classes: given }) {
|
|
|
28
29
|
return _jsx(Slides, { s: s, level: level, classes: classes });
|
|
29
30
|
case "own":
|
|
30
31
|
return _jsx(Own, { s: s, level: level, classes: classes });
|
|
32
|
+
case "built":
|
|
33
|
+
return _jsx(BuiltSection, { s: s });
|
|
34
|
+
case "use":
|
|
35
|
+
return _jsx(UseSection, { s: s });
|
|
31
36
|
default:
|
|
32
37
|
return null;
|
|
33
38
|
}
|
package/dist/policies.js
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
// nothing said in its place: what to say of the counting is the business's, in `extra` (CalOS
|
|
7
7
|
// suggests a paragraph as they turn it on). Plain language, US-first; a business adds its own
|
|
8
8
|
// paragraphs with `extra`, and should have a lawyer read them once.
|
|
9
|
+
import { asksConsent } from "./consent.js";
|
|
10
|
+
/** Whether any page (a built section, the site's own header or footer) has a form of its own built in. */
|
|
11
|
+
function hasBuiltForm(site) {
|
|
12
|
+
const seen = new WeakSet();
|
|
13
|
+
const visit = (v) => {
|
|
14
|
+
if (!v || typeof v !== "object" || seen.has(v))
|
|
15
|
+
return false;
|
|
16
|
+
seen.add(v);
|
|
17
|
+
if (!Array.isArray(v) && v.tag === "form")
|
|
18
|
+
return true;
|
|
19
|
+
return Object.values(v).some(visit);
|
|
20
|
+
};
|
|
21
|
+
return visit(site.pages) || visit(site.ownHeader) || visit(site.ownFooter) || visit(site.components);
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* `collectsFromForm`: the site has a form that takes a visitor's name, email and message, which the
|
|
11
25
|
* privacy policy says (it defaults to the site having a form address).
|
|
@@ -15,8 +29,10 @@ export function policy(site, which, o = {}) {
|
|
|
15
29
|
const host = new URL(site.url).host.replace(/^www\./, "");
|
|
16
30
|
const reach = [b.email ? `email [${b.email}](mailto:${b.email})` : null, b.phone ? `call ${b.phone}` : null].filter(Boolean).join(" or ");
|
|
17
31
|
const place = b.address ? `${b.address.locality}, ${b.address.region}` : null;
|
|
18
|
-
const hasForm = o.collectsFromForm ?? !!site.form;
|
|
32
|
+
const hasForm = o.collectsFromForm ?? (!!site.form || hasBuiltForm(site));
|
|
19
33
|
const counts = !!site.countsVisits;
|
|
34
|
+
// Asking before counting (the site's consent setting): said, in place of the claim of no cookies.
|
|
35
|
+
const asks = asksConsent(site);
|
|
20
36
|
if (which === "privacy") {
|
|
21
37
|
return {
|
|
22
38
|
title: "Privacy policy",
|
|
@@ -31,9 +47,11 @@ export function policy(site, which, o = {}) {
|
|
|
31
47
|
]
|
|
32
48
|
.filter(Boolean)
|
|
33
49
|
.join("\n"),
|
|
34
|
-
|
|
35
|
-
? `This site doesn't use
|
|
36
|
-
:
|
|
50
|
+
asks
|
|
51
|
+
? `This site doesn't use advertising trackers. ${counts ? "It counts visits to see what's useful, but only if you agree when it asks (the banner when you first visit); until then, nothing is counted." : "It asks before using anything to count visits (the banner when you first visit), and uses nothing until you agree."} Your choice is kept in your own browser, and you can change it at any time. Its fonts and pictures come from this site itself.`
|
|
52
|
+
: counts
|
|
53
|
+
? `This site doesn't use cookies or advertising trackers. Its fonts and pictures come from this site itself.`
|
|
54
|
+
: `This site doesn't use cookies, advertising or analytics trackers. Its fonts and pictures come from this site itself, so visiting it doesn't tell anyone else you were here.`,
|
|
37
55
|
`## How we use it`,
|
|
38
56
|
`To answer you, to quote for and do the work you ask about, and to keep the records the law requires. We keep enquiries in our customer records system, which Caltho runs for us. We don't sell or rent your information, and we don't share it for advertising. The companies that help us run this site and our business (hosting, email, records) only use it to do that for us.`,
|
|
39
57
|
`## How long we keep it`,
|
package/dist/prepare.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Site } from "./content/index.ts";
|
|
2
|
+
import type { BehaviorName } from "./tree/index.ts";
|
|
2
3
|
export type PreparedImage = {
|
|
3
4
|
width: number;
|
|
4
5
|
height: number;
|
|
@@ -31,6 +32,11 @@ export type Prepared = {
|
|
|
31
32
|
look?: Record<string, string>;
|
|
32
33
|
/** The @font-face rules for the Google fonts its Look uses, their files fetched into public/_fonts (LookStyle writes them). */
|
|
33
34
|
fontFaces?: string;
|
|
35
|
+
/** Built sections' CSS and their behaviors' scripts, as the site serves them (public/_kit), where its content has any: a page with a built section links them. */
|
|
36
|
+
built?: {
|
|
37
|
+
css?: string;
|
|
38
|
+
scripts?: Partial<Record<BehaviorName, string>>;
|
|
39
|
+
};
|
|
34
40
|
};
|
|
35
41
|
/** A site's visits counted: the id its pages send them with, where they're sent, and the script that sends them. */
|
|
36
42
|
export type Analytics = {
|
|
@@ -39,6 +45,10 @@ export type Analytics = {
|
|
|
39
45
|
script: string;
|
|
40
46
|
};
|
|
41
47
|
/** What CalOS says of the site's analytics, taken only when it's whole and its addresses are https (or a laptop's). */
|
|
48
|
+
/** The business's forms' addresses CalOS sends, by key: only http(s) addresses under plain keys. */
|
|
49
|
+
export declare function formsIn(body: {
|
|
50
|
+
forms?: unknown;
|
|
51
|
+
} | null): Record<string, string> | undefined;
|
|
42
52
|
export declare function analyticsIn(body: {
|
|
43
53
|
analytics?: unknown;
|
|
44
54
|
} | null): Analytics | undefined;
|
package/dist/prepare.js
CHANGED
|
@@ -13,9 +13,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
13
13
|
}
|
|
14
14
|
return path;
|
|
15
15
|
};
|
|
16
|
+
import { allPages } from "./content/lists.js";
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
17
18
|
import { existsSync } from "node:fs";
|
|
18
|
-
import { copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
19
|
+
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
19
20
|
import { dirname, extname, join, relative } from "node:path";
|
|
20
21
|
import sharp from "sharp";
|
|
21
22
|
import { pathToFileURL } from "node:url";
|
|
@@ -23,7 +24,20 @@ import { lookVars, uploadedFontCss } from "./content/index.js";
|
|
|
23
24
|
import { fontFaceCss, fontFiles, googleFont, servedAt } from "./fonts-google.js";
|
|
24
25
|
import { imagesIn, shareImageOf } from "./images.js";
|
|
25
26
|
import { makeCards } from "./share.js";
|
|
27
|
+
import { behaviorScript } from "./behaviors/index.js";
|
|
28
|
+
import { builtCss } from "./built/css.js";
|
|
29
|
+
import { migrateContent } from "./content-migrate.js";
|
|
30
|
+
import { swatchSlugs } from "./built/theme.js";
|
|
31
|
+
import { behaviorsIn, classesIn } from "./built/tree.js";
|
|
26
32
|
/** What CalOS says of the site's analytics, taken only when it's whole and its addresses are https (or a laptop's). */
|
|
33
|
+
/** The business's forms' addresses CalOS sends, by key: only http(s) addresses under plain keys. */
|
|
34
|
+
export function formsIn(body) {
|
|
35
|
+
const f = body?.forms;
|
|
36
|
+
if (!f || typeof f !== "object" || Array.isArray(f))
|
|
37
|
+
return undefined;
|
|
38
|
+
const out = Object.fromEntries(Object.entries(f).filter(([k, v]) => /^[a-z0-9][a-z0-9-]{0,59}$/.test(k) && typeof v === "string" && /^https?:\/\//i.test(v) && URL.canParse(v)).map(([k, v]) => [k, new URL(v).href]));
|
|
39
|
+
return Object.keys(out).length ? out : undefined;
|
|
40
|
+
}
|
|
27
41
|
export function analyticsIn(body) {
|
|
28
42
|
const a = body?.analytics;
|
|
29
43
|
const address = (v) => typeof v === "string" && /^(https:\/\/|http:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/?)/i.test(v) && URL.canParse(v);
|
|
@@ -43,6 +57,7 @@ export async function prepare(root, env = process.env, log = console.log) {
|
|
|
43
57
|
let from;
|
|
44
58
|
let editors = [];
|
|
45
59
|
let analytics;
|
|
60
|
+
let forms;
|
|
46
61
|
// A site CalOS has nothing published for yet (just made, its content not brought in) builds from
|
|
47
62
|
// its own until there is: 404 is "nothing published", any other refusal is an error.
|
|
48
63
|
const res = env.CALOS_CONTENT_URL
|
|
@@ -64,6 +79,7 @@ export async function prepare(root, env = process.env, log = console.log) {
|
|
|
64
79
|
throw new Error(`Couldn't get the content from CalOS (${env.CALOS_CONTENT_URL}): ${res.status}`);
|
|
65
80
|
const body = (await res.json());
|
|
66
81
|
raw = body.content;
|
|
82
|
+
forms = formsIn(body);
|
|
67
83
|
editors = editorsIn(body);
|
|
68
84
|
analytics = analyticsIn(body);
|
|
69
85
|
from = "CalOS";
|
|
@@ -90,6 +106,13 @@ export async function prepare(root, env = process.env, log = console.log) {
|
|
|
90
106
|
from = "content/site.json";
|
|
91
107
|
}
|
|
92
108
|
const definition = await loadDefinition(root);
|
|
109
|
+
// Content in an older format than this kit's (CalOS hasn't moved it yet) is moved first, as CalOS would.
|
|
110
|
+
const moved = migrateContent((raw ?? {}));
|
|
111
|
+
if (!moved.ok)
|
|
112
|
+
throw new Error(`The site's content couldn't be moved to this kit's format (${moved.error.id}${moved.error.path.length ? ` at ${moved.error.path.join(".")}` : ""}): ${moved.error.message}`);
|
|
113
|
+
if (moved.applied.length)
|
|
114
|
+
log(`site-kit: content moved from format ${moved.from} to ${moved.format} (${moved.applied.map((a) => a.id).join(", ")})`);
|
|
115
|
+
raw = moved.content;
|
|
93
116
|
const parsed = definition.parse(raw);
|
|
94
117
|
if (parsed.problems)
|
|
95
118
|
throw new Error(`The site's content needs fixing (${from}):\n - ${parsed.problems.join("\n - ")}`);
|
|
@@ -99,6 +122,11 @@ export async function prepare(root, env = process.env, log = console.log) {
|
|
|
99
122
|
site.countsVisits = true;
|
|
100
123
|
else
|
|
101
124
|
delete site.countsVisits;
|
|
125
|
+
// Its built forms' addresses, by the business's forms' keys (never in content).
|
|
126
|
+
if (forms)
|
|
127
|
+
site.forms = forms;
|
|
128
|
+
else
|
|
129
|
+
delete site.forms;
|
|
102
130
|
for (const w of parsed.warnings)
|
|
103
131
|
log(`site-kit: warning: ${w}`);
|
|
104
132
|
const schema = definition.jsonSchema();
|
|
@@ -111,15 +139,51 @@ export async function prepare(root, env = process.env, log = console.log) {
|
|
|
111
139
|
const preferPicture = !!definition.share && definition.share.prefer === "picture";
|
|
112
140
|
const look = definition.look ? lookVars(site.look, definition.look) : undefined;
|
|
113
141
|
const fontFaces = definition.look ? [await googleFontsFor(site.look, definition.look, pub, log), await uploadedFontsFor(site.look, imagesDir, pub, log)].filter(Boolean).join("\n") : "";
|
|
114
|
-
const
|
|
142
|
+
const built = await builtAssets(site, look, pub, log);
|
|
143
|
+
const prepared = { site, images, csp: definition.csp, cards, ...(look ? { look } : {}), ...(built ? { built } : {}), ...(fontFaces ? { fontFaces } : {}), ...(preferPicture ? { preferPicture } : {}), ...(editors.length ? { editors } : {}), ...(analytics ? { analytics } : {}) };
|
|
115
144
|
await writeFile(join(out, "prepared.json"), JSON.stringify(prepared));
|
|
116
145
|
log(`site-kit: content from ${from}, ${site.pages.length} pages, ${Object.keys(images).length} images${analytics ? ", visits counted" : ""}`);
|
|
117
146
|
return prepared;
|
|
118
147
|
}
|
|
148
|
+
// ---- built sections ----
|
|
149
|
+
/**
|
|
150
|
+
* What built sections need on the published site, where the content has any: the CSS for exactly
|
|
151
|
+
* the classes their trees use (Tailwind's compiler, the Look as its theme, no reset, ./built/css.ts)
|
|
152
|
+
* and the script of each behavior they use (./behaviors), written to public/_kit named by their
|
|
153
|
+
* content, so they cache for a year. A page links them only where it has a built section
|
|
154
|
+
* (./next/built.tsx). Made again every build; what an earlier build wrote is cleared first.
|
|
155
|
+
*/
|
|
156
|
+
async function builtAssets(site, look, pub, log) {
|
|
157
|
+
const dir = join(pub, "_kit");
|
|
158
|
+
await rm(dir, { recursive: true, force: true });
|
|
159
|
+
const classes = classesIn(site);
|
|
160
|
+
const behaviors = behaviorsIn(site);
|
|
161
|
+
if (!classes.size && !behaviors.size)
|
|
162
|
+
return undefined;
|
|
163
|
+
await mkdir(dir, { recursive: true });
|
|
164
|
+
const named = (body) => createHash("sha256").update(body).digest("hex").slice(0, 10);
|
|
165
|
+
const out = {};
|
|
166
|
+
const css = await builtCss(classes, swatchSlugs(look));
|
|
167
|
+
if (css) {
|
|
168
|
+
const name = `built.${named(css)}.css`;
|
|
169
|
+
await writeFile(join(dir, name), css);
|
|
170
|
+
out.css = `/_kit/${name}`;
|
|
171
|
+
}
|
|
172
|
+
for (const b of behaviors) {
|
|
173
|
+
const js = behaviorScript(b);
|
|
174
|
+
if (!js)
|
|
175
|
+
continue;
|
|
176
|
+
const name = `${b}.${named(js)}.js`;
|
|
177
|
+
await writeFile(join(dir, name), js);
|
|
178
|
+
(out.scripts ??= {})[b] = `/_kit/${name}`;
|
|
179
|
+
}
|
|
180
|
+
log(`site-kit: built sections, ${classes.size} classes${behaviors.size ? `, ${[...behaviors].join(", ")}` : ""}`);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
119
183
|
// ---- pictures ----
|
|
120
184
|
async function pictures(site, imagesDir, pub, log) {
|
|
121
185
|
const used = imagesIn(site);
|
|
122
|
-
const shared = new Set(site.
|
|
186
|
+
const shared = new Set(allPages(site).map((p) => shareImageOf(site, p)?.src).filter((s) => !!s));
|
|
123
187
|
const have = existsSync(imagesDir) ? await list(imagesDir) : [];
|
|
124
188
|
const missing = [...used].filter((src) => !have.includes(src));
|
|
125
189
|
if (missing.length) {
|
package/dist/preview-match.d.ts
CHANGED
|
@@ -31,3 +31,24 @@ export declare function fieldForImage(value: unknown, base: Path, url: string, i
|
|
|
31
31
|
* unknown, never guessed, unless the next block has no words to find it by.
|
|
32
32
|
*/
|
|
33
33
|
export declare function matchBlocks(blocks: unknown[], drawn: string[]): (number | null)[];
|
|
34
|
+
/** What the preview reads of an element and each element it's in, to know whether a person sees it. */
|
|
35
|
+
export type Shown = {
|
|
36
|
+
ariaHidden: string | null;
|
|
37
|
+
inert?: boolean;
|
|
38
|
+
visibility: string;
|
|
39
|
+
display: string;
|
|
40
|
+
opacity: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Whether an element is one a person sees, from it and each element it's in (itself first): not in
|
|
44
|
+
* something hidden from them (a carousel's other slides, stacked in the same place and faded out,
|
|
45
|
+
* aria-hidden or inert) and not faded out to nothing. A click is matched only against what's seen,
|
|
46
|
+
* so the slide showing is the one a click on it opens, not one faded out on top of it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isShown(chain: readonly Shown[]): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* A built section's class (as drawn, prefixed) without the state variants given: `kit:md:hover:bg-ink`
|
|
51
|
+
* without hover is `kit:md:bg-ink`, which the preview adds to draw an element as hovered while its
|
|
52
|
+
* hover classes are edited. Null for a class without any of them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function withoutVariant(cls: string, variants: readonly string[]): string | null;
|
package/dist/preview-match.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Which field of the content a click in CalOS's preview landed on, with no markers in the site's
|
|
2
2
|
// components: the words clicked are matched against the content's text, a picture by its address.
|
|
3
3
|
// Pure, so it's tested here and runs in the preview in the browser.
|
|
4
|
+
import { PREFIX } from "./built/theme.js";
|
|
5
|
+
import { splitVariants } from "./tree/classes.js";
|
|
4
6
|
/** The preview page's list of the pictures its build made (by path), as a JSON script with this id (../finish.ts). */
|
|
5
7
|
export const BUILT_IMAGES = "calos-built-images";
|
|
6
8
|
/** Keys that hold no words a visitor reads: never what a click on words means. */
|
|
@@ -110,3 +112,26 @@ export function matchBlocks(blocks, drawn) {
|
|
|
110
112
|
}
|
|
111
113
|
return out;
|
|
112
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether an element is one a person sees, from it and each element it's in (itself first): not in
|
|
117
|
+
* something hidden from them (a carousel's other slides, stacked in the same place and faded out,
|
|
118
|
+
* aria-hidden or inert) and not faded out to nothing. A click is matched only against what's seen,
|
|
119
|
+
* so the slide showing is the one a click on it opens, not one faded out on top of it.
|
|
120
|
+
*/
|
|
121
|
+
export function isShown(chain) {
|
|
122
|
+
return chain.every((c) => c.ariaHidden !== "true" && !c.inert && c.visibility !== "hidden" && c.display !== "none" && !(Number(c.opacity) < 0.05));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A built section's class (as drawn, prefixed) without the state variants given: `kit:md:hover:bg-ink`
|
|
126
|
+
* without hover is `kit:md:bg-ink`, which the preview adds to draw an element as hovered while its
|
|
127
|
+
* hover classes are edited. Null for a class without any of them.
|
|
128
|
+
*/
|
|
129
|
+
export function withoutVariant(cls, variants) {
|
|
130
|
+
if (!cls.startsWith(`${PREFIX}:`))
|
|
131
|
+
return null;
|
|
132
|
+
const { variants: vs, utility } = splitVariants(cls.slice(PREFIX.length + 1));
|
|
133
|
+
const kept = vs.filter((v) => !variants.includes(v));
|
|
134
|
+
if (kept.length === vs.length)
|
|
135
|
+
return null;
|
|
136
|
+
return [PREFIX, ...kept, utility].join(":");
|
|
137
|
+
}
|