@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
@@ -0,0 +1,318 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A built section drawn (cal-os docs/design/site-builder.md): a tree of elements, each its tag with
3
+ // its Tailwind classes, as the business (or Cal) made it in CalOS. And a component used by name, its
4
+ // slots filled. Drawn by KitSection like the kit's other sections, on the server for the published
5
+ // page and in the browser for CalOS's preview, with no hooks, so it's the same in both.
6
+ //
7
+ // Nothing in a tree is drawn unchecked, whatever the content says: a tag outside the list is left
8
+ // out, a class that isn't allowed is dropped (safeClasses), a link that isn't a page, a part, an
9
+ // http(s), mailto: or tel: address goes nowhere, a picture is one of the site's own through the
10
+ // kit's images, a video is the kit's own player address built from a provider and an id, and words
11
+ // are light Markdown whose HTML is shown as text. Classes are drawn prefixed (kit:p-4), so the CSS
12
+ // made for them (../built/css.ts) touches nothing else on the site.
13
+ //
14
+ // In CalOS's preview each element carries its node's id (data-node), so a click names the node
15
+ // exactly; the published page never does. A published page links the CSS made for built sections
16
+ // and the scripts of the behaviors it uses, once each however many sections use them (React puts
17
+ // them in the head); the preview draws classes with Tailwind's browser build and runs behaviors
18
+ // itself (./preview.tsx).
19
+ import { THIS_ITEM } from "../content/lists.js";
20
+ import { placementClass } from "../content/index.js";
21
+ import { mdInline } from "../markdown.js";
22
+ import { BEHAVIOR_ATTR, SETTINGS_ATTR } from "../behaviors/index.js";
23
+ import { nodeClasses, readPath } from "../built/tree.js";
24
+ import { ICONS } from "../sections/icons.js";
25
+ import { imageRef, LIMITS, NODE_ATTR, safeHref, TAGS, VOID_TAGS } from "../tree/index.js";
26
+ import { isDraft, prepared } from "./data.js";
27
+ import { Img } from "./parts/Img.js";
28
+ const ALLOWED = new Set(TAGS);
29
+ const VIDEO_ID = /^[\w-]{6,20}$/;
30
+ const CONTROLS = new Set(["prev", "next", "dots", "pause"]);
31
+ /** Where the published page gets built sections' CSS and behaviors' scripts (prepare writes them; none before it has). */
32
+ function assetsOf() {
33
+ try {
34
+ return prepared().built ?? {};
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ function contentOf() {
41
+ try {
42
+ return prepared().site;
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ }
48
+ /** A built section: its tree, the block's id and Layout on its root. */
49
+ export function BuiltSection({ s }) {
50
+ const root = s.root;
51
+ if (!root || typeof root !== "object")
52
+ return null;
53
+ return _jsx(Drawn, { root: root, block: s, ctx: { site: contentOf(), self: s[THIS_ITEM], preview: isDraft(), behaviors: new Set() } });
54
+ }
55
+ /** A component used by name, its slots filled; nothing (a quiet box in the preview) when the site has no such component. */
56
+ export function UseSection({ s }) {
57
+ const site = contentOf();
58
+ const c = (site.components ?? []).find((x) => x?.key === s.component);
59
+ if (!c)
60
+ return isDraft() ? _jsx("div", { className: "kit-built-missing", "aria-hidden": "true" }) : null;
61
+ const slots = (s.slots && typeof s.slots === "object" ? s.slots : {});
62
+ return _jsx(Drawn, { root: c.root, block: s, ctx: { site, self: s[THIS_ITEM], slots, preview: isDraft(), behaviors: new Set() } });
63
+ }
64
+ /**
65
+ * One of the site's components drawn outside a page's blocks (a cookie banner's design): its slots
66
+ * filled, the kit's attributes on the elements of the slots named in `marks`. Nothing for a
67
+ * component the site hasn't.
68
+ */
69
+ export function ComponentTree({ name, slots, marks }) {
70
+ const site = contentOf();
71
+ const c = (site.components ?? []).find((x) => x?.key === name);
72
+ if (!c)
73
+ return null;
74
+ return _jsx(Drawn, { root: c.root, block: { type: "use" }, ctx: { site, slots, marks, preview: isDraft(), behaviors: new Set() } });
75
+ }
76
+ /**
77
+ * The site's own header or footer (content's ownHeader, ownFooter): its tree drawn as a built
78
+ * section's, its links to the page it's on marked (aria-current).
79
+ */
80
+ export function BuiltFrame({ tree, current, which }) {
81
+ const root = tree.root;
82
+ if (!root || typeof root !== "object")
83
+ return null;
84
+ return _jsx(Drawn, { root: root, block: { type: "built" }, ctx: { site: contentOf(), preview: isDraft(), behaviors: new Set(), current }, mark: { "data-kit-frame": which ?? (root.tag === "footer" ? "footer" : "header") } });
85
+ }
86
+ /** The tree, and on the published page what it needs loaded: the CSS and its behaviors' scripts. */
87
+ function Drawn({ root, block, ctx, mark }) {
88
+ const top = { attrs: { id: block.id, "data-kit-built": "", ...mark }, placement: placementClass(block) };
89
+ const drawn = expand([root], ctx).map((it, i) => draw(it.node, it.ctx, 1, i === 0 ? { attrs: top.attrs } : { attrs: { "data-kit-built": "" } }, top.placement, it.key));
90
+ if (ctx.preview)
91
+ return _jsx(_Fragment, { children: drawn });
92
+ const assets = assetsOf();
93
+ return (_jsxs(_Fragment, { children: [assets.css && _jsx("link", { rel: "stylesheet", href: assets.css, precedence: "kit-built" }), [...ctx.behaviors].map((name) => (assets.scripts?.[name] ? _jsx("script", { async: true, src: assets.scripts[name] }, name) : null)), drawn] }));
94
+ }
95
+ /** A list of nodes as drawn: hidden ones left out, a repeated one once for each item of its list. */
96
+ function expand(list, ctx) {
97
+ const out = [];
98
+ for (const n of list ?? []) {
99
+ if (!n || typeof n !== "object" || n.hidden)
100
+ continue;
101
+ if (n.repeat) {
102
+ const items = readPath(n.repeat.path, ctx.site, ctx.item, ctx.self);
103
+ if (!Array.isArray(items))
104
+ continue;
105
+ items.slice(0, Math.min(n.repeat.limit ?? 100, 100)).forEach((item, i) => out.push({ node: n, ctx: { ...ctx, item }, key: `${n.id}-${i}` }));
106
+ }
107
+ else
108
+ out.push({ node: n, ctx, key: n.id });
109
+ }
110
+ return out;
111
+ }
112
+ /** A node's words, link and picture: what its slot is filled with, else what it's bound to, else its own. */
113
+ /** A form field's id on the page, from its node's: a label names it by this. */
114
+ const fieldId = (nodeId) => `kit-f-${nodeId}`;
115
+ /** What the browser fills in for a field, by the name its answer is sent as. */
116
+ const AUTOCOMPLETE = { name: "name", first_name: "given-name", last_name: "family-name", email: "email", phone: "tel", company: "organization", business: "organization" };
117
+ /**
118
+ * Where a form sends: the business's form it names (its key, among the addresses CalOS gave the
119
+ * site: `forms`), or, naming none, the site's own (its content's `form`). Only an http(s) address,
120
+ * else none: a form naming one the site wasn't given isn't sent anywhere else.
121
+ */
122
+ function formAddress(site, sendTo) {
123
+ const s = site && typeof site === "object" ? site : {};
124
+ const f = sendTo ? (s.forms && typeof s.forms === "object" ? s.forms[sendTo] : undefined) : s.form;
125
+ return typeof f === "string" && /^https?:\/\/\S+$/i.test(f) ? f : null;
126
+ }
127
+ /** A bound value as a link: an email address is a mailto:, a phone number a tel: (dialled as its digits), anything else as it is. */
128
+ export function linkFor(v) {
129
+ const s = v.trim();
130
+ if (/^[^\s@/:]+@[^\s@/]+\.[^\s@/]+$/.test(s))
131
+ return `mailto:${s}`;
132
+ if (/^\+?[\d\s().-]{7,}$/.test(s) && /\d{7,}/.test(s.replace(/\D/g, "")))
133
+ return `tel:${s.startsWith("+") ? "+" : ""}${s.replace(/\D/g, "")}`;
134
+ return s;
135
+ }
136
+ function valuesOf(n, ctx) {
137
+ let text = n.text;
138
+ let href = n.href;
139
+ let image = n.image;
140
+ if (n.bind) {
141
+ const v = readPath(n.bind.path, ctx.site, ctx.item, ctx.self);
142
+ const as = n.bind.as ?? (v && typeof v === "object" ? "image" : n.tag === "img" ? "image" : "text");
143
+ if (as === "image" && v && typeof v === "object")
144
+ image = v;
145
+ else if (as === "href" && typeof v === "string")
146
+ href = linkFor(v);
147
+ else if (as === "text" && (typeof v === "string" || typeof v === "number"))
148
+ text = String(v);
149
+ }
150
+ const fill = n.slot ? ctx.slots?.[n.slot] : undefined;
151
+ if (fill) {
152
+ if (typeof fill.text === "string")
153
+ text = fill.text;
154
+ if (typeof fill.href === "string")
155
+ href = fill.href;
156
+ if (fill.image)
157
+ image = fill.image;
158
+ }
159
+ return { text, href: href && safeHref.safeParse(href).success ? href : undefined, image: image && imageRef.safeParse(image).success ? imageRef.parse(image) : undefined };
160
+ }
161
+ /** One node drawn as its element, its children after it, with whatever its behavior adds. */
162
+ function draw(n, ctx, depth, extra = {}, kitClass, key) {
163
+ if (depth > LIMITS.depth || !ALLOWED.has(n.tag))
164
+ return null;
165
+ const { text, href, image } = valuesOf(n, ctx);
166
+ const className = [kitClass, ...nodeClasses(n)].filter(Boolean).join(" ") || undefined;
167
+ const attrs = { className, ...(ctx.preview ? { [NODE_ATTR]: n.id } : {}), ...(n.label ? { "aria-label": n.label } : {}), ...(n.slot ? ctx.marks?.[n.slot] : {}), ...extra.attrs };
168
+ // An item a filter chooses between: its value of the filter's field, as the filter matches words.
169
+ if (n.repeat && ctx.filterField && ctx.item && typeof ctx.item === "object") {
170
+ const v = ctx.item[ctx.filterField];
171
+ attrs["data-kit-filter-value"] = String(v ?? "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
172
+ }
173
+ const b = n.behavior;
174
+ if (b) {
175
+ ctx.behaviors.add(b.name);
176
+ const { name, ...settings } = b;
177
+ attrs[BEHAVIOR_ATTR] = name;
178
+ if (Object.keys(settings).length)
179
+ attrs[SETTINGS_ATTR] = JSON.stringify(settings);
180
+ if (b.name === "reveal")
181
+ attrs["data-kit-effect"] = b.effect ?? "up";
182
+ }
183
+ switch (n.tag) {
184
+ case "img":
185
+ if (!image)
186
+ return null;
187
+ return _jsx(Img, { src: image.src, alt: image.alt ?? "", phoneSrc: image.phoneSrc, sizes: "(min-width: 1200px) 1200px, 100vw", className: className, node: ctx.preview ? n.id : undefined }, key);
188
+ case "icon": {
189
+ const paths = n.icon && Object.prototype.hasOwnProperty.call(ICONS, n.icon) ? ICONS[n.icon] : null;
190
+ if (!paths)
191
+ return null;
192
+ return (_jsx("svg", { ...attrs, className: ["kit-built-icon", className].filter(Boolean).join(" "), viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": n.label ? undefined : true, role: n.label ? "img" : undefined, focusable: "false", children: paths.map((d, i) => (_jsx("path", { d: d }, i))) }, key));
193
+ }
194
+ case "video": {
195
+ const v = n.video;
196
+ if (!v || !VIDEO_ID.test(v.id) || (v.provider !== "youtube" && v.provider !== "vimeo"))
197
+ return null;
198
+ // The kit's own player addresses, as an own section's video part uses: YouTube's no-cookie one, Vimeo's with tracking off.
199
+ const src = v.provider === "youtube" ? `https://www.youtube-nocookie.com/embed/${v.id}` : /^\d+$/.test(v.id) ? `https://player.vimeo.com/video/${v.id}?dnt=1` : null;
200
+ if (!src)
201
+ return null;
202
+ return _jsx("iframe", { ...attrs, src: src, title: n.label ?? n.name ?? "Video", width: 1280, height: 720, loading: "lazy", allow: "accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen", allowFullScreen: true, referrerPolicy: "strict-origin-when-cross-origin" }, key);
203
+ }
204
+ // A form's parts: plain HTML, sent by the browser with no script to the business's form it names
205
+ // (or the site's own, its content's `form`), never an address in the tree. Each field's id is its node's, so a
206
+ // label names it; the kit adds the spam trap a person never fills in. Without the address (not set
207
+ // yet), it's drawn but can't be sent: a box, not a form.
208
+ case "form": {
209
+ const action = formAddress(ctx.site, n.sendTo);
210
+ const inner = expand(n.children, { ...ctx, inForm: true }).map((k, i) => draw(k.node, k.ctx, depth + 1, {}, undefined, `${k.key}-${i}`));
211
+ if (!action)
212
+ return _jsx("div", { ...attrs, "data-kit-form-unset": "", children: inner }, key);
213
+ return (_jsxs("form", { ...attrs, method: "post", action: action, children: [inner, _jsx("div", { className: "kit-trap", "aria-hidden": "true", children: _jsxs("label", { children: ["Leave this empty", _jsx("input", { name: "_website", type: "text", tabIndex: -1, autoComplete: "off" })] }) })] }, key));
214
+ }
215
+ case "input": {
216
+ const f = n.field;
217
+ if (!f)
218
+ return null;
219
+ const type = f.type ?? "text";
220
+ const choice = type === "checkbox" || type === "radio";
221
+ return _jsx("input", { ...attrs, id: fieldId(n.id), name: f.name, type: type, required: f.required || undefined, placeholder: choice ? undefined : f.placeholder, value: choice ? (f.value ?? "yes") : undefined, defaultChecked: (choice && f.checked) || undefined, autoComplete: AUTOCOMPLETE[f.name] }, key);
222
+ }
223
+ case "textarea": {
224
+ const f = n.field;
225
+ if (!f)
226
+ return null;
227
+ return _jsx("textarea", { ...attrs, id: fieldId(n.id), name: f.name, rows: f.rows ?? 5, required: f.required || undefined, placeholder: f.placeholder }, key);
228
+ }
229
+ case "select": {
230
+ const f = n.field;
231
+ if (!f)
232
+ return null;
233
+ return (_jsxs("select", { ...attrs, id: fieldId(n.id), name: f.name, required: f.required || undefined, defaultValue: "", children: [f.placeholder && (_jsx("option", { value: "", disabled: true, children: f.placeholder })), (n.children ?? []).filter((c) => c.tag === "option").map((c) => (_jsx("option", { value: c.text ?? "", children: c.text ?? "" }, c.id)))] }, key));
234
+ }
235
+ case "option":
236
+ return null; // drawn by its list of choices
237
+ case "hr":
238
+ case "br": {
239
+ const Void = n.tag;
240
+ return _jsx(Void, { ...attrs }, key);
241
+ }
242
+ }
243
+ // A link: checked again (valuesOf), opened in a new tab only for an outside address. A button with
244
+ // a link goes there (drawn as a link); without one, it's a button that does nothing by itself (a
245
+ // behavior's control, a tab).
246
+ let Tag = n.tag;
247
+ if (n.tag === "a" || (n.tag === "button" && href)) {
248
+ Tag = "a";
249
+ if (href)
250
+ attrs.href = href;
251
+ // A link to the page it's on (or, past home, to a page above it) says so: designs style it with aria-[current=page]:.
252
+ const here = ctx.current && href ? href.split("#")[0].replace(/\/?$/, "/") : null;
253
+ const at = ctx.current?.replace(/\/?$/, "/");
254
+ if (here && at && !href.startsWith("#") && (here === at || (here !== "/" && at.startsWith(here))))
255
+ attrs["aria-current"] = "page";
256
+ if (href && n.external && /^https?:/i.test(href))
257
+ Object.assign(attrs, { target: "_blank", rel: "noopener noreferrer" });
258
+ }
259
+ else if (n.tag === "button")
260
+ attrs.type = ctx.inForm ? "submit" : "button";
261
+ if (n.tag === "label" && n.for)
262
+ attrs.htmlFor = fieldId(n.for);
263
+ const words = text ? _jsx("span", { dangerouslySetInnerHTML: { __html: mdInline(text) } }) : null;
264
+ // A filter's items are marked with their value of its field, wherever they're repeated inside it.
265
+ const kids = expand(n.children, b?.name === "filter" ? { ...ctx, filterField: b.field } : ctx);
266
+ let inner;
267
+ if (b?.name === "carousel")
268
+ inner = carouselInner(n, b, kids, depth, attrs);
269
+ else if (b?.name === "marquee") {
270
+ const row = kids.map((k, i) => draw(k.node, k.ctx, depth + 1, {}, undefined, `${k.key}-${i}`));
271
+ inner = (_jsxs("div", { className: "kit-marquee-track", "data-kit-track": "", children: [_jsx("div", { className: "kit-marquee-group", children: row }), _jsx("div", { className: "kit-marquee-group", "data-kit-copy": "", "aria-hidden": "true", inert: true, children: kids.map((k, i) => draw(k.node, k.ctx, depth + 1, {}, undefined, `${k.key}-copy-${i}`)) })] }));
272
+ }
273
+ else {
274
+ const childAttrs = (i) => {
275
+ if (extra.child)
276
+ return extra.child(i);
277
+ if (b?.name === "tabs")
278
+ return i === 0 ? { "data-kit-tablist": "" } : { "data-kit-panel": "" };
279
+ return undefined;
280
+ };
281
+ inner = kids.map((k, i) => {
282
+ // An accordion's item: its first child the heading that opens it, the rest what it opens.
283
+ const item = b?.name === "accordion" ? { attrs: { "data-kit-item": "" }, child: (j) => (j === 0 ? { "data-kit-head": "" } : { "data-kit-body": "" }) } : { attrs: childAttrs(i) };
284
+ return draw(k.node, k.ctx, depth + 1, item, undefined, `${k.key}-${i}`);
285
+ });
286
+ }
287
+ if (VOID_TAGS.includes(n.tag))
288
+ return null;
289
+ // Words alone are the element's own content; with children too, the words come first.
290
+ if (words && !kids.length && !extra.inner && b?.name !== "carousel")
291
+ return _jsx(Tag, { ...attrs, dangerouslySetInnerHTML: { __html: mdInline(text) } }, key);
292
+ return (_jsxs(Tag, { ...attrs, children: [words, inner, extra.inner] }, key));
293
+ }
294
+ /**
295
+ * A carousel's inside: its slides (its children, but for any marked as its controls: prev, next,
296
+ * dots) in a track, the first shown; its own controls where they are; and the kit's (dots, arrows,
297
+ * a stop button where it moves by itself) where the design has none. The controls show only once
298
+ * its script has started (sections.css): without it, the first slide alone.
299
+ */
300
+ function carouselInner(n, b, kids, depth, attrs) {
301
+ const slides = kids.filter((k) => !(k.node.slot && CONTROLS.has(k.node.slot)));
302
+ const controls = kids.filter((k) => k.node.slot && CONTROLS.has(k.node.slot));
303
+ const count = slides.length;
304
+ Object.assign(attrs, { role: "region", "aria-roledescription": "carousel", "aria-label": n.label ?? n.name ?? "Slides" });
305
+ const dots = (className) => slides.map((_, i) => _jsx("button", { type: "button", className: className, "data-kit-to": String(i), "aria-label": `Show slide ${i + 1} of ${count}`, "aria-current": i === 0 ? "true" : "false" }, i));
306
+ const has = (slot) => controls.some((c) => c.node.slot === slot);
307
+ const kitDots = b.dots !== false && !has("dots") && count > 1;
308
+ const kitArrows = b.arrows === true && !has("prev") && !has("next") && count > 1;
309
+ const arrow = (dir) => (_jsx("button", { type: "button", className: `kit-carousel-arrow kit-carousel-${dir}`, [`data-kit-${dir}`]: "", "aria-label": dir === "prev" ? "Previous slide" : "Next slide", children: _jsx("svg", { className: "kit-built-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: ICONS["arrow-right"].map((d, i) => (_jsx("path", { d: d }, i))) }) }));
310
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "kit-carousel-track", "data-kit-track": "", "data-kit-transition": b.transition ?? "fade", children: slides.map((k, i) => draw(k.node, k.ctx, depth + 1, { attrs: { "data-kit-slide": "", "data-active": i === 0 ? "" : undefined, role: "group", "aria-roledescription": "slide", "aria-label": `${i + 1} of ${count}` } }, undefined, `${k.key}-${i}`)) }), controls.map((k, i) => k.node.slot === "dots"
311
+ ? draw({ ...k.node, children: [], text: undefined }, k.ctx, depth + 1, { inner: dots("kit-carousel-dot") }, undefined, `${k.key}-${i}`)
312
+ : k.node.slot === "pause"
313
+ ? // The design's own place for the stop button (where it moves by itself): the kit's bars, or play once stopped, inside it.
314
+ b.seconds && count > 1
315
+ ? draw({ ...k.node, children: [], text: undefined }, k.ctx, depth + 1, { attrs: { "data-kit-pause": "", "aria-pressed": "false", "aria-label": k.node.label ?? "Pause the slides" }, inner: _jsx("span", { "data-kit-pause-icon": "", "aria-hidden": "true" }) }, undefined, `${k.key}-${i}`)
316
+ : null
317
+ : draw(k.node, k.ctx, depth + 1, { attrs: { [`data-kit-${k.node.slot}`]: "", "aria-label": k.node.label ?? (k.node.slot === "prev" ? "Previous slide" : "Next slide") } }, undefined, `${k.key}-${i}`)), (kitDots || kitArrows || (b.seconds && count > 1 && !has("pause"))) && (_jsxs("div", { className: "kit-carousel-controls", children: [b.seconds && !has("pause") && (_jsx("button", { type: "button", className: "kit-carousel-pause", "data-kit-pause": "", "aria-pressed": "false", "aria-label": "Pause the slides", children: _jsx("span", { "aria-hidden": "true" }) })), kitArrows && arrow("prev"), kitDots && dots("kit-carousel-dot"), kitArrows && arrow("next")] }))] }));
318
+ }
@@ -0,0 +1,4 @@
1
+ import type { Site } from "../content/index.ts";
2
+ export declare function KitConsent({ site }: {
3
+ site: Pick<Site, "consent">;
4
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { asksConsent } from "../consent.js";
3
+ import { ComponentTree } from "./built.js";
4
+ import { isDraft } from "./data.js";
5
+ import { Md } from "./parts/Md.js";
6
+ export function KitConsent({ site }) {
7
+ if (!asksConsent(site))
8
+ return null;
9
+ const c = site.consent;
10
+ const place = c.place ?? "bottom";
11
+ const words = { text: c.text, accept: c.accept ?? "Accept", decline: c.decline ?? "Decline" };
12
+ return (_jsx("div", { className: `kit-consent kit-consent-${place}${c.design ? " kit-consent-own" : ""}`, "data-kit-consent-banner": "", role: "region", "aria-label": "Cookie choices", hidden: !isDraft(), children: c.design ? (_jsx(ComponentTree, { name: c.design, slots: { text: { text: words.text }, accept: { text: words.accept }, decline: { text: words.decline }, ...(c.policy ? { policy: { text: c.policy.label, href: c.policy.href } } : {}) }, marks: { accept: { "data-kit-consent": "accept" }, decline: { "data-kit-consent": "decline" } } })) : (_jsxs("div", { className: "kit-consent-box", children: [_jsx(Md, { text: words.text, className: "kit-consent-text" }), _jsxs("div", { className: "kit-consent-actions", children: [c.policy && (_jsx("a", { className: "kit-consent-policy", href: c.policy.href, children: c.policy.label })), _jsx("button", { type: "button", className: "kit-button kit-button-quiet", "data-kit-consent": "decline", children: words.decline }), _jsx("button", { type: "button", className: "kit-button", "data-kit-consent": "accept", children: words.accept })] })] })) }));
13
+ }
@@ -2,10 +2,14 @@ import type { Page } from "../content/index.ts";
2
2
  import type { Prepared, PreparedImage } from "../prepare.ts";
3
3
  /** Where the server reads the prepared content from (./data-fs.ts). */
4
4
  export declare function loadPreparedWith(load: () => Prepared): void;
5
- /** Puts content in the store: CalOS's preview, with the draft it's editing. */
6
- export declare function setPrepared(p: Prepared): void;
5
+ /** Puts content in the store: CalOS's preview, with the draft it's editing (`draft`: false for content as published, as a test draws it). */
6
+ export declare function setPrepared(p: Prepared, draft?: boolean): void;
7
+ /** Whether what's drawn is a draft in CalOS's preview (a part with nothing in it yet shows where it goes). */
8
+ export declare const isDraft: () => boolean;
7
9
  export declare function prepared(): Prepared;
8
10
  export declare const site: () => import("../content/index.ts").Site;
11
+ /** The Look's values as CSS variables (lookVars), at build and in the preview; empty for a site without a Look. */
12
+ export declare const lookValues: () => Record<string, string>;
9
13
  export declare function image(src: string): PreparedImage;
10
14
  /** The page at a catch-all route's segments (`[[...path]]`), or null. */
11
15
  export declare function pageAt(path: string[] | undefined): Page | null;
package/dist/next/data.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // `.site/prepared.json` (./data-fs.ts, which the server entry loads) and in CalOS's preview by the
3
3
  // draft CalOS sends (./preview.tsx). Components read it the same way in both, so one set of
4
4
  // components draws the published site and the draft being edited.
5
+ import { allPages } from "../content/lists.js";
5
6
  let current = null;
6
7
  let loader = null;
7
8
  /** Where the server reads the prepared content from (./data-fs.ts). */
@@ -10,11 +11,13 @@ export function loadPreparedWith(load) {
10
11
  }
11
12
  /** Whether the store holds a draft from CalOS's preview, rather than a build's content. */
12
13
  let drafting = false;
13
- /** Puts content in the store: CalOS's preview, with the draft it's editing. */
14
- export function setPrepared(p) {
14
+ /** Puts content in the store: CalOS's preview, with the draft it's editing (`draft`: false for content as published, as a test draws it). */
15
+ export function setPrepared(p, draft = true) {
15
16
  current = p;
16
- drafting = true;
17
+ drafting = draft;
17
18
  }
19
+ /** Whether what's drawn is a draft in CalOS's preview (a part with nothing in it yet shows where it goes). */
20
+ export const isDraft = () => drafting;
18
21
  /** A picture not chosen yet, in a draft: a grey box where it will go. */
19
22
  const NOT_CHOSEN = {
20
23
  width: 1600,
@@ -30,6 +33,8 @@ export function prepared() {
30
33
  return current;
31
34
  }
32
35
  export const site = () => prepared().site;
36
+ /** The Look's values as CSS variables (lookVars), at build and in the preview; empty for a site without a Look. */
37
+ export const lookValues = () => prepared().look ?? {};
33
38
  export function image(src) {
34
39
  const found = prepared().images[src];
35
40
  // A section being filled in, in the preview, is drawn with its picture still to choose; a build
@@ -43,5 +48,6 @@ export function image(src) {
43
48
  /** The page at a catch-all route's segments (`[[...path]]`), or null. */
44
49
  export function pageAt(path) {
45
50
  const at = path?.length ? `/${path.join("/")}${site().trailingSlash === false ? "" : "/"}` : "/";
46
- return site().pages.find((p) => p.path === at) ?? null;
51
+ // A page of the site, else an item of one of its lists at its page's address.
52
+ return allPages(site()).find((p) => p.path === at) ?? null;
47
53
  }
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from "react";
2
+ import { type Business, type LookOptions, type Site } from "../content/index.ts";
3
+ /** What the frame reads of the site: its business, pages, menu, footer and Look. */
4
+ export type FrameSite = Pick<Site, "pages" | "menu" | "footer"> & {
5
+ business: Business;
6
+ look?: unknown;
7
+ consent?: Site["consent"]; /** A header or footer built as a tree, in place of the kit's. */
8
+ ownHeader?: {
9
+ root?: unknown;
10
+ };
11
+ ownFooter?: {
12
+ root?: unknown;
13
+ };
14
+ };
15
+ /**
16
+ * The top of every page: the business's logo or name (home), the menu, and the one thing most
17
+ * visitors came to do (the menu's button, else a call button where there's a phone). On a phone the
18
+ * menu folds into a native disclosure, so it opens without any script, and calling stays one tap
19
+ * away. Sticky or not, its style and layout are the Look's Header (headerOf).
20
+ */
21
+ export declare function KitHeader({ site, current, look }: {
22
+ site: FrameSite;
23
+ current: string;
24
+ look: Pick<LookOptions, "header">;
25
+ }): import("react").JSX.Element;
26
+ /** The bottom of every page: how to reach them, when they're open, the pages, the policies, and where else they are online. */
27
+ export declare function KitFooter({ site }: {
28
+ site: FrameSite;
29
+ }): import("react").JSX.Element;
30
+ /**
31
+ * A whole page's frame: skip to the content, the header, the page in `main`, the footer. The kit's
32
+ * header and footer unless the site passes its own (`header`, `footer`: null leaves one out), or its
33
+ * content has one built (ownHeader, ownFooter: made in CalOS like a built section).
34
+ */
35
+ export declare function KitFrame({ site, current, look, header, footer, children }: {
36
+ site: FrameSite;
37
+ current: string;
38
+ look: Pick<LookOptions, "header">;
39
+ header?: ReactNode;
40
+ footer?: ReactNode;
41
+ children: ReactNode;
42
+ }): import("react").JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { buttonStyleClass, dayRange, footerOf, headerOf, menuOf, telHref } from "../content/index.js";
3
+ import { lookValues } from "./data.js";
4
+ /** The Look's values, where the site's data is loaded (not in a test drawing the frame alone). */
5
+ const lookOf = () => {
6
+ try {
7
+ return lookValues();
8
+ }
9
+ catch {
10
+ return {};
11
+ }
12
+ };
13
+ import { hoursLines } from "../seo.js";
14
+ import { KitMenuGroup } from "./menu-group.js";
15
+ import { Img } from "./parts/Img.js";
16
+ import { KitConsent } from "./consent.js";
17
+ import { BuiltFrame } from "./built.js";
18
+ const PATHS = {
19
+ phone: "M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.1 4.2 2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.8.7a2 2 0 0 1 1.7 2z",
20
+ mail: "M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zm18 2-10 7L2 6",
21
+ pin: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0zM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
22
+ menu: "M4 6h16M4 12h16M4 18h16",
23
+ close: "M18 6 6 18M6 6l12 12",
24
+ };
25
+ /** A line icon beside words that say the same, so hidden from a screen reader. */
26
+ function FrameIcon({ name, className }) {
27
+ return (_jsx("svg", { 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: _jsx("path", { d: PATHS[name] }) }));
28
+ }
29
+ /**
30
+ * The top of every page: the business's logo or name (home), the menu, and the one thing most
31
+ * visitors came to do (the menu's button, else a call button where there's a phone). On a phone the
32
+ * menu folds into a native disclosure, so it opens without any script, and calling stays one tap
33
+ * away. Sticky or not, its style and layout are the Look's Header (headerOf).
34
+ */
35
+ export function KitHeader({ site, current, look }) {
36
+ const b = site.business;
37
+ const h = headerOf(site.look, look);
38
+ const { links: menu, button: chosen } = menuOf(site);
39
+ const action = chosen ?? (b.phone ? { label: b.phone, href: telHref(b.phone, b.address?.country) } : null);
40
+ const isCurrent = (href) => href === current || (href !== "/" && current.startsWith(href));
41
+ const call = !!action?.href.startsWith("tel:");
42
+ const link = (m) => (_jsx("a", { href: m.href, "aria-current": isCurrent(m.href) ? "page" : undefined, className: m.emphasis ? "is-emphasis" : undefined, children: m.label }));
43
+ // The business's button as it chose it: its style (accent, quiet) and a border of its own.
44
+ const vars = lookOf();
45
+ const styled = [chosen?.style === "accent" ? "kit-button-accent" : chosen?.style === "quiet" ? "kit-button-quiet" : "", buttonStyleClass(chosen?.style, vars)].filter(Boolean).join(" ");
46
+ // The phone's menu button: in one of the business's button styles, where the header says.
47
+ const menuStyled = buttonStyleClass(h.menuStyle, vars);
48
+ const button = (className) => action && (_jsxs("a", { className: styled ? `${className} ${styled}` : className, href: action.href, children: [call && _jsx(FrameIcon, { name: "phone" }), _jsxs("span", { children: [call && !/call/i.test(action.label) && _jsx("span", { className: "kit-sr", children: "Call " }), action.label] })] }));
49
+ return (_jsx("header", { className: `kit-header kit-header-${h.style} kit-header-${h.layout}${h.sticky ? " is-sticky" : ""}`, children: _jsxs("div", { className: "kit-wrap kit-header-row", children: [_jsx("a", { className: "kit-brand", href: "/", "aria-label": current === "/" ? `${b.name}, home` : `${b.name}, back to home`, children: b.logo ? _jsx(Img, { src: b.logo.src, alt: "", sizes: "180px", className: "kit-logo", eager: true }) : _jsx("span", { className: "kit-brand-name", children: b.name }) }), menu.length > 0 && (_jsx("nav", { className: "kit-menu-wide", "aria-label": "Main", children: _jsx("ul", { children: menu.map((m) => m.children?.length ? (_jsx(KitMenuGroup, { label: m.label, href: m.href, emphasis: m.emphasis, current: current, children: m.children }, m.href + m.label)) : (_jsx("li", { children: link(m) }, m.href + m.label))) }) })), button(`kit-button kit-header-action${call ? " is-call" : ""}`), menu.length > 0 && (_jsxs("details", { className: `kit-menu-narrow kit-menu-${h.menuShows}`, children: [_jsxs("summary", { className: menuStyled, children: [_jsx(FrameIcon, { name: "menu", className: "when-closed" }), _jsx(FrameIcon, { name: "close", className: "when-open" }), _jsx("span", { className: "kit-menu-word", children: "Menu" })] }), _jsxs("nav", { "aria-label": "Main", children: [_jsx("ul", { children: menu.map((m) => (_jsxs("li", { children: [link(m), !!m.children?.length && (_jsx("ul", { className: "kit-menu-sub", children: m.children.map((c) => (_jsx("li", { children: link(c) }, c.href + c.label))) }))] }, m.href + m.label))) }), button("kit-button")] })] }))] }) }));
50
+ }
51
+ const SOCIAL = { google: "Google", facebook: "Facebook", instagram: "Instagram", yelp: "Yelp", nextdoor: "Nextdoor", linkedin: "LinkedIn", youtube: "YouTube", tiktok: "TikTok" };
52
+ /** The bottom of every page: how to reach them, when they're open, the pages, the policies, and where else they are online. */
53
+ export function KitFooter({ site }) {
54
+ const b = site.business;
55
+ // The links edited in CalOS (Footer), else the menu's under "Pages" and the policy pages in the legal row.
56
+ const isPolicy = (path) => site.pages.find((p) => p.path === path)?.blocks.some((s) => s.type === "policy");
57
+ const footer = footerOf(site);
58
+ const columns = site.footer ? footer.columns : footer.columns.map((c) => ({ heading: "Pages", links: c.links.filter((l) => !isPolicy(l.href)) }));
59
+ const legal = site.footer ? footer.legal : site.pages.filter((p) => isPolicy(p.path)).map((p) => ({ label: p.menu ?? p.title, href: p.path }));
60
+ const social = Object.entries(b.social ?? {}).filter(([, url]) => url);
61
+ const hours = b.hours ?? [];
62
+ return (_jsxs("footer", { className: "kit-footer", children: [_jsxs("div", { className: "kit-wrap kit-footer-grid", children: [_jsxs("div", { className: "kit-footer-about", children: [_jsx("p", { className: "kit-footer-name", children: b.name }), b.tagline && _jsx("p", { className: "kit-footer-muted", children: b.tagline }), _jsxs("ul", { className: "kit-footer-contact", children: [b.phone && (_jsxs("li", { children: [_jsx(FrameIcon, { name: "phone" }), _jsx("a", { href: telHref(b.phone, b.address?.country), children: b.phone })] })), b.email && (_jsxs("li", { children: [_jsx(FrameIcon, { name: "mail" }), _jsx("a", { href: `mailto:${b.email}`, children: b.email })] })), b.address && (_jsxs("li", { children: [_jsx(FrameIcon, { name: "pin" }), _jsx("span", { children: b.showAddress ? (_jsxs(_Fragment, { children: [b.address.street, _jsx("br", {}), b.address.locality, ", ", b.address.region, " ", b.address.postcode] })) : (_jsxs(_Fragment, { children: [b.address.locality, ", ", b.address.region] })) })] }))] })] }), hours.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "kit-footer-heading", children: "Hours" }), _jsx("dl", { className: "kit-footer-hours", children: hoursLines(b).map((h) => (_jsxs("div", { children: [_jsx("dt", { children: dayRange(h.days) }), _jsx("dd", { children: h.time })] }, h.days.join("")))) }), b.hoursNote && _jsx("p", { className: "kit-footer-muted", children: b.hoursNote })] })), columns
63
+ .filter((c) => c.links.length)
64
+ .map((c, i) => (_jsxs("nav", { "aria-label": c.heading ?? "Footer", children: [c.heading && _jsx("h2", { className: "kit-footer-heading", children: c.heading }), _jsx("ul", { className: "kit-footer-links", children: c.links.map((l) => (_jsx("li", { children: _jsx("a", { href: l.href, children: l.label }) }, l.href))) })] }, i))), social.length > 0 && (_jsxs("div", { children: [_jsx("h2", { className: "kit-footer-heading", children: "Follow" }), _jsx("ul", { className: "kit-footer-links", children: social.map(([key, url]) => (_jsx("li", { children: _jsx("a", { href: url, rel: "noopener me", children: SOCIAL[key] ?? key }) }, key))) })] }))] }), _jsxs("div", { className: "kit-wrap kit-footer-base", children: [_jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " ", b.name, b.licence && _jsxs(_Fragment, { children: [" \u00B7 ", b.licence] }), footer.note && _jsxs(_Fragment, { children: [" \u00B7 ", footer.note] })] }), legal.length > 0 && (_jsx("ul", { className: "kit-footer-policies", children: legal.map((l) => (_jsx("li", { children: _jsx("a", { href: l.href, children: l.label }) }, l.href))) }))] })] }));
65
+ }
66
+ /**
67
+ * A whole page's frame: skip to the content, the header, the page in `main`, the footer. The kit's
68
+ * header and footer unless the site passes its own (`header`, `footer`: null leaves one out), or its
69
+ * content has one built (ownHeader, ownFooter: made in CalOS like a built section).
70
+ */
71
+ export function KitFrame({ site, current, look, header, footer, children }) {
72
+ return (_jsxs(_Fragment, { children: [_jsx("a", { className: "kit-skip", href: "#main", children: "Skip to content" }), header !== undefined ? header : site.ownHeader?.root ? _jsx(BuiltFrame, { tree: site.ownHeader, current: current, which: "header" }) : _jsx(KitHeader, { site: site, current: current, look: look }), _jsx("main", { id: "main", tabIndex: -1, children: children }), footer !== undefined ? footer : site.ownFooter?.root ? _jsx(BuiltFrame, { tree: site.ownFooter, current: current, which: "footer" }) : _jsx(KitFooter, { site: site }), _jsx(KitConsent, { site: site })] }));
73
+ }
@@ -1,8 +1,14 @@
1
- export { image, pageAt, prepared, site } from "./data.ts";
1
+ export { image, lookValues, pageAt, prepared, site } from "./data.ts";
2
+ export { KitBlocks } from "./blocks.tsx";
3
+ export { BlockMark } from "./block-mark.tsx";
2
4
  export { JsonLd } from "./JsonLd.tsx";
3
5
  export { pageMetadata, robots, sitemap, staticParams } from "./metadata.ts";
4
6
  export { Img, type Ratio } from "./parts/Img.tsx";
5
7
  export { LookStyle } from "./parts/LookStyle.tsx";
6
8
  export { Md } from "./parts/Md.tsx";
7
- export { KitSection, type KitBlock, type KitClasses } from "./sections.tsx";
9
+ export { KIT_CLASSES, KitSection, type KitBlock, type KitClasses } from "./sections.tsx";
8
10
  export { Slideshow } from "./slideshow.tsx";
11
+ export { KitFooter, KitFrame, KitHeader, type FrameSite } from "./frame.tsx";
12
+ export { KitMenuGroup } from "./menu-group.tsx";
13
+ export { KitConsent } from "./consent.tsx";
14
+ export { BuiltSection, ComponentTree, UseSection } from "./built.tsx";
@@ -1,11 +1,17 @@
1
1
  // The plumbing a Next.js site needs from the kit, whatever its design: the prepared content, a page by
2
2
  // its path, metadata, picture and Markdown parts, structured data. A site's own components, in its own
3
- // repository, do the looks. Server components only: nothing here runs in the visitor's browser.
4
- export { image, pageAt, prepared, site } from "./data.js";
3
+ // repository, do the looks. Server components, but for BlockMark and Slideshow, small client parts.
4
+ export { image, lookValues, pageAt, prepared, site } from "./data.js";
5
+ export { KitBlocks } from "./blocks.js";
6
+ export { BlockMark } from "./block-mark.js";
5
7
  export { JsonLd } from "./JsonLd.js";
6
8
  export { pageMetadata, robots, sitemap, staticParams } from "./metadata.js";
7
9
  export { Img } from "./parts/Img.js";
8
10
  export { LookStyle } from "./parts/LookStyle.js";
9
11
  export { Md } from "./parts/Md.js";
10
- export { KitSection } from "./sections.js";
12
+ export { KIT_CLASSES, KitSection } from "./sections.js";
11
13
  export { Slideshow } from "./slideshow.js";
14
+ export { KitFooter, KitFrame, KitHeader } from "./frame.js";
15
+ export { KitMenuGroup } from "./menu-group.js";
16
+ export { KitConsent } from "./consent.js";
17
+ export { BuiltSection, ComponentTree, UseSection } from "./built.js";
@@ -0,0 +1,11 @@
1
+ export type MenuChild = {
2
+ label: string;
3
+ href: string;
4
+ };
5
+ export declare function KitMenuGroup({ label, href, children, current, emphasis }: {
6
+ label: string;
7
+ href: string;
8
+ children: MenuChild[];
9
+ current: string;
10
+ emphasis?: boolean;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // A menu link with links under it, on a wide screen: the link itself, and beside it a button that
4
+ // opens the links under it (aria-expanded). Opened by a click, Enter or Space; closed by Escape (back
5
+ // to its button), a click elsewhere, or moving focus out of it. Never hover only: what a mouse can
6
+ // open, a keyboard and a finger can too.
7
+ import { useEffect, useId, useRef, useState } from "react";
8
+ export function KitMenuGroup({ label, href, children, current, emphasis }) {
9
+ const [open, setOpen] = useState(false);
10
+ const id = useId();
11
+ const box = useRef(null);
12
+ const button = useRef(null);
13
+ const isCurrent = (h) => h === current || (h !== "/" && current.startsWith(h));
14
+ useEffect(() => {
15
+ if (!open)
16
+ return;
17
+ const away = (e) => {
18
+ if (!box.current?.contains(e.target))
19
+ setOpen(false);
20
+ };
21
+ document.addEventListener("pointerdown", away);
22
+ return () => document.removeEventListener("pointerdown", away);
23
+ }, [open]);
24
+ return (_jsxs("li", { ref: box, className: open ? "kit-menu-group is-open" : "kit-menu-group", onKeyDown: (e) => {
25
+ if (e.key === "Escape" && open) {
26
+ setOpen(false);
27
+ button.current?.focus();
28
+ }
29
+ }, onBlur: (e) => {
30
+ if (!box.current?.contains(e.relatedTarget))
31
+ setOpen(false);
32
+ }, children: [_jsx("a", { href: href, "aria-current": isCurrent(href) ? "page" : undefined, className: emphasis ? "is-emphasis" : undefined, children: label }), _jsxs("button", { ref: button, type: "button", className: "kit-menu-group-toggle", "aria-expanded": open, "aria-controls": id, onClick: () => setOpen((o) => !o), children: [_jsx("svg", { className: "kit-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m6 9 6 6 6-6" }) }), _jsxs("span", { className: "kit-sr", children: ["More under ", label] })] }), _jsx("ul", { id: id, className: "kit-menu-group-list", hidden: !open, children: children.map((c) => (_jsx("li", { children: _jsx("a", { href: c.href, "aria-current": isCurrent(c.href) ? "page" : undefined, children: c.label }) }, c.href + c.label))) })] }));
33
+ }