@caltho/site-kit 0.2.10 → 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 +146 -16
- 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/cli/check.js +31 -1
- 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 +450 -33
- package/dist/content/index.js +297 -15
- 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 +260 -9
- package/dist/content/look.js +386 -22
- 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 +27 -6
- 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 +58 -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/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 +6 -2
- package/dist/next/data.js +10 -4
- package/dist/next/frame.d.ts +42 -0
- package/dist/next/frame.js +73 -0
- package/dist/next/index.d.ts +8 -2
- package/dist/next/index.js +9 -3
- package/dist/next/menu-group.d.ts +11 -0
- package/dist/next/menu-group.js +33 -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/parts/LookStyle.js +3 -1
- package/dist/next/preview.js +511 -27
- package/dist/next/sections.d.ts +19 -10
- package/dist/next/sections.js +134 -17
- package/dist/policies.js +22 -4
- package/dist/prepare.d.ts +15 -1
- package/dist/prepare.js +125 -3
- package/dist/preview-match.d.ts +29 -0
- package/dist/preview-match.js +58 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +29 -1
- package/dist/sections/icons.d.ts +49 -0
- package/dist/sections/icons.js +55 -0
- package/dist/sections/index.d.ts +188 -12
- package/dist/sections/index.js +136 -10
- package/dist/sections/presets.d.ts +13 -0
- package/dist/sections/presets.js +300 -0
- package/dist/sections.css +1584 -41
- package/dist/sections.test.js +276 -14
- 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 +22 -3
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Tabs: the node's first child holds the tabs (one child for each panel), the children after it are
|
|
2
|
+
// the panels, in the same order. A tab list, tabs and panels as a screen reader expects them (roles,
|
|
3
|
+
// selected, controls), the arrow keys, Home and End moving between tabs. Without scripting every
|
|
4
|
+
// panel shows, one after another, and the tabs don't (sections.css).
|
|
5
|
+
//
|
|
6
|
+
// Self-contained: sent to visitors as its own script (./index.ts). The kit marks the list
|
|
7
|
+
// (data-kit-tablist) and the panels (data-kit-panel) (../next/built.tsx).
|
|
8
|
+
export function tabs(el, s) {
|
|
9
|
+
const list = el.querySelector(":scope > [data-kit-tablist]");
|
|
10
|
+
const panels = Array.from(el.querySelectorAll(":scope > [data-kit-panel]"));
|
|
11
|
+
if (!list || !panels.length)
|
|
12
|
+
return;
|
|
13
|
+
const tabs = Array.from(list.children).slice(0, panels.length);
|
|
14
|
+
const key = Math.random().toString(36).slice(2, 8);
|
|
15
|
+
list.setAttribute("role", "tablist");
|
|
16
|
+
tabs.forEach((t, i) => {
|
|
17
|
+
t.id ||= `kit-tab-${key}-${i}`;
|
|
18
|
+
panels[i].id ||= `kit-panel-${key}-${i}`;
|
|
19
|
+
t.setAttribute("role", "tab");
|
|
20
|
+
t.setAttribute("aria-controls", panels[i].id);
|
|
21
|
+
panels[i].setAttribute("role", "tabpanel");
|
|
22
|
+
panels[i].setAttribute("aria-labelledby", t.id);
|
|
23
|
+
panels[i].tabIndex = 0;
|
|
24
|
+
t.addEventListener("click", (e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
pick(i, false);
|
|
27
|
+
});
|
|
28
|
+
t.addEventListener("keydown", (e) => {
|
|
29
|
+
const to = e.key === "ArrowRight" || e.key === "ArrowDown" ? i + 1 : e.key === "ArrowLeft" || e.key === "ArrowUp" ? i - 1 : e.key === "Home" ? 0 : e.key === "End" ? tabs.length - 1 : e.key === "Enter" || e.key === " " ? i : null;
|
|
30
|
+
if (to === null)
|
|
31
|
+
return;
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
pick((to + tabs.length) % tabs.length, true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
function pick(i, focus) {
|
|
37
|
+
tabs.forEach((t, j) => {
|
|
38
|
+
const on = j === i;
|
|
39
|
+
t.setAttribute("aria-selected", String(on));
|
|
40
|
+
t.tabIndex = on ? 0 : -1;
|
|
41
|
+
t.toggleAttribute("data-active", on);
|
|
42
|
+
panels[j].hidden = !on;
|
|
43
|
+
});
|
|
44
|
+
if (focus)
|
|
45
|
+
tabs[i].focus();
|
|
46
|
+
}
|
|
47
|
+
// CalOS's preview asks for the panel holding what's selected to be shown.
|
|
48
|
+
el.addEventListener("kit:show", (e) => {
|
|
49
|
+
const i = panels.findIndex((p) => p.contains(e.target));
|
|
50
|
+
if (i >= 0)
|
|
51
|
+
pick(i, false);
|
|
52
|
+
});
|
|
53
|
+
pick(Math.min(s.first ?? 0, tabs.length - 1), false);
|
|
54
|
+
el.setAttribute("data-kit-ready", "");
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Built sections' CSS at build: Tailwind v4's own compiler, given exactly the classes the prepared
|
|
2
|
+
// content's trees use (./tree.ts), so the CSS holds those and nothing else; the theme from the Look
|
|
3
|
+
// and no reset (./theme.ts). Node only: prepare runs it.
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { compile } from "tailwindcss";
|
|
8
|
+
import { tailwindInput } from "./theme.js";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
/** Tailwind's own stylesheets (`tailwindcss/theme.css`, `tailwindcss/utilities.css`), from its package. */
|
|
11
|
+
async function loadStylesheet(id, base) {
|
|
12
|
+
const name = id.startsWith("tailwindcss") ? id : `tailwindcss/${id.replace(/^\.\//, "")}`;
|
|
13
|
+
if (!/^tailwindcss\/(theme|utilities)(\.css)?$/.test(name))
|
|
14
|
+
throw new Error(`Built sections' CSS imports only Tailwind's theme and utilities, not ${id}`);
|
|
15
|
+
const path = require.resolve(name.endsWith(".css") ? name : `${name}.css`);
|
|
16
|
+
return { path, base: dirname(path) || base, content: await readFile(path, "utf8") };
|
|
17
|
+
}
|
|
18
|
+
/** The CSS for these classes (prefixed, as drawn), with the site's own colours (their slugs) as Tailwind colours. Empty for none. */
|
|
19
|
+
export async function builtCss(classes, swatches = []) {
|
|
20
|
+
const list = [...classes];
|
|
21
|
+
if (!list.length)
|
|
22
|
+
return "";
|
|
23
|
+
const compiler = await compile(tailwindInput(swatches), { base: process.cwd(), loadStylesheet });
|
|
24
|
+
// Tailwind's banner comment names its version; the file's name carries its hash, so it's dropped.
|
|
25
|
+
return compiler.build(list).replace(/^\/\*![^*]*\*\/\n?/, "");
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** What every built section's class carries on the page (the tree keeps it without). */
|
|
2
|
+
export declare const PREFIX = "kit";
|
|
3
|
+
/** A class of a tree as it's drawn: `md:p-4` → `kit:md:p-4`. */
|
|
4
|
+
export declare const prefixed: (token: string) => string;
|
|
5
|
+
/** The Look's colours by role, as Tailwind colours (`bg-brand`, `text-on-brand`, `border-line`), with what they fall back to on a site without a Look. */
|
|
6
|
+
export declare const THEME_COLOURS: Record<string, string>;
|
|
7
|
+
/** The Look's fonts: `font-display` and `font-heading` its headings', `font-body` and `font-text` its text's. */
|
|
8
|
+
export declare const THEME_FONTS: Record<string, string>;
|
|
9
|
+
/** The Look's type scale as text sizes (`text-h1`, `text-body`), each with its line height, tracking and weight. */
|
|
10
|
+
export declare const THEME_TYPE: readonly ["display", "h1", "h2", "h3", "body", "small"];
|
|
11
|
+
/** Its corners, width and spacing: `rounded-card`, `rounded-button`, `max-w-content`, `px-gutter`, `py-section`. */
|
|
12
|
+
export declare const THEME_OTHER: Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* The site's own colours' slugs, from the Look's values (lookVars: `--look-swatch-dark-sand`), each
|
|
15
|
+
* one a Tailwind colour by its slug (`bg-dark-sand`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function swatchSlugs(look: Record<string, string> | undefined): string[];
|
|
18
|
+
/** The Tailwind input for built sections: the theme and utilities only, prefixed, and the site's Look as tokens. */
|
|
19
|
+
export declare function tailwindInput(swatches?: string[]): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// How a built section's Tailwind classes reach a page, on any site, whatever its own CSS
|
|
2
|
+
// (cal-os docs/design/site-builder.md, "Tailwind"). Pure: the build (./css.ts) and the preview
|
|
3
|
+
// (../next/preview.tsx, with Tailwind's browser build) compile the same input, so what's typed in
|
|
4
|
+
// CalOS's preview is what's published.
|
|
5
|
+
//
|
|
6
|
+
// - Prefixed: a tree's `p-4` is drawn as `kit:p-4`, so only elements the kit drew from a tree ever
|
|
7
|
+
// carry one of these classes. A site's own markup with a class called `flex` or `hidden` (its own
|
|
8
|
+
// CSS's) is never restyled by them, and the kit's never by its.
|
|
9
|
+
// - No preflight (Tailwind's reset): the rest of the site looks as it did.
|
|
10
|
+
// - Not in a cascade layer: a site's own plain CSS (unlayered) would otherwise beat every utility,
|
|
11
|
+
// whatever its specificity.
|
|
12
|
+
// - The site's Look as the theme: `bg-brand`, `text-ink`, `font-display`, `rounded-card`, read from
|
|
13
|
+
// the Look's CSS variables (--look-*, content/look.ts), so a change of Look in CalOS reaches built
|
|
14
|
+
// sections with nothing compiled again.
|
|
15
|
+
import { KIT_TYPE, TYPE_ROLES } from "../content/look.js";
|
|
16
|
+
/** What every built section's class carries on the page (the tree keeps it without). */
|
|
17
|
+
export const PREFIX = "kit";
|
|
18
|
+
/** A class of a tree as it's drawn: `md:p-4` → `kit:md:p-4`. */
|
|
19
|
+
export const prefixed = (token) => `${PREFIX}:${token}`;
|
|
20
|
+
/** The Look's colours by role, as Tailwind colours (`bg-brand`, `text-on-brand`, `border-line`), with what they fall back to on a site without a Look. */
|
|
21
|
+
export const THEME_COLOURS = {
|
|
22
|
+
brand: "var(--look-brand, #111)",
|
|
23
|
+
"on-brand": "var(--look-on-brand, #fff)",
|
|
24
|
+
accent: "var(--look-accent, currentColor)",
|
|
25
|
+
"on-accent": "var(--look-on-accent, #111)",
|
|
26
|
+
page: "var(--look-page, #fff)",
|
|
27
|
+
ink: "var(--look-ink, #111)",
|
|
28
|
+
muted: "var(--look-muted, #666)",
|
|
29
|
+
line: "var(--look-line, #ddd)",
|
|
30
|
+
soft: "var(--look-soft, #f3f3f3)",
|
|
31
|
+
light: "var(--look-light, #fff)",
|
|
32
|
+
dark: "var(--look-dark, #111)",
|
|
33
|
+
};
|
|
34
|
+
/** The Look's fonts: `font-display` and `font-heading` its headings', `font-body` and `font-text` its text's. */
|
|
35
|
+
export const THEME_FONTS = {
|
|
36
|
+
display: "var(--look-heading-font, inherit)",
|
|
37
|
+
heading: "var(--look-heading-font, inherit)",
|
|
38
|
+
body: "var(--look-body-font, inherit)",
|
|
39
|
+
text: "var(--look-body-font, inherit)",
|
|
40
|
+
};
|
|
41
|
+
/** The Look's type scale as text sizes (`text-h1`, `text-body`), each with its line height, tracking and weight. */
|
|
42
|
+
export const THEME_TYPE = TYPE_ROLES;
|
|
43
|
+
/** Its corners, width and spacing: `rounded-card`, `rounded-button`, `max-w-content`, `px-gutter`, `py-section`. */
|
|
44
|
+
export const THEME_OTHER = {
|
|
45
|
+
"--radius-card": "var(--look-radius, 8px)",
|
|
46
|
+
"--radius-button": "var(--look-button-radius, 6px)",
|
|
47
|
+
"--container-content": "var(--look-width, 1200px)",
|
|
48
|
+
"--spacing-gutter": "var(--look-gutter, 20px)",
|
|
49
|
+
"--spacing-section": "var(--look-section-pad, clamp(56px, 8vw, 96px))",
|
|
50
|
+
};
|
|
51
|
+
/** Names a site's own colour can't take as a Tailwind colour: they're Tailwind's own words, or the roles. */
|
|
52
|
+
const RESERVED = new Set(["inherit", "current", "transparent", ...Object.keys(THEME_COLOURS)]);
|
|
53
|
+
/**
|
|
54
|
+
* The site's own colours' slugs, from the Look's values (lookVars: `--look-swatch-dark-sand`), each
|
|
55
|
+
* one a Tailwind colour by its slug (`bg-dark-sand`).
|
|
56
|
+
*/
|
|
57
|
+
export function swatchSlugs(look) {
|
|
58
|
+
return Object.keys(look ?? {})
|
|
59
|
+
.map((k) => /^--look-swatch-([a-z][a-z0-9-]*)$/.exec(k)?.[1])
|
|
60
|
+
.filter((s) => !!s && !RESERVED.has(s));
|
|
61
|
+
}
|
|
62
|
+
/** The Tailwind input for built sections: the theme and utilities only, prefixed, and the site's Look as tokens. */
|
|
63
|
+
export function tailwindInput(swatches = []) {
|
|
64
|
+
const colours = [...Object.entries(THEME_COLOURS).map(([k, v]) => ` --color-${k}: ${v};`), ...swatches.map((s) => ` --color-${s}: var(--look-swatch-${s});`)];
|
|
65
|
+
const fonts = Object.entries(THEME_FONTS).map(([k, v]) => ` --font-${k}: ${v};`);
|
|
66
|
+
const type = THEME_TYPE.flatMap((r) => [
|
|
67
|
+
` --text-${r}: var(--look-type-${r}-size, ${KIT_TYPE[r].size}px);`,
|
|
68
|
+
` --text-${r}--line-height: var(--look-type-${r}-leading, ${KIT_TYPE[r].leading});`,
|
|
69
|
+
` --text-${r}--letter-spacing: var(--look-type-${r}-tracking, ${KIT_TYPE[r].tracking}em);`,
|
|
70
|
+
` --text-${r}--font-weight: var(--look-type-${r}-weight, ${KIT_TYPE[r].weight});`,
|
|
71
|
+
]);
|
|
72
|
+
const other = Object.entries(THEME_OTHER).map(([k, v]) => ` ${k}: ${v};`);
|
|
73
|
+
return [
|
|
74
|
+
`@import "tailwindcss/theme.css" prefix(${PREFIX});`,
|
|
75
|
+
`@import "tailwindcss/utilities.css";`,
|
|
76
|
+
// A link to the page it's on (the kit marks it: next/built.tsx): aria-current:underline. Tailwind has no such variant of its own.
|
|
77
|
+
`@custom-variant aria-current (&[aria-current="page"]);`,
|
|
78
|
+
// inline: a utility reads the Look's variable itself, so nothing new is set on :root.
|
|
79
|
+
`@theme inline {`,
|
|
80
|
+
...colours,
|
|
81
|
+
...fonts,
|
|
82
|
+
...type,
|
|
83
|
+
...other,
|
|
84
|
+
`}`,
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type BehaviorName, type Node } from "../tree/index.ts";
|
|
2
|
+
/** A node's classes as drawn: only the allowed ones (a bad one dropped, never drawn), and its behavior's, each prefixed. */
|
|
3
|
+
export declare function nodeClasses(n: Node): string[];
|
|
4
|
+
/** A block's or component's tree, wherever the content keeps one: built sections, saved sections, the site's components. */
|
|
5
|
+
export declare function treesIn(site: {
|
|
6
|
+
pages?: {
|
|
7
|
+
blocks?: unknown[];
|
|
8
|
+
}[];
|
|
9
|
+
saved?: unknown;
|
|
10
|
+
components?: unknown;
|
|
11
|
+
}): Generator<Node>;
|
|
12
|
+
/** Every class the site's built sections are drawn with (prefixed): all the build compiles. Hidden nodes too, so showing one again needs no build. */
|
|
13
|
+
export declare function classesIn(site: Parameters<typeof treesIn>[0]): Set<string>;
|
|
14
|
+
/** The behaviors the site's built sections use, whose scripts the build makes. */
|
|
15
|
+
export declare function behaviorsIn(site: Parameters<typeof treesIn>[0]): Set<BehaviorName>;
|
|
16
|
+
/** Whether a page has any built section on it (a `built` or `use` block). */
|
|
17
|
+
export declare const hasBuilt: (blocks: readonly {
|
|
18
|
+
type?: unknown;
|
|
19
|
+
}[]) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The value at a dotted path: "company.email" in the site's content, "item.name" in the item a
|
|
22
|
+
* `repeat` is drawing. Only the content's own fields are read, never anything an object inherits.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readPath(path: string, site: unknown, item?: unknown, self?: unknown): unknown;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// What drawing a built section needs to know of its tree beyond each node: the classes it's drawn
|
|
2
|
+
// with (its own, checked, and those its behavior adds), what a `bind` or `repeat` reads from the
|
|
3
|
+
// site's content, and every class and behavior a site's built sections use (for the build to
|
|
4
|
+
// compile only those classes and send only those scripts). Pure, for the server, the preview and the
|
|
5
|
+
// build alike.
|
|
6
|
+
import { safeClasses } from "../tree/index.js";
|
|
7
|
+
import { prefixed } from "./theme.js";
|
|
8
|
+
/** A node's classes as drawn: only the allowed ones (a bad one dropped, never drawn), and its behavior's, each prefixed. */
|
|
9
|
+
export function nodeClasses(n) {
|
|
10
|
+
const own = safeClasses(n.class).split(" ").filter(Boolean);
|
|
11
|
+
// Sticky is CSS alone: no script for it.
|
|
12
|
+
const extra = n.behavior?.name === "sticky" ? ["sticky", `top-[${n.behavior.top ?? 0}px]`] : [];
|
|
13
|
+
return [...own, ...extra].map(prefixed);
|
|
14
|
+
}
|
|
15
|
+
/** A block's or component's tree, wherever the content keeps one: built sections, saved sections, the site's components. */
|
|
16
|
+
export function* treesIn(site) {
|
|
17
|
+
const fromBlock = (b) => {
|
|
18
|
+
const o = b;
|
|
19
|
+
return o && o.type === "built" && o.root && typeof o.root === "object" ? o.root : null;
|
|
20
|
+
};
|
|
21
|
+
for (const p of site.pages ?? [])
|
|
22
|
+
for (const b of p.blocks ?? []) {
|
|
23
|
+
const r = fromBlock(b);
|
|
24
|
+
if (r)
|
|
25
|
+
yield r;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(site.saved))
|
|
28
|
+
for (const s of site.saved) {
|
|
29
|
+
const r = fromBlock(s?.block);
|
|
30
|
+
if (r)
|
|
31
|
+
yield r;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(site.components))
|
|
34
|
+
for (const c of site.components)
|
|
35
|
+
if (c?.root && typeof c.root === "object")
|
|
36
|
+
yield c.root;
|
|
37
|
+
// A header or footer built as a tree (the site's own, in place of the kit's).
|
|
38
|
+
for (const k of ["ownHeader", "ownFooter"]) {
|
|
39
|
+
const o = site[k];
|
|
40
|
+
if (o?.root && typeof o.root === "object")
|
|
41
|
+
yield o.root;
|
|
42
|
+
}
|
|
43
|
+
// A list's page design: drawn for each of its items.
|
|
44
|
+
const lists = site.lists;
|
|
45
|
+
if (lists && typeof lists === "object")
|
|
46
|
+
for (const l of Object.values(lists))
|
|
47
|
+
for (const b of l?.page?.blocks ?? []) {
|
|
48
|
+
const r = fromBlock(b);
|
|
49
|
+
if (r)
|
|
50
|
+
yield r;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function* nodes(root) {
|
|
54
|
+
yield root;
|
|
55
|
+
for (const c of root.children ?? [])
|
|
56
|
+
yield* nodes(c);
|
|
57
|
+
}
|
|
58
|
+
/** Every class the site's built sections are drawn with (prefixed): all the build compiles. Hidden nodes too, so showing one again needs no build. */
|
|
59
|
+
export function classesIn(site) {
|
|
60
|
+
const out = new Set();
|
|
61
|
+
for (const root of treesIn(site))
|
|
62
|
+
for (const n of nodes(root))
|
|
63
|
+
for (const c of nodeClasses(n))
|
|
64
|
+
out.add(c);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** The behaviors the site's built sections use, whose scripts the build makes. */
|
|
68
|
+
export function behaviorsIn(site) {
|
|
69
|
+
const out = new Set();
|
|
70
|
+
for (const root of treesIn(site))
|
|
71
|
+
for (const n of nodes(root))
|
|
72
|
+
if (n.behavior && !n.hidden)
|
|
73
|
+
out.add(n.behavior.name);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** Whether a page has any built section on it (a `built` or `use` block). */
|
|
77
|
+
export const hasBuilt = (blocks) => blocks.some((b) => b?.type === "built" || b?.type === "use");
|
|
78
|
+
/**
|
|
79
|
+
* The value at a dotted path: "company.email" in the site's content, "item.name" in the item a
|
|
80
|
+
* `repeat` is drawing. Only the content's own fields are read, never anything an object inherits.
|
|
81
|
+
*/
|
|
82
|
+
export function readPath(path, site, item, self) {
|
|
83
|
+
const parts = path.split(".");
|
|
84
|
+
let at = site;
|
|
85
|
+
// item.: the item a repeat is drawing; this.: the item an item page is drawn for (a list's page).
|
|
86
|
+
if (parts[0] === "item" || parts[0] === "this") {
|
|
87
|
+
at = parts[0] === "item" ? item : self;
|
|
88
|
+
parts.shift();
|
|
89
|
+
}
|
|
90
|
+
for (const k of parts) {
|
|
91
|
+
if (at === null || typeof at !== "object" || !Object.prototype.hasOwnProperty.call(at, k))
|
|
92
|
+
return undefined;
|
|
93
|
+
at = at[k];
|
|
94
|
+
}
|
|
95
|
+
return at;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
9
|
+
import { behaviorScript, BEHAVIOR_RUNS } from "./behaviors/index.js";
|
|
10
|
+
import { builtCss } from "./built/css.js";
|
|
11
|
+
import { swatchSlugs, tailwindInput } from "./built/theme.js";
|
|
12
|
+
import { behaviorsIn, classesIn, nodeClasses, readPath } from "./built/tree.js";
|
|
13
|
+
import { defineSite } from "./content/index.js";
|
|
14
|
+
import { finish } from "./finish.js";
|
|
15
|
+
import { setPrepared } from "./next/data.js";
|
|
16
|
+
import { KitSection } from "./next/sections.js";
|
|
17
|
+
import { prepare } from "./prepare.js";
|
|
18
|
+
import { isShown, withoutVariant } from "./preview-match.js";
|
|
19
|
+
import { isKitSection, sections } from "./sections/index.js";
|
|
20
|
+
// Built sections: a tree of elements styled with Tailwind, drawn by the kit on any site, with the CSS
|
|
21
|
+
// for exactly its classes and the scripts of exactly its behaviors.
|
|
22
|
+
const content = {
|
|
23
|
+
url: "https://acme.example",
|
|
24
|
+
business: { name: "Acme" },
|
|
25
|
+
company: { email: "hello@acme.example", phone: "555 0100" },
|
|
26
|
+
caseStudies: [
|
|
27
|
+
{ name: "Maple & Oak", summary: "Tax season, **calm**.", photo: { src: "a.jpg", alt: "The office" } },
|
|
28
|
+
{ name: "Harbor Dental", summary: "Booked out.", photo: { src: "b.jpg", alt: "The chair" } },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
const images = {
|
|
32
|
+
"a.jpg": { width: 1600, height: 1000, src: "/_img/a.1600.webp", srcset: [{ url: "/_img/a.640.webp", w: 640 }, { url: "/_img/a.1600.webp", w: 1600 }] },
|
|
33
|
+
"b.jpg": { width: 800, height: 600, src: "/_img/b.800.webp", srcset: [] },
|
|
34
|
+
};
|
|
35
|
+
/** Draws a block as the published site does (draft: as CalOS's preview does). */
|
|
36
|
+
function draw(block, o = {}) {
|
|
37
|
+
setPrepared({ site: { ...content, lang: "en", redirects: [], pages: [], ...o.site }, images, csp: {}, ...(o.built ? { built: o.built } : {}) }, o.draft ?? false);
|
|
38
|
+
return renderToStaticMarkup(_jsx(KitSection, { s: block }));
|
|
39
|
+
}
|
|
40
|
+
const tree = (root) => ({ type: "built", id: "hero", root });
|
|
41
|
+
describe("a built section's tree, drawn", () => {
|
|
42
|
+
it("draws each node as its tag with its classes prefixed, a class that isn't allowed dropped", () => {
|
|
43
|
+
const html = draw(tree({ id: "root000001", tag: "section", class: "grid md:grid-cols-2 gap-6 bg-[url(https://evil.example/x.png)]", children: [{ id: "head000001", tag: "h1", class: "font-display text-h1 text-ink", text: "One place to run the **business**." }] }));
|
|
44
|
+
expect(html).toContain('<section class="kit:grid kit:md:grid-cols-2 kit:gap-6" id="hero" data-kit-built="">');
|
|
45
|
+
expect(html).toContain('<h1 class="kit:font-display kit:text-h1 kit:text-ink">One place to run the <strong>business</strong>.</h1>');
|
|
46
|
+
expect(html).not.toContain("evil");
|
|
47
|
+
});
|
|
48
|
+
it("leaves out a tag that isn't allowed, hidden nodes, and HTML in words; marks nodes only in the preview", () => {
|
|
49
|
+
const root = { id: "root000002", tag: "div", children: [{ id: "bad0000001", tag: "script", text: "alert(1)" }, { id: "hid0000001", tag: "p", text: "Draft", hidden: true }, { id: "txt0000001", tag: "p", text: "<img src=x onerror=alert(1)> hi" }] };
|
|
50
|
+
const html = draw(tree(root));
|
|
51
|
+
expect(html).not.toContain("<script");
|
|
52
|
+
expect(html).not.toContain("Draft");
|
|
53
|
+
expect(html).not.toContain("<img");
|
|
54
|
+
expect(html).toContain("<img src=x onerror=alert(1)> hi");
|
|
55
|
+
expect(html).not.toContain("data-node");
|
|
56
|
+
expect(draw(tree(root), { draft: true })).toContain('data-node="txt0000001"');
|
|
57
|
+
});
|
|
58
|
+
it("checks links again, opens outside ones in a new tab safely, and labels what has no words", () => {
|
|
59
|
+
const html = draw(tree({
|
|
60
|
+
id: "root000003",
|
|
61
|
+
tag: "nav",
|
|
62
|
+
children: [
|
|
63
|
+
{ id: "lnk0000001", tag: "a", text: "Work", href: "/work/" },
|
|
64
|
+
{ id: "lnk0000002", tag: "a", text: "Them", href: "https://them.example", external: true },
|
|
65
|
+
{ id: "lnk0000003", tag: "a", text: "Bad", href: "javascript:alert(1)" },
|
|
66
|
+
{ id: "btn0000001", tag: "button", label: "Next", children: [{ id: "ico0000001", tag: "icon", icon: "arrow-right" }] },
|
|
67
|
+
{ id: "btn0000002", tag: "button", text: "Call", href: "tel:+15550100" },
|
|
68
|
+
],
|
|
69
|
+
}));
|
|
70
|
+
expect(html).toContain('<a href="/work/">Work</a>');
|
|
71
|
+
expect(html).toContain('<a href="https://them.example" target="_blank" rel="noopener noreferrer">Them</a>');
|
|
72
|
+
expect(html).toContain("<a>Bad</a>");
|
|
73
|
+
expect(html).not.toContain("javascript:");
|
|
74
|
+
expect(html).toContain('<button aria-label="Next" type="button"><svg class="kit-built-icon" viewBox="0 0 24 24"');
|
|
75
|
+
expect(html).toContain('<a href="tel:+15550100">Call</a>');
|
|
76
|
+
});
|
|
77
|
+
it("draws a picture through the kit's images and a video as the kit's own player", () => {
|
|
78
|
+
const html = draw(tree({ id: "root000004", tag: "figure", children: [{ id: "pic0000001", tag: "img", class: "rounded-card", image: { src: "a.jpg", alt: "The office" } }, { id: "vid0000001", tag: "video", label: "Our story", video: { provider: "youtube", id: "dQw4w9WgXcQ" } }, { id: "vid0000002", tag: "video", video: { provider: "vimeo", id: "123456789" } }] }));
|
|
79
|
+
expect(html).toContain('srcSet="/_img/a.640.webp 640w, /_img/a.1600.webp 1600w"');
|
|
80
|
+
expect(html).toContain('alt="The office"');
|
|
81
|
+
expect(html).toContain('class="kit:rounded-card"');
|
|
82
|
+
expect(html).toContain('src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ" title="Our story"');
|
|
83
|
+
expect(html).toContain('src="https://player.vimeo.com/video/123456789?dnt=1"');
|
|
84
|
+
});
|
|
85
|
+
it("shows shared values (bind) and one copy for each item of a list (repeat)", () => {
|
|
86
|
+
const html = draw(tree({
|
|
87
|
+
id: "root000005",
|
|
88
|
+
tag: "section",
|
|
89
|
+
children: [
|
|
90
|
+
{ id: "mail000001", tag: "a", bind: { path: "company.email" }, href: "mailto:hello@acme.example" },
|
|
91
|
+
{ id: "card000001", tag: "article", repeat: { path: "caseStudies" }, children: [{ id: "name000001", tag: "h3", bind: { path: "item.name" } }, { id: "sum0000001", tag: "p", bind: { path: "item.summary" } }, { id: "pic0000002", tag: "img", bind: { path: "item.photo" } }] },
|
|
92
|
+
{ id: "card000002", tag: "p", repeat: { path: "caseStudies", limit: 1 }, bind: { path: "item.name" } },
|
|
93
|
+
{ id: "none000001", tag: "p", bind: { path: "company.__proto__" }, text: "Kept" },
|
|
94
|
+
],
|
|
95
|
+
}));
|
|
96
|
+
expect(html).toContain('<a href="mailto:hello@acme.example">hello@acme.example</a>');
|
|
97
|
+
expect(html).toContain("<h3>Maple & Oak</h3>");
|
|
98
|
+
expect(html).toContain("<h3>Harbor Dental</h3>");
|
|
99
|
+
expect(html).toContain("Tax season, <strong>calm</strong>.");
|
|
100
|
+
expect(html).toContain('alt="The chair"');
|
|
101
|
+
expect(html.match(/<p>Maple & Oak<\/p>/g)).toHaveLength(1);
|
|
102
|
+
expect(html).not.toContain("<p>Harbor Dental</p>");
|
|
103
|
+
expect(html).toContain("<p>Kept</p>");
|
|
104
|
+
expect(readPath("item.name", content, { name: "x" })).toBe("x");
|
|
105
|
+
expect(readPath("company.toString", content)).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
it("draws a component used by name with its slots filled, and nothing for one the site hasn't", () => {
|
|
108
|
+
const components = [{ key: "case-card", name: "Case card", root: { id: "comp000001", tag: "article", class: "p-6 rounded-card", children: [{ id: "tit0000001", tag: "h3", slot: "title", text: "Title" }, { id: "go00000001", tag: "a", slot: "link", text: "View project", href: "/" }] } }];
|
|
109
|
+
const html = draw({ type: "use", id: "case", component: "case-card", slots: { title: { text: "Maple & Oak" }, link: { href: "/work/maple/" } } }, { site: { components } });
|
|
110
|
+
expect(html).toContain('<article class="kit:p-6 kit:rounded-card" id="case" data-kit-built=""><h3>Maple & Oak</h3><a href="/work/maple/">View project</a></article>');
|
|
111
|
+
expect(draw({ type: "use", component: "nope", slots: {} }, { site: { components } })).toBe("");
|
|
112
|
+
expect(draw({ type: "use", component: "nope", slots: {} }, { site: { components }, draft: true })).toContain("kit-built-missing");
|
|
113
|
+
});
|
|
114
|
+
it("links the built sections' CSS and the scripts of the behaviors it uses, on the published page only", () => {
|
|
115
|
+
const built = { css: "/_kit/built.abc.css", scripts: { carousel: "/_kit/carousel.def.js", tabs: "/_kit/tabs.ghi.js" } };
|
|
116
|
+
const block = tree({ id: "root000006", tag: "div", behavior: { name: "carousel", transition: "fade", pauseOnHover: true, dots: true, arrows: false }, children: [{ id: "s000000001", tag: "p", text: "One" }, { id: "s000000002", tag: "p", text: "Two" }] });
|
|
117
|
+
const html = draw(block, { built });
|
|
118
|
+
expect(html).toContain('href="/_kit/built.abc.css"');
|
|
119
|
+
expect(html).toContain('src="/_kit/carousel.def.js"');
|
|
120
|
+
expect(html).not.toContain("tabs.ghi.js");
|
|
121
|
+
const preview = draw(block, { built, draft: true });
|
|
122
|
+
expect(preview).not.toContain("/_kit/");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe("behaviors, drawn", () => {
|
|
126
|
+
it("a carousel: the first slide shown, the rest hidden until its script starts, dots as labelled buttons, and a stop button where it moves by itself", () => {
|
|
127
|
+
const html = draw(tree({ id: "root000007", tag: "div", class: "relative", label: "Case studies", behavior: { name: "carousel", seconds: 6, transition: "fade", pauseOnHover: true, dots: true, arrows: true }, children: [{ id: "s000000003", tag: "article", text: "One" }, { id: "s000000004", tag: "article", text: "Two" }] }));
|
|
128
|
+
expect(html).toContain('data-kit-behavior="carousel" data-kit-settings="{"seconds":6,"transition":"fade","pauseOnHover":true,"dots":true,"arrows":true}" role="region" aria-roledescription="carousel"');
|
|
129
|
+
expect(html).toContain('<div class="kit-carousel-track" data-kit-track="" data-kit-transition="fade"><article data-kit-slide="" data-active="" role="group" aria-roledescription="slide" aria-label="1 of 2">One</article><article data-kit-slide="" role="group" aria-roledescription="slide" aria-label="2 of 2">Two</article></div>');
|
|
130
|
+
expect(html).toContain('data-kit-to="0" aria-label="Show slide 1 of 2" aria-current="true"');
|
|
131
|
+
expect(html).toContain('data-kit-pause="" aria-pressed="false" aria-label="Pause the slides"');
|
|
132
|
+
expect(html).toContain('data-kit-prev="" aria-label="Previous slide"');
|
|
133
|
+
});
|
|
134
|
+
it("a carousel's own designed controls are used in place of the kit's", () => {
|
|
135
|
+
const html = draw(tree({ id: "root000008", tag: "div", behavior: { name: "carousel", transition: "slide", pauseOnHover: true, dots: true, arrows: false }, children: [{ id: "s000000005", tag: "p", text: "One" }, { id: "s000000006", tag: "p", text: "Two" }, { id: "nxt0000001", tag: "button", slot: "next", children: [{ id: "ico0000002", tag: "icon", icon: "arrow-right" }] }, { id: "dts0000001", tag: "div", slot: "dots", class: "flex gap-2" }] }));
|
|
136
|
+
expect(html).toContain('<button data-kit-next="" aria-label="Next slide" type="button">');
|
|
137
|
+
expect(html).toContain('<div class="kit:flex kit:gap-2"><button type="button" class="kit-carousel-dot" data-kit-to="0"');
|
|
138
|
+
expect(html).not.toContain("kit-carousel-controls");
|
|
139
|
+
});
|
|
140
|
+
it("tabs, accordion, marquee, reveal and sticky mark what their scripts and CSS work on", () => {
|
|
141
|
+
const tabs = draw(tree({ id: "root000009", tag: "div", behavior: { name: "tabs", first: 0 }, children: [{ id: "lst0000001", tag: "div", children: [{ id: "tab0000001", tag: "button", text: "Returns" }, { id: "tab0000002", tag: "button", text: "Payroll" }] }, { id: "pan0000001", tag: "div", text: "We file them." }, { id: "pan0000002", tag: "div", text: "We run it." }] }));
|
|
142
|
+
expect(tabs).toContain('<div data-kit-tablist=""><button type="button">Returns</button>');
|
|
143
|
+
expect(tabs).toContain('<div data-kit-panel="">We file them.</div>');
|
|
144
|
+
const acc = draw(tree({ id: "root000010", tag: "div", behavior: { name: "accordion", single: true }, children: [{ id: "itm0000001", tag: "div", children: [{ id: "q000000001", tag: "h3", text: "How long?" }, { id: "a000000001", tag: "p", text: "Three weeks." }] }] }));
|
|
145
|
+
expect(acc).toContain('<div data-kit-item=""><h3 data-kit-head="">How long?</h3><p data-kit-body="">Three weeks.</p></div>');
|
|
146
|
+
const mq = draw(tree({ id: "root000011", tag: "ul", class: "flex gap-8", behavior: { name: "marquee", seconds: 20, reverse: false }, children: [{ id: "li00000001", tag: "li", text: "Acme" }] }));
|
|
147
|
+
expect(mq).toContain('<div class="kit-marquee-track" data-kit-track=""><div class="kit-marquee-group"><li>Acme</li></div><div class="kit-marquee-group" data-kit-copy="" aria-hidden="true" inert=""><li>Acme</li></div></div>');
|
|
148
|
+
const rv = draw(tree({ id: "root000012", tag: "div", behavior: { name: "reveal", effect: "left", stagger: 80 } }));
|
|
149
|
+
expect(rv).toContain('data-kit-behavior="reveal" data-kit-settings="{"effect":"left","stagger":80}" data-kit-effect="left"');
|
|
150
|
+
expect(nodeClasses({ id: "stk0000001", tag: "aside", class: "p-4", behavior: { name: "sticky", top: 24 } })).toEqual(["kit:p-4", "kit:sticky", "kit:top-[24px]"]);
|
|
151
|
+
});
|
|
152
|
+
it("each behavior with a script is one that can be sent alone, its code and its starter", () => {
|
|
153
|
+
for (const name of Object.keys(BEHAVIOR_RUNS)) {
|
|
154
|
+
const js = behaviorScript(name);
|
|
155
|
+
expect(() => new Function(js)).not.toThrow();
|
|
156
|
+
expect(js).toContain(`"${name}"`);
|
|
157
|
+
}
|
|
158
|
+
expect(behaviorScript("sticky")).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe("the kit's sections, with built ones", () => {
|
|
162
|
+
const site = defineSite({ ...sections });
|
|
163
|
+
const page = (blocks, extra = {}) => ({ url: "https://acme.example", business: { name: "Acme" }, ...extra, pages: [{ path: "/", title: "Acme", description: "Acme does things.", blocks }] });
|
|
164
|
+
it("takes built and use blocks, components, and refuses a tree with an id twice or a component it hasn't", () => {
|
|
165
|
+
expect(isKitSection("built") && isKitSection("use")).toBe(true);
|
|
166
|
+
const root = { id: "root000013", tag: "section", children: [{ id: "head000002", tag: "h2", text: "Hi" }] };
|
|
167
|
+
expect(site.parse(page([{ type: "built", root }])).problems).toBeUndefined();
|
|
168
|
+
const twice = { ...root, children: [root.children[0], root.children[0]] };
|
|
169
|
+
expect(site.parse(page([{ type: "built", root: twice }])).problems?.join(" ")).toMatch(/head000002 is used twice/);
|
|
170
|
+
expect(site.parse(page([{ type: "use", component: "case-card" }])).problems?.join(" ")).toMatch(/no component called case-card/);
|
|
171
|
+
const withComponent = page([{ type: "use", component: "case-card", slots: { title: { text: "Hi" } } }], { components: [{ key: "case-card", name: "Case card", root }] });
|
|
172
|
+
expect(site.parse(withComponent).problems).toBeUndefined();
|
|
173
|
+
expect(site.parse(page([{ type: "built", root: { ...root, class: "bg-[url(x)]" } }])).problems?.join(" ")).toMatch(/Not allowed: bg-\[url\(x\)\]/);
|
|
174
|
+
});
|
|
175
|
+
it("says its schema, trees and all, for CalOS", () => {
|
|
176
|
+
const json = JSON.stringify(site.jsonSchema());
|
|
177
|
+
expect(json).toContain("Built section");
|
|
178
|
+
expect(json).toContain('"components"');
|
|
179
|
+
});
|
|
180
|
+
it("collects every class and behavior a site's trees use: built sections, saved ones and components", () => {
|
|
181
|
+
const s = {
|
|
182
|
+
pages: [{ blocks: [{ type: "built", root: { id: "a000000001", tag: "div", class: "p-4 bg-[url(x)]", behavior: { name: "reveal" }, children: [{ id: "b000000001", tag: "p", class: "md:text-h2", hidden: true }] } }, { type: "hero" }] }],
|
|
183
|
+
saved: [{ name: "x", block: { type: "built", root: { id: "c000000001", tag: "div", class: "flex" } } }],
|
|
184
|
+
components: [{ key: "k", name: "K", root: { id: "d000000001", tag: "div", class: "grid", behavior: { name: "carousel" } } }],
|
|
185
|
+
};
|
|
186
|
+
expect([...classesIn(s)].sort()).toEqual(["kit:flex", "kit:grid", "kit:md:text-h2", "kit:p-4"]);
|
|
187
|
+
expect([...behaviorsIn(s)].sort()).toEqual(["carousel", "reveal"]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe("built sections' CSS", () => {
|
|
191
|
+
it("is Tailwind's, for those classes only, with no reset, unlayered, and the Look as its theme", async () => {
|
|
192
|
+
const css = await builtCss(["kit:p-4", "kit:md:hover:bg-brand/90", "kit:text-h1", "kit:bg-dark-sand", "kit:font-display", "kit:rounded-card", "kit:max-w-content"], ["dark-sand"]);
|
|
193
|
+
expect(css).toContain(".kit\\:p-4 {");
|
|
194
|
+
expect(css).toContain("var(--look-brand, #111)");
|
|
195
|
+
expect(css).toContain("font-size: var(--look-type-h1-size, 56px)");
|
|
196
|
+
expect(css).toContain("background-color: var(--look-swatch-dark-sand)");
|
|
197
|
+
expect(css).toContain("font-family: var(--look-heading-font, inherit)");
|
|
198
|
+
expect(css).toContain("border-radius: var(--look-radius, 8px)");
|
|
199
|
+
expect(css).toContain("max-width: var(--look-width, 1200px)");
|
|
200
|
+
// Nothing for a class of the site's own, no reset of its elements, no layer the site's CSS would beat.
|
|
201
|
+
expect(css).not.toMatch(/\n\.flex|box-sizing|@layer (theme|base|utilities)|^\/\*!/);
|
|
202
|
+
expect(await builtCss([])).toBe("");
|
|
203
|
+
});
|
|
204
|
+
it("names the site's own colours by their slugs, never over Tailwind's own words or the roles", () => {
|
|
205
|
+
expect(swatchSlugs({ "--look-swatch-dark-sand": "#c2a", "--look-swatch-1": "#c2a", "--look-swatch-current": "#000", "--look-swatch-brand": "#111", "--look-brand": "#111" })).toEqual(["dark-sand"]);
|
|
206
|
+
expect(tailwindInput(["dark-sand"])).toContain("--color-dark-sand: var(--look-swatch-dark-sand);");
|
|
207
|
+
expect(tailwindInput()).toStartWith('@import "tailwindcss/theme.css" prefix(kit);\n@import "tailwindcss/utilities.css";');
|
|
208
|
+
});
|
|
209
|
+
it("is made by prepare into the site's public files, with each behavior's script, and cleared when there are none", async () => {
|
|
210
|
+
const kit = pathToFileURL(join(import.meta.dir, "content", "index.ts")).href;
|
|
211
|
+
const kitSections = pathToFileURL(join(import.meta.dir, "sections", "index.ts")).href;
|
|
212
|
+
const root = await mkdtemp(join(tmpdir(), "site-"));
|
|
213
|
+
await mkdir(join(root, "content"), { recursive: true });
|
|
214
|
+
await writeFile(join(root, "content", "schema.ts"), `import { defineSite } from ${JSON.stringify(kit)};\nimport { sections } from ${JSON.stringify(kitSections)};\nexport default defineSite({ ...sections }, { look: { brand: "#2b5f8a", palette: [{ name: "Dark sand", color: "#8a6d3b" }] } });\n`);
|
|
215
|
+
const site = (blocks) => writeFile(join(root, "content", "site.json"), JSON.stringify({ url: "https://acme.example", business: { name: "Acme" }, pages: [{ path: "/", title: "Acme", description: "Acme does things.", blocks }] }));
|
|
216
|
+
await site([{ type: "built", root: { id: "root000014", tag: "div", class: "p-4 bg-dark-sand", behavior: { name: "carousel" }, children: [{ id: "s000000007", tag: "p", text: "One" }] } }]);
|
|
217
|
+
const p = await prepare(root, {}, () => { });
|
|
218
|
+
expect(p.built?.css).toMatch(/^\/_kit\/built\.[0-9a-f]{10}\.css$/);
|
|
219
|
+
expect(p.built?.scripts?.carousel).toMatch(/^\/_kit\/carousel\.[0-9a-f]{10}\.js$/);
|
|
220
|
+
const css = await readFile(join(root, "public", p.built.css), "utf8");
|
|
221
|
+
expect(css).toContain("background-color: var(--look-swatch-dark-sand)");
|
|
222
|
+
expect(await readFile(join(root, "public", p.built.scripts.carousel), "utf8")).toContain('"carousel"');
|
|
223
|
+
await site([{ type: "own", columns: [{ parts: [] }] }]);
|
|
224
|
+
const again = await prepare(root, {}, () => { });
|
|
225
|
+
expect(again.built).toBeUndefined();
|
|
226
|
+
expect(existsSync(join(root, "public", "_kit"))).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe("the preview", () => {
|
|
230
|
+
it("lets Tailwind's browser build write its styles on the preview page only", async () => {
|
|
231
|
+
const root = await mkdtemp(join(tmpdir(), "site-"));
|
|
232
|
+
await mkdir(join(root, ".site"), { recursive: true });
|
|
233
|
+
await mkdir(join(root, "out"), { recursive: true });
|
|
234
|
+
await writeFile(join(root, ".site", "prepared.json"), JSON.stringify({ site: { url: "https://acme.example", business: { name: "Acme" }, redirects: [], pages: [{ path: "/", blocks: [] }] }, images: {}, csp: {} }));
|
|
235
|
+
await writeFile(join(root, "out", "index.html"), "<html><head><style>a{}</style></head><body></body></html>");
|
|
236
|
+
await writeFile(join(root, "out", "calos-preview.html"), "<html><head><style>a{}</style></head><body></body></html>");
|
|
237
|
+
await finish(root, join(root, "out"), {}, () => { });
|
|
238
|
+
expect(await readFile(join(root, "out", "calos-preview.html"), "utf8")).toContain("style-src 'self' 'unsafe-inline';");
|
|
239
|
+
const page = await readFile(join(root, "out", "index.html"), "utf8");
|
|
240
|
+
expect(page).toMatch(/style-src 'self' 'sha256-[^']+';/);
|
|
241
|
+
expect(page).not.toContain("unsafe-inline");
|
|
242
|
+
});
|
|
243
|
+
it("matches a click only against what's seen: not a slide faded out over the one showing", () => {
|
|
244
|
+
const shown = { ariaHidden: null, visibility: "visible", display: "block", opacity: "1" };
|
|
245
|
+
expect(isShown([shown, shown])).toBe(true);
|
|
246
|
+
expect(isShown([shown, { ...shown, opacity: "0" }])).toBe(false);
|
|
247
|
+
expect(isShown([{ ...shown, ariaHidden: "true" }])).toBe(false);
|
|
248
|
+
expect(isShown([shown, { ...shown, inert: true }])).toBe(false);
|
|
249
|
+
expect(isShown([{ ...shown, visibility: "hidden" }])).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
it("draws a node as hovered by its hover classes without the state", () => {
|
|
252
|
+
expect(withoutVariant("kit:md:hover:bg-ink", ["hover"])).toBe("kit:md:bg-ink");
|
|
253
|
+
expect(withoutVariant("kit:group-hover:underline", ["group-hover"])).toBe("kit:underline");
|
|
254
|
+
expect(withoutVariant("kit:bg-ink", ["hover"])).toBeNull();
|
|
255
|
+
expect(withoutVariant("hover:bg-ink", ["hover"])).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
});
|