@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.
Files changed (140) hide show
  1. package/README.md +146 -16
  2. package/dist/behaviors/accordion.d.ts +4 -0
  3. package/dist/behaviors/accordion.js +56 -0
  4. package/dist/behaviors/carousel.d.ts +4 -0
  5. package/dist/behaviors/carousel.js +127 -0
  6. package/dist/behaviors/filter.d.ts +1 -0
  7. package/dist/behaviors/filter.js +25 -0
  8. package/dist/behaviors/index.d.ts +14 -0
  9. package/dist/behaviors/index.js +78 -0
  10. package/dist/behaviors/motion.d.ts +13 -0
  11. package/dist/behaviors/motion.js +88 -0
  12. package/dist/behaviors/tabs.d.ts +3 -0
  13. package/dist/behaviors/tabs.js +55 -0
  14. package/dist/built/css.d.ts +2 -0
  15. package/dist/built/css.js +26 -0
  16. package/dist/built/theme.d.ts +19 -0
  17. package/dist/built/theme.js +86 -0
  18. package/dist/built/tree.d.ts +24 -0
  19. package/dist/built/tree.js +96 -0
  20. package/dist/built.test.d.ts +1 -0
  21. package/dist/built.test.js +257 -0
  22. package/dist/cli/check.js +31 -1
  23. package/dist/consent.d.ts +33 -0
  24. package/dist/consent.js +36 -0
  25. package/dist/consent.test.d.ts +1 -0
  26. package/dist/consent.test.js +61 -0
  27. package/dist/content/index.d.ts +450 -33
  28. package/dist/content/index.js +297 -15
  29. package/dist/content/lists.d.ts +69 -0
  30. package/dist/content/lists.js +77 -0
  31. package/dist/content/lists.test.d.ts +1 -0
  32. package/dist/content/lists.test.js +257 -0
  33. package/dist/content/look.d.ts +260 -9
  34. package/dist/content/look.js +386 -22
  35. package/dist/content-migrate.d.ts +51 -0
  36. package/dist/content-migrate.js +69 -0
  37. package/dist/finish.d.ts +1 -1
  38. package/dist/finish.js +27 -6
  39. package/dist/fonts/families/dm-sans/LICENSE +93 -0
  40. package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-italic.woff2 +0 -0
  41. package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-normal.woff2 +0 -0
  42. package/dist/fonts/families/dm-sans/dm-sans-latin-wght-italic.woff2 +0 -0
  43. package/dist/fonts/families/dm-sans/dm-sans-latin-wght-normal.woff2 +0 -0
  44. package/dist/fonts/families/fraunces/LICENSE +93 -0
  45. package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-italic.woff2 +0 -0
  46. package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-normal.woff2 +0 -0
  47. package/dist/fonts/families/fraunces/fraunces-latin-wght-italic.woff2 +0 -0
  48. package/dist/fonts/families/fraunces/fraunces-latin-wght-normal.woff2 +0 -0
  49. package/dist/fonts/families/inter/LICENSE +93 -0
  50. package/dist/fonts/families/inter/inter-latin-ext-wght-italic.woff2 +0 -0
  51. package/dist/fonts/families/inter/inter-latin-ext-wght-normal.woff2 +0 -0
  52. package/dist/fonts/families/inter/inter-latin-wght-italic.woff2 +0 -0
  53. package/dist/fonts/families/inter/inter-latin-wght-normal.woff2 +0 -0
  54. package/dist/fonts/families/lora/LICENSE +93 -0
  55. package/dist/fonts/families/lora/lora-latin-ext-wght-italic.woff2 +0 -0
  56. package/dist/fonts/families/lora/lora-latin-ext-wght-normal.woff2 +0 -0
  57. package/dist/fonts/families/lora/lora-latin-wght-italic.woff2 +0 -0
  58. package/dist/fonts/families/lora/lora-latin-wght-normal.woff2 +0 -0
  59. package/dist/fonts/families/manrope/LICENSE +93 -0
  60. package/dist/fonts/families/manrope/manrope-latin-ext-wght-normal.woff2 +0 -0
  61. package/dist/fonts/families/manrope/manrope-latin-wght-normal.woff2 +0 -0
  62. package/dist/fonts/families/newsreader/LICENSE +93 -0
  63. package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-italic.woff2 +0 -0
  64. package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-normal.woff2 +0 -0
  65. package/dist/fonts/families/newsreader/newsreader-latin-wght-italic.woff2 +0 -0
  66. package/dist/fonts/families/newsreader/newsreader-latin-wght-normal.woff2 +0 -0
  67. package/dist/fonts/families/outfit/LICENSE +93 -0
  68. package/dist/fonts/families/outfit/outfit-latin-ext-wght-normal.woff2 +0 -0
  69. package/dist/fonts/families/outfit/outfit-latin-wght-normal.woff2 +0 -0
  70. package/dist/fonts/families/playfair-display/LICENSE +93 -0
  71. package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-italic.woff2 +0 -0
  72. package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-normal.woff2 +0 -0
  73. package/dist/fonts/families/playfair-display/playfair-display-latin-wght-italic.woff2 +0 -0
  74. package/dist/fonts/families/playfair-display/playfair-display-latin-wght-normal.woff2 +0 -0
  75. package/dist/fonts/families/source-serif-4/LICENSE +93 -0
  76. package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
  77. package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
  78. package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-italic.woff2 +0 -0
  79. package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-normal.woff2 +0 -0
  80. package/dist/fonts/families/work-sans/LICENSE +93 -0
  81. package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-italic.woff2 +0 -0
  82. package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-normal.woff2 +0 -0
  83. package/dist/fonts/families/work-sans/work-sans-latin-wght-italic.woff2 +0 -0
  84. package/dist/fonts/families/work-sans/work-sans-latin-wght-normal.woff2 +0 -0
  85. package/dist/fonts/google.json +1 -0
  86. package/dist/fonts-google.d.ts +35 -0
  87. package/dist/fonts-google.js +70 -0
  88. package/dist/fonts.css +365 -0
  89. package/dist/frame.test.d.ts +1 -0
  90. package/dist/frame.test.js +58 -0
  91. package/dist/migrate.d.ts +45 -0
  92. package/dist/migrate.js +284 -0
  93. package/dist/next/block-mark.d.ts +7 -0
  94. package/dist/next/block-mark.js +16 -0
  95. package/dist/next/blocks.d.ts +11 -0
  96. package/dist/next/blocks.js +35 -0
  97. package/dist/next/built.d.ts +40 -0
  98. package/dist/next/built.js +318 -0
  99. package/dist/next/consent.d.ts +4 -0
  100. package/dist/next/consent.js +13 -0
  101. package/dist/next/data.d.ts +6 -2
  102. package/dist/next/data.js +10 -4
  103. package/dist/next/frame.d.ts +42 -0
  104. package/dist/next/frame.js +73 -0
  105. package/dist/next/index.d.ts +8 -2
  106. package/dist/next/index.js +9 -3
  107. package/dist/next/menu-group.d.ts +11 -0
  108. package/dist/next/menu-group.js +33 -0
  109. package/dist/next/metadata.js +3 -2
  110. package/dist/next/parts/Img.d.ts +6 -1
  111. package/dist/next/parts/Img.js +11 -3
  112. package/dist/next/parts/LookStyle.js +3 -1
  113. package/dist/next/preview.js +511 -27
  114. package/dist/next/sections.d.ts +19 -10
  115. package/dist/next/sections.js +134 -17
  116. package/dist/policies.js +22 -4
  117. package/dist/prepare.d.ts +15 -1
  118. package/dist/prepare.js +125 -3
  119. package/dist/preview-match.d.ts +29 -0
  120. package/dist/preview-match.js +58 -1
  121. package/dist/schema.d.ts +10 -0
  122. package/dist/schema.js +29 -1
  123. package/dist/sections/icons.d.ts +49 -0
  124. package/dist/sections/icons.js +55 -0
  125. package/dist/sections/index.d.ts +188 -12
  126. package/dist/sections/index.js +136 -10
  127. package/dist/sections/presets.d.ts +13 -0
  128. package/dist/sections/presets.js +300 -0
  129. package/dist/sections.css +1584 -41
  130. package/dist/sections.test.js +276 -14
  131. package/dist/seo.js +2 -1
  132. package/dist/tree/behaviors.d.ts +50 -0
  133. package/dist/tree/behaviors.js +67 -0
  134. package/dist/tree/classes.d.ts +24 -0
  135. package/dist/tree/classes.js +94 -0
  136. package/dist/tree/index.d.ts +145 -0
  137. package/dist/tree/index.js +196 -0
  138. package/dist/tree/messages.d.ts +45 -0
  139. package/dist/tree/messages.js +7 -0
  140. package/package.json +22 -3
