@caltho/site-kit 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -3
- package/dist/behaviors/accordion.d.ts +4 -0
- package/dist/behaviors/accordion.js +56 -0
- package/dist/behaviors/carousel.d.ts +4 -0
- package/dist/behaviors/carousel.js +127 -0
- package/dist/behaviors/filter.d.ts +1 -0
- package/dist/behaviors/filter.js +25 -0
- package/dist/behaviors/index.d.ts +14 -0
- package/dist/behaviors/index.js +78 -0
- package/dist/behaviors/motion.d.ts +13 -0
- package/dist/behaviors/motion.js +88 -0
- package/dist/behaviors/tabs.d.ts +3 -0
- package/dist/behaviors/tabs.js +55 -0
- package/dist/built/css.d.ts +2 -0
- package/dist/built/css.js +26 -0
- package/dist/built/theme.d.ts +19 -0
- package/dist/built/theme.js +86 -0
- package/dist/built/tree.d.ts +24 -0
- package/dist/built/tree.js +96 -0
- package/dist/built.test.d.ts +1 -0
- package/dist/built.test.js +257 -0
- package/dist/consent.d.ts +33 -0
- package/dist/consent.js +36 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +61 -0
- package/dist/content/index.d.ts +170 -40
- package/dist/content/index.js +155 -0
- package/dist/content/lists.d.ts +69 -0
- package/dist/content/lists.js +77 -0
- package/dist/content/lists.test.d.ts +1 -0
- package/dist/content/lists.test.js +257 -0
- package/dist/content/look.d.ts +13 -13
- package/dist/content-migrate.d.ts +51 -0
- package/dist/content-migrate.js +69 -0
- package/dist/finish.d.ts +1 -1
- package/dist/finish.js +18 -5
- package/dist/frame.test.js +25 -0
- package/dist/next/built.d.ts +40 -0
- package/dist/next/built.js +318 -0
- package/dist/next/consent.d.ts +4 -0
- package/dist/next/consent.js +13 -0
- package/dist/next/data.d.ts +2 -2
- package/dist/next/data.js +6 -4
- package/dist/next/frame.d.ts +9 -1
- package/dist/next/frame.js +5 -2
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +2 -0
- package/dist/next/metadata.js +3 -2
- package/dist/next/parts/Img.d.ts +6 -1
- package/dist/next/parts/Img.js +11 -3
- package/dist/next/preview.js +242 -15
- package/dist/next/sections.js +5 -0
- package/dist/policies.js +22 -4
- package/dist/prepare.d.ts +10 -0
- package/dist/prepare.js +67 -3
- package/dist/preview-match.d.ts +21 -0
- package/dist/preview-match.js +25 -0
- package/dist/schema.js +3 -1
- package/dist/sections/icons.d.ts +2 -0
- package/dist/sections/icons.js +4 -0
- package/dist/sections/index.d.ts +38 -16
- package/dist/sections/index.js +7 -2
- package/dist/sections.css +324 -0
- package/dist/seo.js +2 -1
- package/dist/tree/behaviors.d.ts +50 -0
- package/dist/tree/behaviors.js +67 -0
- package/dist/tree/classes.d.ts +24 -0
- package/dist/tree/classes.js +94 -0
- package/dist/tree/index.d.ts +145 -0
- package/dist/tree/index.js +196 -0
- package/dist/tree/messages.d.ts +45 -0
- package/dist/tree/messages.js +7 -0
- package/package.json +15 -1
package/dist/finish.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// as inline scripts; a static page can't carry a per-request nonce), and Cloudflare Pages' `_headers`
|
|
4
4
|
// and `_redirects` beside the pages. Where CalOS counts its visits, every published page (not the
|
|
5
5
|
// preview) gets the script that sends them, and its phone and map links are marked, so a tap on one
|
|
6
|
-
// is counted too
|
|
6
|
+
// is counted too; where the site asks first (its consent setting), its banner's script, and nothing
|
|
7
|
+
// counted until a visitor accepts.
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
9
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
9
10
|
import { join, relative } from "node:path";
|
|
10
11
|
import { BUILT_IMAGES } from "./preview-match.js";
|
|
12
|
+
import { asksConsent, CONSENT_KEY, consentScript } from "./consent.js";
|
|
11
13
|
import { VIDEO_ORIGINS } from "./sections/index.js";
|
|
12
14
|
import { headersFile, loadRedirects, PREVIEW_PATH } from "./seo.js";
|
|
13
15
|
export async function finish(root, outDir, env = process.env, log = console.log) {
|
|
@@ -23,8 +25,12 @@ export async function finish(root, outDir, env = process.env, log = console.log)
|
|
|
23
25
|
// build made, it shows as built (./next/preview.tsx), from the list it carries.
|
|
24
26
|
const isPreview = relative(outDir, file).replace(/(\/index)?\.html$/, "") === PREVIEW_PATH.slice(1);
|
|
25
27
|
// Before the hashing: its own little script is named in the policy like any other.
|
|
28
|
+
// A site that asks first: its banner's script, and its visits counted only once accepted.
|
|
29
|
+
const asks = asksConsent(site);
|
|
30
|
+
if (asks && !isPreview)
|
|
31
|
+
html = html.replace(/<\/body>/i, () => `<script>${consentScript()}</script></body>`);
|
|
26
32
|
if (prepared.analytics && !isPreview)
|
|
27
|
-
html = counted(html, prepared.analytics);
|
|
33
|
+
html = counted(html, prepared.analytics, asks);
|
|
28
34
|
const scripts = new Set();
|
|
29
35
|
const styles = new Set();
|
|
30
36
|
for (const m of html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)) {
|
|
@@ -41,7 +47,11 @@ export async function finish(root, outDir, env = process.env, log = console.log)
|
|
|
41
47
|
const directives = {
|
|
42
48
|
"default-src": ["'self'"],
|
|
43
49
|
"script-src": ["'self'", ...[...scripts].map((h) => `'${h}'`), ...(prepared.analytics && !isPreview ? [new URL(prepared.analytics.script).origin] : [])],
|
|
44
|
-
|
|
50
|
+
// The preview draws built sections' classes as they're typed with Tailwind's browser build, which
|
|
51
|
+
// writes them into a <style> of its own: inline styles are allowed there (with no hashes, which
|
|
52
|
+
// would turn that off), and only there. It's framed only by the business's CalOS addresses, and
|
|
53
|
+
// draws only classes the kit's checker allows.
|
|
54
|
+
"style-src": isPreview ? ["'self'", "'unsafe-inline'"] : ["'self'", ...[...styles].map((h) => `'${h}'`)],
|
|
45
55
|
"img-src": ["'self'", "data:"],
|
|
46
56
|
// The preview may show a Google font just chosen, from Fontsource's CDN, until the next build serves it.
|
|
47
57
|
// So may a font the business uploaded, from CalOS (the editor's origin).
|
|
@@ -82,9 +92,12 @@ export async function finish(root, outDir, env = process.env, log = console.log)
|
|
|
82
92
|
* out, and what's marked to count), and each phone link and map link marked: a call or directions
|
|
83
93
|
* tapped is what a business wants to know came from the site. No cookies: nothing to consent to.
|
|
84
94
|
*/
|
|
85
|
-
export function counted(html, a) {
|
|
95
|
+
export function counted(html, a, asks = false) {
|
|
86
96
|
const init = `window.op=window.op||function(){(window.op.q=window.op.q||[]).push(arguments)};window.op("init",${JSON.stringify({ apiUrl: a.api, clientId: a.client, trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true })});`;
|
|
87
|
-
|
|
97
|
+
// Asking first (the site's consent setting): nothing starts, and the counter isn't even loaded,
|
|
98
|
+
// until the visitor has accepted, now or on an earlier visit.
|
|
99
|
+
const gated = `(()=>{let on=false;const go=()=>{if(on)return;on=true;${init}const s=document.createElement("script");s.src=${JSON.stringify(a.script)};s.async=true;document.head.appendChild(s)};try{if(localStorage.getItem(${JSON.stringify(CONSENT_KEY)})==="yes")go()}catch{}addEventListener("kit:consent",(e)=>{if(e.detail==="yes")go()})})();`;
|
|
100
|
+
const tags = asks ? `<script>${gated}</script>` : `<script>${init}</script><script src="${a.script}" defer async></script>`;
|
|
88
101
|
html = html.replace(/<\/head>/i, () => `${tags}</head>`);
|
|
89
102
|
return html.replace(/<a\b([^>]*)>/gi, (tag, attrs) => {
|
|
90
103
|
if (/\sdata-track\s*=/i.test(attrs))
|
package/dist/frame.test.js
CHANGED
|
@@ -30,4 +30,29 @@ describe("the kit's frame", () => {
|
|
|
30
30
|
expect(own).toMatch(/<header class="mine">/);
|
|
31
31
|
expect(own).not.toMatch(/kit-header|kit-footer/);
|
|
32
32
|
});
|
|
33
|
+
it("draws a header and footer the business built (ownHeader, ownFooter) in place of the kit's, its links to the page it's on marked", () => {
|
|
34
|
+
const header = {
|
|
35
|
+
root: {
|
|
36
|
+
id: "hdrroot001",
|
|
37
|
+
tag: "header",
|
|
38
|
+
class: "flex items-center justify-between",
|
|
39
|
+
children: [
|
|
40
|
+
{ id: "hdrlogo001", tag: "a", href: "/", text: "Acme" },
|
|
41
|
+
{ id: "hdrnav0001", tag: "nav", children: [
|
|
42
|
+
{ id: "hdrlink001", tag: "a", href: "/services/", text: "Services", class: "aria-current:underline" },
|
|
43
|
+
{ id: "hdrlink002", tag: "a", href: "/about/", text: "About" },
|
|
44
|
+
] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const footer = { root: { id: "ftrroot001", tag: "footer", class: "py-10", children: [{ id: "ftrtext001", tag: "p", text: "© Acme" }] } };
|
|
49
|
+
const html = renderToStaticMarkup(_jsx(KitFrame, { site: { ...site, ownHeader: header, ownFooter: footer }, current: "/services/walnut/", look: {}, children: "x" }));
|
|
50
|
+
expect(html).not.toMatch(/kit-header|kit-footer/);
|
|
51
|
+
expect(html).toMatch(/<header class="kit:flex kit:items-center kit:justify-between"[^>]*data-kit-built/);
|
|
52
|
+
// The page under Services marks Services; home and About aren't.
|
|
53
|
+
expect(html).toMatch(/href="\/services\/" aria-current="page"/);
|
|
54
|
+
expect(html).not.toMatch(/href="\/" aria-current/);
|
|
55
|
+
expect(html).not.toMatch(/href="\/about\/" aria-current/);
|
|
56
|
+
expect(html).toMatch(/<footer class="kit:py-10"[^>]*>.*© Acme/);
|
|
57
|
+
});
|
|
33
58
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ImageRef } from "../tree/index.ts";
|
|
2
|
+
import type { KitBlock } from "./sections.tsx";
|
|
3
|
+
/** What a slot of a component is filled with where it's used. */
|
|
4
|
+
type Fill = {
|
|
5
|
+
text?: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
image?: ImageRef;
|
|
8
|
+
};
|
|
9
|
+
/** A built section: its tree, the block's id and Layout on its root. */
|
|
10
|
+
export declare function BuiltSection({ s }: {
|
|
11
|
+
s: KitBlock;
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
13
|
+
/** A component used by name, its slots filled; nothing (a quiet box in the preview) when the site has no such component. */
|
|
14
|
+
export declare function UseSection({ s }: {
|
|
15
|
+
s: KitBlock;
|
|
16
|
+
}): import("react").JSX.Element | null;
|
|
17
|
+
/**
|
|
18
|
+
* One of the site's components drawn outside a page's blocks (a cookie banner's design): its slots
|
|
19
|
+
* filled, the kit's attributes on the elements of the slots named in `marks`. Nothing for a
|
|
20
|
+
* component the site hasn't.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ComponentTree({ name, slots, marks }: {
|
|
23
|
+
name: string;
|
|
24
|
+
slots: Record<string, Fill>;
|
|
25
|
+
marks?: Record<string, Record<string, string>>;
|
|
26
|
+
}): import("react").JSX.Element | null;
|
|
27
|
+
/**
|
|
28
|
+
* The site's own header or footer (content's ownHeader, ownFooter): its tree drawn as a built
|
|
29
|
+
* section's, its links to the page it's on marked (aria-current).
|
|
30
|
+
*/
|
|
31
|
+
export declare function BuiltFrame({ tree, current, which }: {
|
|
32
|
+
tree: {
|
|
33
|
+
root?: unknown;
|
|
34
|
+
};
|
|
35
|
+
current: string;
|
|
36
|
+
which?: "header" | "footer";
|
|
37
|
+
}): import("react").JSX.Element | null;
|
|
38
|
+
/** 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. */
|
|
39
|
+
export declare function linkFor(v: string): string;
|
|
40
|
+
export {};
|
|
@@ -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,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
|
+
}
|
package/dist/next/data.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ 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
7
|
/** Whether what's drawn is a draft in CalOS's preview (a part with nothing in it yet shows where it goes). */
|
|
8
8
|
export declare const isDraft: () => boolean;
|
|
9
9
|
export declare function prepared(): Prepared;
|
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,10 +11,10 @@ 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 =
|
|
17
|
+
drafting = draft;
|
|
17
18
|
}
|
|
18
19
|
/** Whether what's drawn is a draft in CalOS's preview (a part with nothing in it yet shows where it goes). */
|
|
19
20
|
export const isDraft = () => drafting;
|
|
@@ -47,5 +48,6 @@ export function image(src) {
|
|
|
47
48
|
/** The page at a catch-all route's segments (`[[...path]]`), or null. */
|
|
48
49
|
export function pageAt(path) {
|
|
49
50
|
const at = path?.length ? `/${path.join("/")}${site().trailingSlash === false ? "" : "/"}` : "/";
|
|
50
|
-
|
|
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;
|
|
51
53
|
}
|
package/dist/next/frame.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { type Business, type LookOptions, type Site } from "../content/index.ts"
|
|
|
4
4
|
export type FrameSite = Pick<Site, "pages" | "menu" | "footer"> & {
|
|
5
5
|
business: Business;
|
|
6
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
|
+
};
|
|
7
14
|
};
|
|
8
15
|
/**
|
|
9
16
|
* The top of every page: the business's logo or name (home), the menu, and the one thing most
|
|
@@ -22,7 +29,8 @@ export declare function KitFooter({ site }: {
|
|
|
22
29
|
}): import("react").JSX.Element;
|
|
23
30
|
/**
|
|
24
31
|
* A whole page's frame: skip to the content, the header, the page in `main`, the footer. The kit's
|
|
25
|
-
* header and footer unless the site passes its own (`header`, `footer`: null leaves one out)
|
|
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).
|
|
26
34
|
*/
|
|
27
35
|
export declare function KitFrame({ site, current, look, header, footer, children }: {
|
|
28
36
|
site: FrameSite;
|
package/dist/next/frame.js
CHANGED
|
@@ -13,6 +13,8 @@ const lookOf = () => {
|
|
|
13
13
|
import { hoursLines } from "../seo.js";
|
|
14
14
|
import { KitMenuGroup } from "./menu-group.js";
|
|
15
15
|
import { Img } from "./parts/Img.js";
|
|
16
|
+
import { KitConsent } from "./consent.js";
|
|
17
|
+
import { BuiltFrame } from "./built.js";
|
|
16
18
|
const PATHS = {
|
|
17
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",
|
|
18
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",
|
|
@@ -63,8 +65,9 @@ export function KitFooter({ site }) {
|
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* A whole page's frame: skip to the content, the header, the page in `main`, the footer. The kit's
|
|
66
|
-
* header and footer unless the site passes its own (`header`, `footer`: null leaves one out)
|
|
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).
|
|
67
70
|
*/
|
|
68
71
|
export function KitFrame({ site, current, look, header, footer, children }) {
|
|
69
|
-
return (_jsxs(_Fragment, { children: [_jsx("a", { className: "kit-skip", href: "#main", children: "Skip to content" }), header
|
|
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 })] }));
|
|
70
73
|
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export { KIT_CLASSES, KitSection, type KitBlock, type KitClasses } from "./secti
|
|
|
10
10
|
export { Slideshow } from "./slideshow.tsx";
|
|
11
11
|
export { KitFooter, KitFrame, KitHeader, type FrameSite } from "./frame.tsx";
|
|
12
12
|
export { KitMenuGroup } from "./menu-group.tsx";
|
|
13
|
+
export { KitConsent } from "./consent.tsx";
|
|
14
|
+
export { BuiltSection, ComponentTree, UseSection } from "./built.tsx";
|
package/dist/next/index.js
CHANGED
|
@@ -13,3 +13,5 @@ export { KIT_CLASSES, KitSection } from "./sections.js";
|
|
|
13
13
|
export { Slideshow } from "./slideshow.js";
|
|
14
14
|
export { KitFooter, KitFrame, KitHeader } from "./frame.js";
|
|
15
15
|
export { KitMenuGroup } from "./menu-group.js";
|
|
16
|
+
export { KitConsent } from "./consent.js";
|
|
17
|
+
export { BuiltSection, ComponentTree, UseSection } from "./built.js";
|
package/dist/next/metadata.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The Next.js side of search: each page's metadata, the static routes, robots.txt and the sitemap,
|
|
2
2
|
// all from the prepared content.
|
|
3
|
+
import { allPages } from "../content/lists.js";
|
|
3
4
|
import { shareImageOf } from "../images.js";
|
|
4
5
|
import { canonical, fullTitle } from "../seo.js";
|
|
5
6
|
import { image, prepared, site } from "./data.js";
|
|
@@ -34,12 +35,12 @@ export function pageMetadata(page) {
|
|
|
34
35
|
}
|
|
35
36
|
/** For `generateStaticParams` on `app/[[...path]]/page.tsx`. */
|
|
36
37
|
export function staticParams() {
|
|
37
|
-
return site().
|
|
38
|
+
return allPages(site()).map((p) => ({ path: p.path.split("/").filter(Boolean) }));
|
|
38
39
|
}
|
|
39
40
|
export function robots() {
|
|
40
41
|
return { rules: { userAgent: "*", allow: "/" }, sitemap: canonical(site(), "/sitemap.xml") };
|
|
41
42
|
}
|
|
42
43
|
export function sitemap() {
|
|
43
44
|
const s = site();
|
|
44
|
-
return s.
|
|
45
|
+
return allPages(s).filter((p) => !p.noindex).map((p) => ({ url: canonical(s, p.path) }));
|
|
45
46
|
}
|
package/dist/next/parts/Img.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export type Ratio = "1/1" | "3/2" | "4/3" | "4/5" | "16/9";
|
|
2
|
-
|
|
2
|
+
/** `node`: a built section's node id, marked on the picture in CalOS's preview (data-node) so a click on it names the node. */
|
|
3
|
+
/** Below this width a picture's phone version (`phoneSrc`) is shown in its place. */
|
|
4
|
+
export declare const PHONE_WIDTH = 700;
|
|
5
|
+
export declare function Img({ src, alt, sizes, ratio, eager, className, node, phoneSrc }: {
|
|
3
6
|
src: string;
|
|
4
7
|
alt: string;
|
|
5
8
|
sizes: string;
|
|
6
9
|
ratio?: Ratio;
|
|
7
10
|
eager?: boolean;
|
|
8
11
|
className?: string;
|
|
12
|
+
node?: string; /** Another picture for phones, shown on small screens. */
|
|
13
|
+
phoneSrc?: string;
|
|
9
14
|
}): import("react").JSX.Element;
|