@@ -4,20 +4,29 @@ export type KitBlock = {
4
4
  placement?: Record<string, unknown>;
5
5
  } & Record<string, unknown>;
6
6
  /**
7
- * The site's own classes for a kit section's parts, each in place of the kit's: its band, its
8
- * container, its heading, its words, and its buttons (the main one and a quieter one).
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).
9
13
  */
10
14
  export type KitClasses = {
11
- section?: string;
12
- wrap?: string;
13
- heading?: string;
14
- text?: string;
15
- button?: string;
16
- buttonQuiet?: string;
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;
17
24
  };
25
+ /** The kit's own classes, drawn from the Look by sections.css. */
26
+ export declare const KIT_CLASSES: KitClasses;
18
27
  /** A block of one of the kit's types, drawn; null for any other (the site's own to draw). */
19
- export declare function KitSection({ s, level, classes }: {
28
+ export declare function KitSection({ s, level, classes: given }: {
20
29
  s: KitBlock;
21
30
  level?: number;
22
- classes?: KitClasses;
31
+ classes?: Partial<KitClasses>;
23
32
  }): import("react").JSX.Element | null;
@@ -1,11 +1,27 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { placementClass } from "../content/index.js";
3
- import { image } from "./data.js";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { buttonStyleClass, placementClass } from "../content/index.js";
3
+ import { ICONS } from "../sections/icons.js";
4
+ import { videoEmbed } from "../sections/index.js";
5
+ import { image, isDraft, lookValues } from "./data.js";
4
6
  import { Img } from "./parts/Img.js";
5
7
  import { Md } from "./parts/Md.js";
6
8
  import { Slideshow } from "./slideshow.js";
9
+ import { BuiltSection, UseSection } from "./built.js";
10
+ /** The kit's own classes, drawn from the Look by sections.css. */
11
+ export const KIT_CLASSES = {
12
+ section: "kit-section",
13
+ wrap: "kit-wrap",
14
+ headingLarge: "kit-h kit-h-large",
15
+ headingMedium: "kit-h kit-h-medium",
16
+ headingSmall: "kit-h kit-h-small",
17
+ text: "kit-text",
18
+ muted: "kit-muted",
19
+ button: "kit-button",
20
+ buttonQuiet: "kit-button kit-button-quiet",
21
+ };
7
22
  /** A block of one of the kit's types, drawn; null for any other (the site's own to draw). */
8
- export function KitSection({ s, level = 2, classes = {} }) {
23
+ export function KitSection({ s, level = 2, classes: given }) {
24
+ const classes = { ...KIT_CLASSES, ...definedOnly(given) };
9
25
  switch (s.type) {
10
26
  case "gallery":
11
27
  return _jsx(Gallery, { s: s, level: level, classes: classes });
@@ -13,10 +29,18 @@ export function KitSection({ s, level = 2, classes = {} }) {
13
29
  return _jsx(Slides, { s: s, level: level, classes: classes });
14
30
  case "own":
15
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 });
16
36
  default:
17
37
  return null;
18
38
  }
19
39
  }
40
+ /** A site's classes, those it left undefined taking the kit's ("" is the site's: nothing). */
41
+ function definedOnly(c) {
42
+ return Object.fromEntries(Object.entries(c ?? {}).filter(([, v]) => typeof v === "string"));
43
+ }
20
44
  const RATIO = { square: "1/1", landscape: "4/3", portrait: "4/5", "as-taken": undefined };
21
45
  function Heading({ level, id, className, children }) {
22
46
  const H = `h${Math.min(Math.max(level, 1), 6)}`;
@@ -26,7 +50,7 @@ function Heading({ level, id, className, children }) {
26
50
  function Gallery({ s, level, classes }) {
27
51
  const columns = Number(s.columns ?? "3");
28
52
  const headingId = s.heading ? `${s.id ?? "gallery"}-heading` : undefined;
29
- return (_jsx("section", { className: [classes.section ?? "kit-section", "kit-gallery", placementClass(s)].filter(Boolean).join(" "), id: s.id, "aria-labelledby": headingId, children: _jsxs("div", { className: classes.wrap ?? "kit-wrap", children: [s.heading && (_jsx(Heading, { level: level, id: headingId, className: classes.heading ?? "kit-heading", children: s.heading })), s.intro && _jsx(Md, { text: s.intro, top: level + 1, className: "kit-intro" }), _jsx("ul", { className: `kit-gallery-grid kit-cols-${columns}`, children: s.images.map((img) => {
53
+ return (_jsx("section", { className: [classes.section, "kit-gallery", placementClass(s)].filter(Boolean).join(" "), id: s.id, "aria-labelledby": headingId, children: _jsxs("div", { className: classes.wrap, children: [s.heading && (_jsx(Heading, { level: level, id: headingId, className: classes.headingLarge, children: s.heading })), s.intro && _jsx(Md, { text: s.intro, top: level + 1, className: "kit-intro" }), _jsx("ul", { className: `kit-gallery-grid kit-cols-${columns}`, children: s.images.map((img) => {
30
54
  const copies = image(img.src).srcset;
31
55
  const largest = copies.at(-1)?.url ?? image(img.src).src;
32
56
  return (_jsx("li", { children: _jsxs("figure", { children: [_jsx("a", { href: largest, className: "kit-gallery-open", children: _jsx(Img, { src: img.src, alt: img.alt, sizes: `(min-width: 1024px) ${Math.round(100 / columns)}vw, 50vw`, ratio: RATIO[s.shape ?? "square"] }) }), img.caption && _jsx("figcaption", { children: img.caption })] }) }, img.src));
@@ -36,45 +60,102 @@ const SLIDE_RATIO = { wide: "kit-shape-wide", landscape: "kit-shape-landscape",
36
60
  /** Photos one at a time (Slideshow), each with its caption under it; its heading above when it has one. */
37
61
  function Slides({ s, level, classes }) {
38
62
  const headingId = s.heading ? `${s.id ?? "slideshow"}-heading` : undefined;
39
- return (_jsx("section", { className: [classes.section ?? "kit-section", "kit-slideshow", placementClass(s)].filter(Boolean).join(" "), id: s.id, "aria-labelledby": headingId, children: _jsxs("div", { className: classes.wrap ?? "kit-wrap", children: [s.heading && (_jsx(Heading, { level: level, id: headingId, className: classes.heading ?? "kit-heading", children: s.heading })), _jsx(Slideshow, { label: s.heading ?? "Photos", seconds: s.seconds, className: SLIDE_RATIO[s.shape ?? "wide"], children: s.images.map((img, i) => (_jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: "(min-width: 1200px) 1200px, 100vw", eager: i === 0 }), img.caption && _jsx("figcaption", { children: img.caption })] }, img.src))) })] }) }));
63
+ return (_jsx("section", { className: [classes.section, "kit-slideshow", placementClass(s)].filter(Boolean).join(" "), id: s.id, "aria-labelledby": headingId, children: _jsxs("div", { className: classes.wrap, children: [s.heading && (_jsx(Heading, { level: level, id: headingId, className: classes.headingLarge, children: s.heading })), _jsx(Slideshow, { label: s.heading ?? "Photos", seconds: s.seconds, className: SLIDE_RATIO[s.shape ?? "wide"], children: s.images.map((img, i) => (_jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: "(min-width: 1200px) 1200px, 100vw", eager: i === 0 }), img.caption && _jsx("figcaption", { children: img.caption })] }, img.src))) })] }) }));
40
64
  }
41
65
  const SHAPE = { "as-taken": undefined, landscape: "4/3", square: "1/1", portrait: "4/5" };
42
66
  const COLUMNS = { one: 1, two: 2, three: 3, four: 4, "wide-left": 2, "wide-right": 2 };
43
67
  /**
44
- * A section of the business's own: its columns of parts, laid out as chosen, each part in the site's
45
- * own classes where it gave them and the kit's otherwise. A heading is the page's next level down.
68
+ * A section of the business's own: its heading and intro (in the site's section heading style), then
69
+ * its columns of parts, laid out as chosen, each part in the site's own classes where it gave them
70
+ * and the kit's otherwise. A part's heading is the page's next level down (two down under a section
71
+ * heading). Its tone and spacing are classes (sections.css) from the site's Look: on soft, brand or
72
+ * dark it's a card inset within the content's width (its shape, unless it's a band across the
73
+ * page); a photo behind it is a picture under a scrim, the words light over it. On brand, dark or a
74
+ * photo the kit keeps the words and buttons readable over the site's own colour classes.
46
75
  */
47
76
  function Own({ s, level, classes }) {
48
77
  const n = COLUMNS[s.layout ?? "one"] ?? 1;
49
78
  const columns = s.columns.slice(0, n);
50
- return (_jsx("section", { className: [classes.section ?? "kit-section", "kit-own", `kit-own-${s.tone ?? "plain"}`, placementClass(s)].filter(Boolean).join(" "), id: s.id, children: _jsx("div", { className: classes.wrap ?? "kit-wrap", children: _jsx("div", { className: `kit-own-grid kit-own-${s.layout ?? "one"} kit-own-align-${s.align ?? "left"}`, children: columns.map((c, i) => (_jsx("div", { className: "kit-own-column", children: c.parts.map((p, j) => (_jsx(OwnPart, { p: p, level: level, classes: classes, columns: n }, j))) }, i))) }) }) }));
79
+ const tone = s.tone ?? "plain";
80
+ const behind = s.background;
81
+ const inverse = Boolean(behind) || tone === "brand" || tone === "dark";
82
+ const shaped = tone !== "plain" && !behind;
83
+ const align = s.align ?? "left";
84
+ const headingId = s.heading ? `${s.id ?? "own"}-heading` : undefined;
85
+ const partLevel = s.heading ? level + 1 : level;
86
+ const className = [
87
+ classes.section,
88
+ "kit-own",
89
+ `kit-own-${tone}`,
90
+ `kit-own-pad-${s.spacing ?? "normal"}`,
91
+ shaped && `kit-own-shape-${s.shape ?? "card"}`,
92
+ behind && "kit-own-behind",
93
+ inverse && "kit-own-inverse",
94
+ placementClass(s),
95
+ ]
96
+ .filter(Boolean)
97
+ .join(" ");
98
+ return (_jsxs("section", { className: className, id: s.id, "aria-labelledby": headingId, children: [behind && (_jsx("div", { className: "kit-own-bg", "aria-hidden": "true", children: _jsx(Img, { src: behind.src, alt: "", sizes: "100vw" }) })), _jsx("div", { className: classes.wrap, children: _jsxs("div", { className: "kit-own-panel", children: [(s.heading || s.intro) && (_jsxs("div", { className: `kit-own-head kit-own-align-${align}`, children: [s.heading && (_jsx(Heading, { level: level, id: headingId, className: classes.headingLarge, children: s.heading })), s.intro && _jsx(Md, { text: s.intro, top: level + 1, className: ["kit-own-intro", classes.text].filter(Boolean).join(" ") })] })), _jsx("div", { className: `kit-own-grid kit-own-${s.layout ?? "one"} kit-own-align-${align}`, children: columns.map((c, i) => (_jsx("div", { className: ["kit-own-column", c.card && "kit-own-card", c.end && "kit-own-column-end", c.place && c.place !== "top" && `kit-own-column-${c.place}`].filter(Boolean).join(" "), children: runs(c.parts).map((run) => run.buttons ? (_jsx("div", { className: "kit-own-actions", children: run.parts.map((p, j) => (_jsx(OwnPart, { p: p, level: partLevel, classes: classes, columns: n }, run.at + j))) }, run.at)) : (_jsx(OwnPart, { p: run.parts[0], level: partLevel, classes: classes, columns: n }, run.at))) }, i))) })] }) })] }));
99
+ }
100
+ /** A column's parts, buttons one after another kept together (side by side, wrapping on a phone). */
101
+ function runs(parts) {
102
+ const out = [];
103
+ parts.forEach((p, i) => {
104
+ const last = out.at(-1);
105
+ if (p.type === "button" && last?.buttons)
106
+ last.parts.push(p);
107
+ else
108
+ out.push({ at: i, buttons: p.type === "button", parts: [p] });
109
+ });
110
+ return out;
51
111
  }
112
+ /** A part's words in the Look's other font, where it says. */
113
+ const fontClass = (font) => (font === "text" ? "kit-font-text" : font === "heading" ? "kit-font-heading" : undefined);
52
114
  function OwnPart({ p, level, classes, columns }) {
53
115
  const width = `(min-width: 1024px) ${Math.round(100 / columns)}vw, 100vw`;
54
116
  switch (p.type) {
55
117
  case "heading": {
56
- // Large is the section's own heading level; medium and small the levels under it.
57
- const at = level + (p.size === "large" ? 0 : p.size === "small" ? 2 : 1);
58
- return (_jsx(Heading, { level: at, className: [classes.heading ?? "kit-heading", `kit-own-h-${p.size ?? "medium"}`].join(" "), children: p.text }));
118
+ // Display, page and large are the section's own heading level (display and page drawn at the
119
+ // Look's largest sizes); medium and small the levels under it.
120
+ const big = p.size === "large" || p.size === "display" || p.size === "page";
121
+ const at = level + (big ? 0 : p.size === "small" ? 2 : 1);
122
+ const cls = [big ? classes.headingLarge : p.size === "small" ? classes.headingSmall : classes.headingMedium, p.size === "display" && "kit-type-display", p.size === "page" && "kit-type-h1", fontClass(p.font)];
123
+ return (_jsx(Heading, { level: at, className: cls.filter(Boolean).join(" "), children: p.text }));
59
124
  }
60
125
  case "text":
61
- return _jsx(Md, { text: p.text, top: level + 1, className: ["kit-own-text", classes.text].filter(Boolean).join(" ") });
126
+ return _jsx(Md, { text: p.text, top: level + 1, className: ["kit-own-text", classes.text, fontClass(p.font)].filter(Boolean).join(" ") });
62
127
  case "photo":
128
+ // No photo chosen yet: a quiet box where it will go in CalOS's preview, nothing on the site.
129
+ if (!p.image)
130
+ return isDraft() ? _jsx("div", { className: `kit-own-photo-empty kit-own-shape-${p.shape ?? "as-taken"}`, "aria-hidden": "true" }) : null;
63
131
  return (_jsxs("figure", { className: "kit-own-photo", children: [_jsx(Img, { src: p.image.src, alt: p.image.alt, sizes: width, ratio: SHAPE[p.shape ?? "as-taken"] }), p.image.caption && _jsx("figcaption", { children: p.image.caption })] }));
64
132
  case "button": {
65
133
  const quiet = p.style === "quiet";
66
- return (_jsx("p", { className: "kit-own-action", children: _jsx("a", { href: p.href, className: (quiet ? classes.buttonQuiet : classes.button) ?? (quiet ? "kit-button kit-button-quiet" : "kit-button"), children: p.label }) }));
134
+ // One of the business's own button styles, where it names one (kit-btn-3); else the theme's.
135
+ let vars = {};
136
+ try {
137
+ vars = lookValues();
138
+ }
139
+ catch {
140
+ // Drawn without the site's data (a test): only the theme's styles.
141
+ }
142
+ const own = buttonStyleClass(p.style, vars);
143
+ return (_jsx("p", { className: quiet ? "kit-own-action kit-own-action-quiet" : "kit-own-action", children: _jsx("a", { href: p.href, className: [quiet ? classes.buttonQuiet : p.style === "accent" ? `${classes.button} kit-button-accent` : classes.button, own].filter(Boolean).join(" "), children: p.label }) }));
67
144
  }
68
145
  case "list": {
69
146
  const L = p.style === "numbers" ? "ol" : "ul";
70
147
  return (_jsx(L, { className: ["kit-own-list", `kit-own-list-${p.style ?? "bullets"}`, classes.text].filter(Boolean).join(" "), children: p.items.map((item, i) => (_jsx("li", { children: item }, i))) }));
71
148
  }
149
+ case "items": {
150
+ const L = p.numbered === false ? "ul" : "ol";
151
+ return (_jsx(L, { className: ["kit-own-items", p.lines === false ? "" : "kit-own-items-lines"].filter(Boolean).join(" "), children: p.items.map((it, i) => (_jsxs("li", { children: [p.numbered !== false && (_jsx("span", { className: ["kit-own-items-n", classes.muted].filter(Boolean).join(" "), "aria-hidden": "true", children: String(i + 1).padStart(2, "0") })), _jsxs("div", { children: [_jsx("h3", { className: ["kit-own-items-title", classes.headingMedium, fontClass(p.font)].filter(Boolean).join(" "), children: it.title }), it.text && _jsx("p", { className: ["kit-own-items-text", classes.muted].filter(Boolean).join(" "), children: it.text })] })] }, i))) }));
152
+ }
72
153
  case "quote":
73
- return (_jsxs("figure", { className: "kit-own-quote", children: [_jsx("blockquote", { className: classes.text, children: p.text }), p.who && _jsx("figcaption", { children: p.who })] }));
154
+ return (_jsxs("figure", { className: "kit-own-quote", children: [_jsx("blockquote", { className: [classes.text, fontClass(p.font)].filter(Boolean).join(" "), children: p.text }), p.who && _jsx("figcaption", { className: classes.muted, children: p.who })] }));
74
155
  case "stats":
75
- return (_jsx("dl", { className: "kit-own-stats", children: p.items.map((it, i) => (_jsxs("div", { children: [_jsx("dt", { children: it.label }), _jsx("dd", { children: it.value })] }, i))) }));
156
+ return (_jsx("dl", { className: "kit-own-stats", children: p.items.map((it, i) => (_jsxs("div", { children: [_jsx("dt", { className: classes.muted, children: it.label }), _jsx("dd", { className: classes.headingMedium, children: it.value })] }, i))) }));
76
157
  case "price":
77
- return (_jsxs("p", { className: "kit-own-price", children: [_jsx("span", { className: "kit-own-amount", children: p.amount }), p.per && _jsxs("span", { className: "kit-own-per", children: [" ", p.per] }), p.note && _jsx("span", { className: "kit-own-note", children: p.note })] }));
158
+ return (_jsxs("p", { className: "kit-own-price", children: [_jsx("span", { className: `kit-own-amount ${classes.headingMedium}`, children: p.amount }), p.per && _jsxs("span", { className: `kit-own-per ${classes.muted}`, children: [" ", p.per] }), p.note && _jsx("span", { className: `kit-own-note ${classes.muted}`, children: p.note })] }));
78
159
  case "gallery": {
79
160
  const n = Number(p.columns ?? "3");
80
161
  return (_jsx("ul", { className: `kit-gallery-grid kit-cols-${n}`, children: p.images.map((img) => (_jsx("li", { children: _jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: `(min-width: 1024px) ${Math.round(100 / (n * columns))}vw, 50vw`, ratio: "1/1" }), img.caption && _jsx("figcaption", { children: img.caption })] }) }, img.src))) }));
@@ -83,5 +164,41 @@ function OwnPart({ p, level, classes, columns }) {
83
164
  return (_jsx(Slideshow, { label: "Photos", seconds: p.seconds, className: "kit-shape-landscape", children: p.images.map((img, i) => (_jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: width, eager: i === 0 }), img.caption && _jsx("figcaption", { children: img.caption })] }, img.src))) }));
84
165
  case "space":
85
166
  return _jsx("div", { "aria-hidden": "true", className: `kit-own-space kit-own-space-${p.size ?? "medium"}` });
167
+ case "table":
168
+ return _jsx(Table, { p: p, level: level, classes: classes });
169
+ case "icon":
170
+ return ICONS[p.name] ? _jsx(Icon, { name: p.name, part: true, className: `kit-own-icon kit-own-icon-${p.size ?? "medium"}` }) : null;
171
+ case "logos":
172
+ return (_jsxs("div", { className: "kit-own-logos", children: [p.title && _jsx("p", { className: ["kit-own-logos-title", classes.muted].filter(Boolean).join(" "), children: p.title }), _jsx("ul", { children: p.images.map((img, i) => (_jsx("li", { children: _jsx(Img, { src: img.src, alt: img.alt, sizes: "200px" }) }, i))) })] }));
173
+ case "video": {
174
+ const embed = p.url ? videoEmbed(p.url) : null;
175
+ // No address yet (or one that isn't a video's): a quiet box where it will go in CalOS's preview, nothing on the site.
176
+ if (!embed)
177
+ return isDraft() ? _jsx("div", { className: "kit-own-video kit-own-video-empty", "aria-hidden": "true" }) : null;
178
+ return (_jsx("div", { className: "kit-own-video", children: _jsx("iframe", { src: embed.src, title: p.title, width: 1280, height: 720, loading: "lazy", allow: "accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen", allowFullScreen: true, referrerPolicy: "strict-origin-when-cross-origin" }) }));
179
+ }
86
180
  }
87
181
  }
182
+ /** One of the kit's line icons, drawn inline in the colour of what it's in; beside words, so hidden from a screen reader. */
183
+ function Icon({ name, className, part }) {
184
+ return (_jsx("svg", { "data-icon": part ? name : undefined, className: className ? `kit-icon ${className}` : "kit-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: ICONS[name].map((d, i) => (_jsx("path", { d: d }, i))) }));
185
+ }
186
+ /** A cell's words, or a tick for "yes" and a dash for "no", each read out as the word. */
187
+ function Cell({ value }) {
188
+ const v = value.trim().toLowerCase();
189
+ if (v === "yes" || v === "no")
190
+ return (_jsxs("span", { className: v === "yes" ? "kit-table-yes" : "kit-table-no", children: [_jsx(Icon, { name: v === "yes" ? "check" : "minus" }), _jsx("span", { className: "kit-sr", children: v === "yes" ? "Yes" : "No" })] }));
191
+ return _jsx(_Fragment, { children: value });
192
+ }
193
+ /**
194
+ * Rows compared across a few columns: a table on a wider screen (a heading over each column, each
195
+ * row's label heading its row), and on a phone each row a small card of its own, its label as its
196
+ * heading, then each column's heading and the row's cell under it, so nothing is read across a
197
+ * table too wide to see. One or the other shows (sections.css), never both.
198
+ */
199
+ function Table({ p, level, classes }) {
200
+ const heads = p.columns.slice(1);
201
+ const cells = (r) => heads.map((_, i) => r.cells[i] ?? "");
202
+ const picked = (i) => (p.highlight === i + 1 ? "kit-table-highlight" : undefined);
203
+ return (_jsxs("div", { className: "kit-own-table", children: [_jsx("div", { className: "kit-table-wide", children: _jsxs("table", { className: ["kit-table", classes.text].filter(Boolean).join(" "), children: [_jsx("thead", { children: _jsxs("tr", { children: [p.columns[0] ? _jsx("th", { scope: "col", children: p.columns[0] }) : _jsx("td", {}), heads.map((h, i) => (_jsx("th", { scope: "col", className: picked(i), children: h }, i)))] }) }), _jsx("tbody", { children: p.rows.map((r, j) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: r.label }), cells(r).map((c, i) => (_jsx("td", { className: picked(i), children: _jsx(Cell, { value: c }) }, i)))] }, j))) })] }) }), _jsx("ul", { className: "kit-table-cards", children: p.rows.map((r, j) => (_jsxs("li", { className: "kit-table-card", children: [_jsx(Heading, { level: level, className: classes.headingSmall, children: r.label }), _jsx("dl", { className: classes.text || undefined, children: cells(r).map((c, i) => (_jsxs("div", { className: picked(i), children: [_jsx("dt", { children: heads[i] }), _jsx("dd", { children: _jsx(Cell, { value: c }) })] }, i))) })] }, j))) })] }));
204
+ }
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
- counts
35
- ? `This site doesn't use cookies or advertising trackers. Its fonts and pictures come from this site itself.`
36
- : `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.`,
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
- import type { Site } from "./content/index.ts";
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;
@@ -27,6 +28,15 @@ export type Prepared = {
27
28
  preferPicture?: boolean;
28
29
  /** Where its visits are counted, when CalOS counts them (Caltho's analytics server): finish puts its script in every page. */
29
30
  analytics?: Analytics;
31
+ /** The Look's values as CSS variables (lookVars), for a site with a Look: KitBlocks reads its colours to know a dark background. */
32
+ look?: Record<string, string>;
33
+ /** The @font-face rules for the Google fonts its Look uses, their files fetched into public/_fonts (LookStyle writes them). */
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
+ };
30
40
  };
31
41
  /** A site's visits counted: the id its pages send them with, where they're sent, and the script that sends them. */
32
42
  export type Analytics = {
@@ -35,6 +45,10 @@ export type Analytics = {
35
45
  script: string;
36
46
  };
37
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;
38
52
  export declare function analyticsIn(body: {
39
53
  analytics?: unknown;
40
54
  } | null): Analytics | undefined;
package/dist/prepare.js CHANGED
@@ -13,15 +13,31 @@ 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
+ import { lookVars, uploadedFontCss } from "./content/index.js";
24
+ import { fontFaceCss, fontFiles, googleFont, servedAt } from "./fonts-google.js";
22
25
  import { imagesIn, shareImageOf } from "./images.js";
23
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";
24
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
+ }
25
41
  export function analyticsIn(body) {
26
42
  const a = body?.analytics;
27
43
  const address = (v) => typeof v === "string" && /^(https:\/\/|http:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/?)/i.test(v) && URL.canParse(v);
@@ -41,6 +57,7 @@ export async function prepare(root, env = process.env, log = console.log) {
41
57
  let from;
42
58
  let editors = [];
43
59
  let analytics;
60
+ let forms;
44
61
  // A site CalOS has nothing published for yet (just made, its content not brought in) builds from
45
62
  // its own until there is: 404 is "nothing published", any other refusal is an error.
46
63
  const res = env.CALOS_CONTENT_URL
@@ -62,6 +79,7 @@ export async function prepare(root, env = process.env, log = console.log) {
62
79
  throw new Error(`Couldn't get the content from CalOS (${env.CALOS_CONTENT_URL}): ${res.status}`);
63
80
  const body = (await res.json());
64
81
  raw = body.content;
82
+ forms = formsIn(body);
65
83
  editors = editorsIn(body);
66
84
  analytics = analyticsIn(body);
67
85
  from = "CalOS";
@@ -88,6 +106,13 @@ export async function prepare(root, env = process.env, log = console.log) {
88
106
  from = "content/site.json";
89
107
  }
90
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;
91
116
  const parsed = definition.parse(raw);
92
117
  if (parsed.problems)
93
118
  throw new Error(`The site's content needs fixing (${from}):\n - ${parsed.problems.join("\n - ")}`);
@@ -97,6 +122,11 @@ export async function prepare(root, env = process.env, log = console.log) {
97
122
  site.countsVisits = true;
98
123
  else
99
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;
100
130
  for (const w of parsed.warnings)
101
131
  log(`site-kit: warning: ${w}`);
102
132
  const schema = definition.jsonSchema();
@@ -107,15 +137,53 @@ export async function prepare(root, env = process.env, log = console.log) {
107
137
  const images = await pictures(site, imagesDir, pub, log);
108
138
  const cards = await makeCards(site, definition.share, root, pub, log);
109
139
  const preferPicture = !!definition.share && definition.share.prefer === "picture";
110
- const prepared = { site, images, csp: definition.csp, cards, ...(preferPicture ? { preferPicture } : {}), ...(editors.length ? { editors } : {}), ...(analytics ? { analytics } : {}) };
140
+ const look = definition.look ? lookVars(site.look, definition.look) : undefined;
141
+ const fontFaces = definition.look ? [await googleFontsFor(site.look, definition.look, pub, log), await uploadedFontsFor(site.look, imagesDir, pub, log)].filter(Boolean).join("\n") : "";
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 } : {}) };
111
144
  await writeFile(join(out, "prepared.json"), JSON.stringify(prepared));
112
145
  log(`site-kit: content from ${from}, ${site.pages.length} pages, ${Object.keys(images).length} images${analytics ? ", visits counted" : ""}`);
113
146
  return prepared;
114
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
+ }
115
183
  // ---- pictures ----
116
184
  async function pictures(site, imagesDir, pub, log) {
117
185
  const used = imagesIn(site);
118
- const shared = new Set(site.pages.map((p) => shareImageOf(site, p)?.src).filter((s) => !!s));
186
+ const shared = new Set(allPages(site).map((p) => shareImageOf(site, p)?.src).filter((s) => !!s));
119
187
  const have = existsSync(imagesDir) ? await list(imagesDir) : [];
120
188
  const missing = [...used].filter((src) => !have.includes(src));
121
189
  if (missing.length) {
@@ -217,3 +285,57 @@ async function pushSchema(contentUrl, schemaUrl, token, schema, log) {
217
285
  log(`site-kit: warning: couldn't tell CalOS the site's schema (${url}): ${err.message}`);
218
286
  }
219
287
  }
288
+ /**
289
+ * The Google fonts the Look uses (a heading or text font that isn't one of the site's own): each
290
+ * one's files fetched once into public/_fonts, so the site serves them itself, and their
291
+ * @font-face rules. A file a font doesn't have (no italic at a weight) is skipped; one that can't
292
+ * be fetched stops the build, rather than a site going out in the wrong font.
293
+ */
294
+ /**
295
+ * The fonts the business uploaded in CalOS (fetched with its pictures into content/images/fonts):
296
+ * copied to public/_fonts/own, served by the site, and their @font-face rules. A file the content
297
+ * names that isn't there stops the build, as a missing picture does.
298
+ */
299
+ async function uploadedFontsFor(look, imagesDir, pub, log) {
300
+ const rules = [];
301
+ for (const f of look?.uploadedFonts ?? []) {
302
+ for (const x of f.files) {
303
+ const from = join(imagesDir, x.src);
304
+ if (!existsSync(from))
305
+ throw new Error(`The content names a font file that isn't in content/images: ${x.src}`);
306
+ const at = join(pub, "_fonts", "own", x.src.slice("fonts/".length));
307
+ await mkdir(dirname(at), { recursive: true });
308
+ await writeFile(at, await readFile(from));
309
+ }
310
+ rules.push(uploadedFontCss(f, (src) => `/_fonts/own/${src.slice("fonts/".length)}`));
311
+ log(`site-kit: ${f.family} (uploaded), ${f.files.length} files, served by the site`);
312
+ }
313
+ return rules.join("\n");
314
+ }
315
+ async function googleFontsFor(look, o, pub, log) {
316
+ const own = o.fonts ?? {};
317
+ const names = [...new Set([look?.headingFont, look?.bodyFont].filter((n) => typeof n === "string" && !(n in own)))];
318
+ const rules = [];
319
+ for (const name of names) {
320
+ const f = googleFont(name);
321
+ if (!f)
322
+ continue;
323
+ const kept = [];
324
+ for (const x of fontFiles(f)) {
325
+ const at = join(pub, servedAt(f, x));
326
+ if (!existsSync(at)) {
327
+ const res = await fetch(x.url);
328
+ if (res.status === 404)
329
+ continue;
330
+ if (!res.ok)
331
+ throw new Error(`Couldn't fetch ${f.family} (${x.file}): ${res.status}`);
332
+ await mkdir(dirname(at), { recursive: true });
333
+ await writeFile(at, new Uint8Array(await res.arrayBuffer()));
334
+ }
335
+ kept.push(x);
336
+ }
337
+ rules.push(fontFaceCss(f, kept, (x) => servedAt(f, x)));
338
+ log(`site-kit: ${f.family}, ${kept.length} files, served by the site`);
339
+ }
340
+ return rules.join("\n");
341
+ }
@@ -23,3 +23,32 @@ export declare function fieldForImage(value: unknown, base: Path, url: string, i
23
23
  url: string;
24
24
  }[];
25
25
  }>): Path | null;
26
+ /**
27
+ * Which block drew each of `main`'s drawn children, by the block's index (null: not known). `drawn`:
28
+ * each child's words, its pictures' alt text included. One drawn for each block pairs them in order.
29
+ * Otherwise they're found by their words, in order: a block may draw nothing (a list with nothing in
30
+ * it yet) or more than one (a hero above its rows), and a child without the words to say is left
31
+ * unknown, never guessed, unless the next block has no words to find it by.
32
+ */
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;