@caltho/site-kit 0.2.9 → 0.2.11
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 +63 -8
- package/dist/cli/check.js +31 -1
- package/dist/content/index.d.ts +318 -23
- package/dist/content/index.js +148 -15
- package/dist/content/look.d.ts +260 -9
- package/dist/content/look.js +386 -22
- package/dist/finish.js +9 -1
- package/dist/fonts/families/dm-sans/LICENSE +93 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/dm-sans/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/LICENSE +93 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/fraunces/fraunces-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/LICENSE +93 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/inter/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/LICENSE +93 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/lora/lora-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/LICENSE +93 -0
- package/dist/fonts/families/manrope/manrope-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/manrope/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/LICENSE +93 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/newsreader/newsreader-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/LICENSE +93 -0
- package/dist/fonts/families/outfit/outfit-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/outfit/outfit-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/LICENSE +93 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/playfair-display/playfair-display-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/LICENSE +93 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/LICENSE +93 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/families/work-sans/work-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/google.json +1 -0
- package/dist/fonts-google.d.ts +35 -0
- package/dist/fonts-google.js +70 -0
- package/dist/fonts.css +365 -0
- package/dist/frame.test.d.ts +1 -0
- package/dist/frame.test.js +33 -0
- package/dist/migrate.d.ts +45 -0
- package/dist/migrate.js +284 -0
- package/dist/next/block-mark.d.ts +7 -0
- package/dist/next/block-mark.js +16 -0
- package/dist/next/blocks.d.ts +11 -0
- package/dist/next/blocks.js +35 -0
- package/dist/next/data.d.ts +4 -0
- package/dist/next/data.js +4 -0
- package/dist/next/frame.d.ts +34 -0
- package/dist/next/frame.js +70 -0
- package/dist/next/index.d.ts +6 -2
- package/dist/next/index.js +7 -3
- package/dist/next/menu-group.d.ts +11 -0
- package/dist/next/menu-group.js +33 -0
- package/dist/next/parts/LookStyle.js +3 -1
- package/dist/next/preview.js +282 -25
- package/dist/next/sections.d.ts +21 -6
- package/dist/next/sections.js +167 -6
- package/dist/prepare.d.ts +5 -1
- package/dist/prepare.js +59 -1
- package/dist/preview-match.d.ts +8 -0
- package/dist/preview-match.js +33 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +26 -0
- package/dist/sections/icons.d.ts +47 -0
- package/dist/sections/icons.js +51 -0
- package/dist/sections/index.d.ts +284 -2
- package/dist/sections/index.js +156 -2
- package/dist/sections/presets.d.ts +13 -0
- package/dist/sections/presets.js +300 -0
- package/dist/sections.css +1393 -9
- package/dist/sections.test.js +310 -8
- package/package.json +8 -3
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Moving a site's own code to what a new kit expects, as text: CalOS reads a site's files from its
|
|
2
|
+
// repository, runs the migrations it hasn't had, and commits what they change on the kit update's
|
|
3
|
+
// branch, never main, so Cloudflare builds it as a preview first and it goes live only once that built.
|
|
4
|
+
// Nothing here runs the site's code, and a migration changes a file only where it's exactly the
|
|
5
|
+
// starter's (a known version, or a known block within it): a file the site has made its own is
|
|
6
|
+
// left as it is and the migration reported for a person, who moves it by hand. What a site has had
|
|
7
|
+
// is kept in its repository (.caltho/migrations.json), so each runs once.
|
|
8
|
+
export const MIGRATIONS_FILE = ".caltho/migrations.json";
|
|
9
|
+
async function sha256(text) {
|
|
10
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
|
|
11
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12
|
+
}
|
|
13
|
+
// The starter's own header, footer and menu group, every version it has shipped.
|
|
14
|
+
const STARTER_FRAME_FILES = {
|
|
15
|
+
"components/Header.tsx": [
|
|
16
|
+
"0bf8abc156b59a5e6302921a7b0a16d4d391f519ee595de61e039aeb4b92ac92",
|
|
17
|
+
"4b101f874022fe290f659dd519849cc89b79c68791e8bc92386ae86718902bf5",
|
|
18
|
+
"e4ff7a7f332919c3611bf55f1fe7a29877b24445d164ab99f2875121af3895b9",
|
|
19
|
+
],
|
|
20
|
+
"components/Footer.tsx": ["7971cfd0c2bdf4cef2a69871869dcc90c678bd272d43ce0ac6c8adf45ee8cf54", "88d6efc19fe636fe8c517faa303b4e05ede779942c2d07a3c551011d969873b4"],
|
|
21
|
+
"components/MenuGroup.tsx": ["4f2e53fbd9bc5fdf3c9e033e28f4e6582470a70a4e05ac51270a233a05345aa8"],
|
|
22
|
+
};
|
|
23
|
+
const OLD_FRAME = `export function Frame({ site, current, children }: { site: Content; current: string; children: ReactNode }) {
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<a className="skip" href="#main">
|
|
27
|
+
Skip to content
|
|
28
|
+
</a>
|
|
29
|
+
<Header site={site} current={current} />
|
|
30
|
+
<main id="main" tabIndex={-1}>
|
|
31
|
+
{children}
|
|
32
|
+
</main>
|
|
33
|
+
<Footer site={site} />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}`;
|
|
37
|
+
const NEW_FRAME = `/**
|
|
38
|
+
* The frame around every page: the kit's (skip link, header, footer), so a new header or footer
|
|
39
|
+
* option reaches this site with a kit release. A site with a header of its own passes it:
|
|
40
|
+
* <KitFrame header={<MyHeader … />}>, and leaves \`header\` out of content/look.ts unless it follows
|
|
41
|
+
* the Look's Header (headerOf).
|
|
42
|
+
*/
|
|
43
|
+
export function Frame({ site, current, children }: { site: Content; current: string; children: ReactNode }) {
|
|
44
|
+
return (
|
|
45
|
+
<KitFrame site={site} current={current} look={look}>
|
|
46
|
+
{children}
|
|
47
|
+
</KitFrame>
|
|
48
|
+
);
|
|
49
|
+
}`;
|
|
50
|
+
/**
|
|
51
|
+
* The starter's header and footer become the kit's (KitFrame), which follow the Look's Header, so
|
|
52
|
+
* the site's look.ts lists one and CalOS offers its options. The starter's footer was its Slate:
|
|
53
|
+
* kept, where the site's CSS has --dark. The old header and footer CSS is left, unused.
|
|
54
|
+
*/
|
|
55
|
+
/** The starter's design, told to the kit's frame: its dark footer and its words on it, and its buttons' weight. */
|
|
56
|
+
const FRAME_CSS = `/* The kit draws the header and footer (KitFrame): in this design's dark, its buttons as its own. */
|
|
57
|
+
:root {
|
|
58
|
+
--kit-footer-bg: var(--dark);
|
|
59
|
+
--kit-footer-fg: var(--on-dark);
|
|
60
|
+
--kit-footer-muted: var(--muted-on-dark);
|
|
61
|
+
--kit-footer-line: var(--line-on-dark);
|
|
62
|
+
--kit-button-weight: var(--b-weight);
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
const kitFrame = {
|
|
66
|
+
id: "kit-frame",
|
|
67
|
+
since: "0.2.11",
|
|
68
|
+
title: "The header and footer are the kit's (KitFrame), so new header and footer options reach this site with kit releases",
|
|
69
|
+
async run(read) {
|
|
70
|
+
const page = await read("components/SitePage.tsx");
|
|
71
|
+
if (!page)
|
|
72
|
+
return { manual: "it has no components/SitePage.tsx: its pages are drawn its own way" };
|
|
73
|
+
if (page.includes("KitFrame"))
|
|
74
|
+
return { done: true };
|
|
75
|
+
if (!page.includes(OLD_FRAME))
|
|
76
|
+
return { manual: "its components/SitePage.tsx draws the frame its own way (not the starter's Frame)" };
|
|
77
|
+
for (const [path, known] of Object.entries(STARTER_FRAME_FILES)) {
|
|
78
|
+
const text = await read(path);
|
|
79
|
+
if (text !== null && !known.includes(await sha256(text)))
|
|
80
|
+
return { manual: `its ${path} is its own, not the starter's` };
|
|
81
|
+
}
|
|
82
|
+
const look = await read("content/look.ts");
|
|
83
|
+
if (!look || !/export const look: LookOptions = \{[\s\S]*\n\};\s*$/.test(look))
|
|
84
|
+
return { manual: "its content/look.ts isn't the starter's shape" };
|
|
85
|
+
const imports = /import \{([^}]*)\} from "@caltho\/site-kit\/next";/;
|
|
86
|
+
const m = page.match(imports);
|
|
87
|
+
if (!m)
|
|
88
|
+
return { manual: "its components/SitePage.tsx doesn't import from @caltho/site-kit/next" };
|
|
89
|
+
const names = [...new Set([...m[1].split(",").map((x) => x.trim()).filter(Boolean), "KitFrame"])].sort((a, b) => a.localeCompare(b));
|
|
90
|
+
let nextPage = page
|
|
91
|
+
.replace(OLD_FRAME, NEW_FRAME)
|
|
92
|
+
.replace(imports, `import { ${names.join(", ")} } from "@caltho/site-kit/next";`)
|
|
93
|
+
.replace('import { Footer } from "./Footer";\n', "")
|
|
94
|
+
.replace('import { Header } from "./Header";\n', "");
|
|
95
|
+
if (!nextPage.includes('from "@/content/look"'))
|
|
96
|
+
nextPage = nextPage.replace(imports, (line) => `${line}\nimport { look } from "@/content/look";`);
|
|
97
|
+
const files = [{ path: "components/SitePage.tsx", text: nextPage }];
|
|
98
|
+
if (!/^\s*header:/m.test(look)) {
|
|
99
|
+
files.push({
|
|
100
|
+
path: "content/look.ts",
|
|
101
|
+
text: look.replace(/\n\};\s*$/, `\n /** The kit's header (KitFrame) follows the Look's Header, so CalOS offers its options: sticky, with a line under it, name left and menu right to start. */\n header: { sticky: true, style: "line", layout: "spread" },\n};\n`),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const css = await read("app/globals.css");
|
|
105
|
+
if (css && /--dark\s*:/.test(css) && !css.includes("--kit-footer-bg")) {
|
|
106
|
+
files.push({ path: "app/globals.css", text: `${css.replace(/\s*$/, "")}\n\n${FRAME_CSS}` });
|
|
107
|
+
}
|
|
108
|
+
for (const path of Object.keys(STARTER_FRAME_FILES))
|
|
109
|
+
if ((await read(path)) !== null)
|
|
110
|
+
files.push({ path, remove: true });
|
|
111
|
+
return { files };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const ACCENT_CSS = `/* The accent colour: a button that stands out on a photo or a dark band (an action's style "accent"). */
|
|
115
|
+
.btn-accent {
|
|
116
|
+
background: var(--look-accent, var(--accent));
|
|
117
|
+
border-color: var(--look-accent, var(--accent));
|
|
118
|
+
color: var(--look-on-accent, var(--ink));
|
|
119
|
+
}
|
|
120
|
+
.btn-accent:hover {
|
|
121
|
+
background: color-mix(in srgb, var(--look-accent, var(--accent)) 88%, #000000);
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
/**
|
|
125
|
+
* Actions may be in the accent colour (style "accent"), which the starter's design draws as
|
|
126
|
+
* .btn-accent: added to a site whose CSS has the starter's buttons (.btn-primary) and none yet.
|
|
127
|
+
*/
|
|
128
|
+
const accentButtons = {
|
|
129
|
+
id: "accent-buttons",
|
|
130
|
+
since: "0.2.11",
|
|
131
|
+
title: "Buttons in the accent colour: an action's style can be the accent, for one that stands out on a photo or a dark band",
|
|
132
|
+
async run(read) {
|
|
133
|
+
const css = await read("app/globals.css");
|
|
134
|
+
if (!css)
|
|
135
|
+
return { manual: "it has no app/globals.css" };
|
|
136
|
+
if (css.includes(".btn-accent"))
|
|
137
|
+
return { done: true };
|
|
138
|
+
if (!css.includes(".btn-primary {"))
|
|
139
|
+
return { manual: "its buttons aren't the starter's (.btn-primary): it draws an accent button its own way" };
|
|
140
|
+
return { files: [{ path: "app/globals.css", text: `${css.replace(/\s*$/, "")}\n\n${ACCENT_CSS}` }] };
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
/** The starter's dark, a palette colour the business may remove (Cal does, building from a picture). */
|
|
144
|
+
const STARTER_DARK = "--dark:var(--look-swatch-slate, #1d2a33);";
|
|
145
|
+
/**
|
|
146
|
+
* The design's dark (its dark sections and the footer) following the theme when the business has
|
|
147
|
+
* taken its Slate away: the theme's own dark (the darker of its page and text), not a navy no
|
|
148
|
+
* colour in the theme shows.
|
|
149
|
+
*/
|
|
150
|
+
const darkFollowsLook = {
|
|
151
|
+
id: "dark-follows-look",
|
|
152
|
+
since: "0.2.11",
|
|
153
|
+
title: "The design's dark follows the theme: with its Slate colour removed, dark sections and the footer take the theme's own dark, not a hidden navy",
|
|
154
|
+
async run(read) {
|
|
155
|
+
const css = await read("app/globals.css");
|
|
156
|
+
if (!css)
|
|
157
|
+
return { manual: "it has no app/globals.css" };
|
|
158
|
+
if (css.includes("--dark:var(--look-swatch-slate, var(--look-dark"))
|
|
159
|
+
return { done: true };
|
|
160
|
+
if (!css.includes(STARTER_DARK))
|
|
161
|
+
return { manual: "its dark isn't the starter's (--dark from Slate): it draws its dark its own way" };
|
|
162
|
+
return { files: [{ path: "app/globals.css", text: css.replace(STARTER_DARK, "--dark:var(--look-swatch-slate, var(--look-dark, #1d2a33));") }] };
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
/** The starter's buttons' border, before and after it reads the Look's button border (width and colour). */
|
|
166
|
+
const BUTTON_BORDER = [
|
|
167
|
+
[" border: 2px solid transparent;\n border-radius: var(--radius-button);", " border: var(--look-button-border-width, 2px) solid transparent;\n border-radius: var(--radius-button);"],
|
|
168
|
+
[" border-color: color-mix(in srgb, var(--brand-fill) var(--fill), var(--c-link));", " border-color: var(--look-button-border-colour, color-mix(in srgb, var(--brand-fill) var(--fill), var(--c-link)));"],
|
|
169
|
+
[" border-color: var(--look-accent, var(--accent));\n color: var(--look-on-accent, var(--ink));", " border-color: var(--look-button-border-colour, var(--look-accent, var(--accent)));\n color: var(--look-on-accent, var(--ink));"],
|
|
170
|
+
[" border-color: var(--on-brand);\n color: color-mix(in srgb, var(--brand-fill) var(--fill), var(--on-brand));", " border-color: var(--look-button-border-colour, var(--on-brand));\n color: color-mix(in srgb, var(--brand-fill) var(--fill), var(--on-brand));"],
|
|
171
|
+
];
|
|
172
|
+
/**
|
|
173
|
+
* Buttons with the Look's border: its width and colour, where the business chooses one (a dark
|
|
174
|
+
* outline on a yellow button). Every line the starter's, or it's left for a person.
|
|
175
|
+
*/
|
|
176
|
+
const buttonBorders = {
|
|
177
|
+
id: "button-borders",
|
|
178
|
+
since: "0.2.11",
|
|
179
|
+
title: "Button borders: the Look's button border (its width and colour) on the site's buttons",
|
|
180
|
+
async run(read) {
|
|
181
|
+
const css = await read("app/globals.css");
|
|
182
|
+
if (!css)
|
|
183
|
+
return { manual: "it has no app/globals.css" };
|
|
184
|
+
if (css.includes("--look-button-border-width"))
|
|
185
|
+
return { done: true };
|
|
186
|
+
if (!BUTTON_BORDER.every(([from]) => css.includes(from)))
|
|
187
|
+
return { manual: "its buttons aren't the starter's: it draws their borders its own way" };
|
|
188
|
+
return { files: [{ path: "app/globals.css", text: BUTTON_BORDER.reduce((t, [from, to]) => t.replace(from, to), css) }] };
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
/** The starter's own font imports, which the kit's fonts.css takes the place of. */
|
|
192
|
+
const STARTER_FONT_IMPORTS = /^import "@fontsource\/(fraunces|inter)\/latin-\d+\.css";\n/gm;
|
|
193
|
+
const STARTER_FONTS_LINE = ` fonts: { Inter: '"Inter", system-ui, Arial, sans-serif', Fraunces: '"Fraunces", Georgia, serif' },`;
|
|
194
|
+
/**
|
|
195
|
+
* The kit's fonts (fonts.css: ten variable families, every weight) in place of the starter's two
|
|
196
|
+
* fixed-weight ones, and listed in the site's look so the business can pick any of them. Where the
|
|
197
|
+
* site loads fonts of its own, or lists its own, it's left for a person.
|
|
198
|
+
*/
|
|
199
|
+
const kitFonts = {
|
|
200
|
+
id: "kit-fonts",
|
|
201
|
+
since: "0.2.11",
|
|
202
|
+
title: "The kit's fonts: ten variable families with every weight, any of them a choice in the site's Look",
|
|
203
|
+
async run(read) {
|
|
204
|
+
const layout = await read("app/layout.tsx");
|
|
205
|
+
const look = await read("content/look.ts");
|
|
206
|
+
if (!layout || !look)
|
|
207
|
+
return { manual: "it has no app/layout.tsx or content/look.ts like the starter's" };
|
|
208
|
+
if (layout.includes("@caltho/site-kit/fonts.css"))
|
|
209
|
+
return { done: true };
|
|
210
|
+
const own = layout.match(/^import "@fontsource[^"]*";$/gm) ?? [];
|
|
211
|
+
if (!own.length || own.some((l) => !/^import "@fontsource\/(fraunces|inter)\/latin-\d+\.css";$/.test(l)))
|
|
212
|
+
return { manual: "its app/layout.tsx loads fonts of its own" };
|
|
213
|
+
if (!look.includes(STARTER_FONTS_LINE))
|
|
214
|
+
return { manual: "its content/look.ts lists fonts of its own" };
|
|
215
|
+
const nextLayout = layout.replace(STARTER_FONT_IMPORTS, "").replace('import "@caltho/site-kit/sections.css";', 'import "@caltho/site-kit/sections.css";\nimport "@caltho/site-kit/fonts.css";');
|
|
216
|
+
if (!nextLayout.includes("@caltho/site-kit/fonts.css"))
|
|
217
|
+
return { manual: "its app/layout.tsx doesn't import the kit's sections.css, where the fonts go beside it" };
|
|
218
|
+
const files = [
|
|
219
|
+
{ path: "app/layout.tsx", text: nextLayout },
|
|
220
|
+
{
|
|
221
|
+
path: "content/look.ts",
|
|
222
|
+
text: look
|
|
223
|
+
.replace(STARTER_FONTS_LINE, " fonts: KIT_FONTS,")
|
|
224
|
+
.replace('import type { LookOptions } from "@caltho/site-kit/content";', 'import { KIT_FONTS, type LookOptions } from "@caltho/site-kit/content";'),
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
const css = await read("app/globals.css");
|
|
228
|
+
if (css)
|
|
229
|
+
files.push({ path: "app/globals.css", text: css.replace('--font-body: "Inter";', '--font-body: "Inter Variable";').replace('--font-display: "Fraunces";', '--font-display: "Fraunces Variable";') });
|
|
230
|
+
const pkg = await read("package.json");
|
|
231
|
+
if (pkg)
|
|
232
|
+
files.push({ path: "package.json", text: pkg.replace(/^\s*"@fontsource\/(fraunces|inter)": "[^"]*",\n/gm, "") });
|
|
233
|
+
return { files };
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
/** Every migration, oldest first. */
|
|
237
|
+
export const MIGRATIONS = [kitFrame, accentButtons, kitFonts, darkFollowsLook, buttonBorders];
|
|
238
|
+
const parts = (v) => v.split(".").map(Number);
|
|
239
|
+
function atLeast(v, min) {
|
|
240
|
+
const [a, b] = [parts(v), parts(min)];
|
|
241
|
+
for (let i = 0; i < 3; i++)
|
|
242
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
243
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* What moving a site to kit `version` changes in its own files: each migration up to that version it
|
|
248
|
+
* hasn't had, in order, each seeing the files as the ones before it left them.
|
|
249
|
+
*/
|
|
250
|
+
export async function migrateSite(read, version, migrations = MIGRATIONS) {
|
|
251
|
+
const recorded = await read(MIGRATIONS_FILE);
|
|
252
|
+
let had = [];
|
|
253
|
+
try {
|
|
254
|
+
const parsed = recorded ? JSON.parse(recorded) : null;
|
|
255
|
+
if (Array.isArray(parsed?.applied))
|
|
256
|
+
had = parsed.applied.filter((x) => typeof x === "string");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// An unreadable record: as if none, each migration checking the files for itself.
|
|
260
|
+
}
|
|
261
|
+
const changed = new Map();
|
|
262
|
+
const view = async (path) => {
|
|
263
|
+
const c = changed.get(path);
|
|
264
|
+
return c ? ("remove" in c ? null : c.text) : read(path);
|
|
265
|
+
};
|
|
266
|
+
const applied = [];
|
|
267
|
+
const manual = [];
|
|
268
|
+
for (const m of migrations) {
|
|
269
|
+
if (had.includes(m.id) || !atLeast(version, m.since))
|
|
270
|
+
continue;
|
|
271
|
+
const r = await m.run(view);
|
|
272
|
+
if ("manual" in r) {
|
|
273
|
+
manual.push({ id: m.id, title: m.title, why: r.manual });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if ("files" in r)
|
|
277
|
+
for (const f of r.files)
|
|
278
|
+
changed.set(f.path, f);
|
|
279
|
+
applied.push({ id: m.id, title: m.title });
|
|
280
|
+
}
|
|
281
|
+
if (applied.length)
|
|
282
|
+
changed.set(MIGRATIONS_FILE, { path: MIGRATIONS_FILE, text: `${JSON.stringify({ applied: [...had, ...applied.map((a) => a.id)] }, null, 2)}\n` });
|
|
283
|
+
return { files: [...changed.values()], applied, manual };
|
|
284
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** True inside CalOS's preview (SitePreview provides it). */
|
|
2
|
+
export declare const InPreview: import("react").Context<boolean>;
|
|
3
|
+
/** The attribute a block's mark carries: the block's index on its page. */
|
|
4
|
+
export declare const BLOCK_MARK = "data-calos-block";
|
|
5
|
+
export declare function BlockMark({ at }: {
|
|
6
|
+
at: number;
|
|
7
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// Where each block starts, for CalOS's preview: a site puts <BlockMark at={i} /> before each block it
|
|
4
|
+
// draws, and the preview knows exactly what each block drew (none, one, or several elements), with
|
|
5
|
+
// nothing guessed from the words. On the live site it draws nothing; in the preview, an empty
|
|
6
|
+
// <template>, which the browser never shows and styles never see as drawn.
|
|
7
|
+
import { createContext, useContext } from "react";
|
|
8
|
+
/** True inside CalOS's preview (SitePreview provides it). */
|
|
9
|
+
export const InPreview = createContext(false);
|
|
10
|
+
/** The attribute a block's mark carries: the block's index on its page. */
|
|
11
|
+
export const BLOCK_MARK = "data-calos-block";
|
|
12
|
+
export function BlockMark({ at }) {
|
|
13
|
+
if (!useContext(InPreview))
|
|
14
|
+
return null;
|
|
15
|
+
return _jsx("template", { [BLOCK_MARK]: String(at) });
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type KitBlock, type KitClasses } from "./sections.tsx";
|
|
3
|
+
export declare function KitBlocks<B extends KitBlock>({ blocks, render, classes, level, }: {
|
|
4
|
+
blocks: readonly B[];
|
|
5
|
+
/** The site's own component for a block, or null (or undefined) for a kind it leaves to the kit. */
|
|
6
|
+
render: (block: B, i: number) => ReactNode | null | undefined;
|
|
7
|
+
/** Classes of the site's own for kit sections, in place of the kit's (KitSection). */
|
|
8
|
+
classes?: Partial<KitClasses>;
|
|
9
|
+
/** The heading level of a block's own heading: 1 for the one that carries the page's h1. */
|
|
10
|
+
level?: (i: number) => number;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// A page's blocks drawn, the same way on every site: each after its mark (BlockMark, for CalOS's
|
|
3
|
+
// preview), in a frame when its Layout has a background (a colour, or a photo under a shade), then
|
|
4
|
+
// the site's own component for it, else the kit's (KitSection). A site hands it how it draws its own
|
|
5
|
+
// kinds (`render`), and nothing else: a block whose kind it doesn't draw (one the business put
|
|
6
|
+
// together in CalOS, a slideshow, one the kit adds later) is drawn by the kit in the site's Look.
|
|
7
|
+
//
|
|
8
|
+
// Classes only, no style attribute (the content security policy blocks them): the frame's colour is
|
|
9
|
+
// a class (sections.css reads the Look's variables), and its words go light by a class when its
|
|
10
|
+
// background is dark, worked out from the Look's colour values (lookValues: at build, and as sent
|
|
11
|
+
// in the preview).
|
|
12
|
+
import { Fragment } from "react";
|
|
13
|
+
import { frameClass, hasBackground, linesClass } from "../content/index.js";
|
|
14
|
+
import { BlockMark } from "./block-mark.js";
|
|
15
|
+
import { lookValues } from "./data.js";
|
|
16
|
+
import { Img } from "./parts/Img.js";
|
|
17
|
+
import { KitSection } from "./sections.js";
|
|
18
|
+
export function KitBlocks({ blocks, render, classes, level = () => 2, }) {
|
|
19
|
+
const vars = lookValues();
|
|
20
|
+
return (_jsx(_Fragment, { children: blocks.map((block, i) => {
|
|
21
|
+
const own = render(block, i);
|
|
22
|
+
const drawn = own ?? _jsx(KitSection, { s: block, level: level(i), classes: classes });
|
|
23
|
+
return (_jsxs(Fragment, { children: [_jsx(BlockMark, { at: i }), lined(block, hasBackground(block) ? _jsx(Frame, { block: block, vars: vars, children: drawn }) : drawn)] }, i));
|
|
24
|
+
}) }));
|
|
25
|
+
}
|
|
26
|
+
/** A section between lines across the page's full width, where its Layout says. */
|
|
27
|
+
function lined(block, drawn) {
|
|
28
|
+
const cls = linesClass(block);
|
|
29
|
+
return cls ? _jsx("div", { className: cls, children: drawn }) : drawn;
|
|
30
|
+
}
|
|
31
|
+
/** What's behind a section: its colour, and a photo under a shade, the section drawn over them. */
|
|
32
|
+
function Frame({ block, vars, children }) {
|
|
33
|
+
const bg = block.placement?.background;
|
|
34
|
+
return (_jsxs("div", { className: frameClass(block, vars), children: [bg?.photo?.src && (_jsx("div", { className: "kit-frame-bg", "aria-hidden": "true", children: _jsx(Img, { src: bg.photo.src, alt: "", sizes: "100vw" }) })), children] }));
|
|
35
|
+
}
|
package/dist/next/data.d.ts
CHANGED
|
@@ -4,8 +4,12 @@ import type { Prepared, PreparedImage } from "../prepare.ts";
|
|
|
4
4
|
export declare function loadPreparedWith(load: () => Prepared): void;
|
|
5
5
|
/** Puts content in the store: CalOS's preview, with the draft it's editing. */
|
|
6
6
|
export declare function setPrepared(p: Prepared): 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
|
@@ -15,6 +15,8 @@ export function setPrepared(p) {
|
|
|
15
15
|
current = p;
|
|
16
16
|
drafting = true;
|
|
17
17
|
}
|
|
18
|
+
/** Whether what's drawn is a draft in CalOS's preview (a part with nothing in it yet shows where it goes). */
|
|
19
|
+
export const isDraft = () => drafting;
|
|
18
20
|
/** A picture not chosen yet, in a draft: a grey box where it will go. */
|
|
19
21
|
const NOT_CHOSEN = {
|
|
20
22
|
width: 1600,
|
|
@@ -30,6 +32,8 @@ export function prepared() {
|
|
|
30
32
|
return current;
|
|
31
33
|
}
|
|
32
34
|
export const site = () => prepared().site;
|
|
35
|
+
/** The Look's values as CSS variables (lookVars), at build and in the preview; empty for a site without a Look. */
|
|
36
|
+
export const lookValues = () => prepared().look ?? {};
|
|
33
37
|
export function image(src) {
|
|
34
38
|
const found = prepared().images[src];
|
|
35
39
|
// A section being filled in, in the preview, is drawn with its picture still to choose; a build
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The top of every page: the business's logo or name (home), the menu, and the one thing most
|
|
10
|
+
* visitors came to do (the menu's button, else a call button where there's a phone). On a phone the
|
|
11
|
+
* menu folds into a native disclosure, so it opens without any script, and calling stays one tap
|
|
12
|
+
* away. Sticky or not, its style and layout are the Look's Header (headerOf).
|
|
13
|
+
*/
|
|
14
|
+
export declare function KitHeader({ site, current, look }: {
|
|
15
|
+
site: FrameSite;
|
|
16
|
+
current: string;
|
|
17
|
+
look: Pick<LookOptions, "header">;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** The bottom of every page: how to reach them, when they're open, the pages, the policies, and where else they are online. */
|
|
20
|
+
export declare function KitFooter({ site }: {
|
|
21
|
+
site: FrameSite;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
/**
|
|
24
|
+
* 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).
|
|
26
|
+
*/
|
|
27
|
+
export declare function KitFrame({ site, current, look, header, footer, children }: {
|
|
28
|
+
site: FrameSite;
|
|
29
|
+
current: string;
|
|
30
|
+
look: Pick<LookOptions, "header">;
|
|
31
|
+
header?: ReactNode;
|
|
32
|
+
footer?: ReactNode;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
const PATHS = {
|
|
17
|
+
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
|
+
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",
|
|
19
|
+
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",
|
|
20
|
+
menu: "M4 6h16M4 12h16M4 18h16",
|
|
21
|
+
close: "M18 6 6 18M6 6l12 12",
|
|
22
|
+
};
|
|
23
|
+
/** A line icon beside words that say the same, so hidden from a screen reader. */
|
|
24
|
+
function FrameIcon({ name, className }) {
|
|
25
|
+
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] }) }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The top of every page: the business's logo or name (home), the menu, and the one thing most
|
|
29
|
+
* visitors came to do (the menu's button, else a call button where there's a phone). On a phone the
|
|
30
|
+
* menu folds into a native disclosure, so it opens without any script, and calling stays one tap
|
|
31
|
+
* away. Sticky or not, its style and layout are the Look's Header (headerOf).
|
|
32
|
+
*/
|
|
33
|
+
export function KitHeader({ site, current, look }) {
|
|
34
|
+
const b = site.business;
|
|
35
|
+
const h = headerOf(site.look, look);
|
|
36
|
+
const { links: menu, button: chosen } = menuOf(site);
|
|
37
|
+
const action = chosen ?? (b.phone ? { label: b.phone, href: telHref(b.phone, b.address?.country) } : null);
|
|
38
|
+
const isCurrent = (href) => href === current || (href !== "/" && current.startsWith(href));
|
|
39
|
+
const call = !!action?.href.startsWith("tel:");
|
|
40
|
+
const link = (m) => (_jsx("a", { href: m.href, "aria-current": isCurrent(m.href) ? "page" : undefined, className: m.emphasis ? "is-emphasis" : undefined, children: m.label }));
|
|
41
|
+
// The business's button as it chose it: its style (accent, quiet) and a border of its own.
|
|
42
|
+
const vars = lookOf();
|
|
43
|
+
const styled = [chosen?.style === "accent" ? "kit-button-accent" : chosen?.style === "quiet" ? "kit-button-quiet" : "", buttonStyleClass(chosen?.style, vars)].filter(Boolean).join(" ");
|
|
44
|
+
// The phone's menu button: in one of the business's button styles, where the header says.
|
|
45
|
+
const menuStyled = buttonStyleClass(h.menuStyle, vars);
|
|
46
|
+
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] })] }));
|
|
47
|
+
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")] })] }))] }) }));
|
|
48
|
+
}
|
|
49
|
+
const SOCIAL = { google: "Google", facebook: "Facebook", instagram: "Instagram", yelp: "Yelp", nextdoor: "Nextdoor", linkedin: "LinkedIn", youtube: "YouTube", tiktok: "TikTok" };
|
|
50
|
+
/** The bottom of every page: how to reach them, when they're open, the pages, the policies, and where else they are online. */
|
|
51
|
+
export function KitFooter({ site }) {
|
|
52
|
+
const b = site.business;
|
|
53
|
+
// The links edited in CalOS (Footer), else the menu's under "Pages" and the policy pages in the legal row.
|
|
54
|
+
const isPolicy = (path) => site.pages.find((p) => p.path === path)?.blocks.some((s) => s.type === "policy");
|
|
55
|
+
const footer = footerOf(site);
|
|
56
|
+
const columns = site.footer ? footer.columns : footer.columns.map((c) => ({ heading: "Pages", links: c.links.filter((l) => !isPolicy(l.href)) }));
|
|
57
|
+
const legal = site.footer ? footer.legal : site.pages.filter((p) => isPolicy(p.path)).map((p) => ({ label: p.menu ?? p.title, href: p.path }));
|
|
58
|
+
const social = Object.entries(b.social ?? {}).filter(([, url]) => url);
|
|
59
|
+
const hours = b.hours ?? [];
|
|
60
|
+
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
|
|
61
|
+
.filter((c) => c.links.length)
|
|
62
|
+
.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))) }))] })] }));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 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).
|
|
67
|
+
*/
|
|
68
|
+
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 === undefined ? _jsx(KitHeader, { site: site, current: current, look: look }) : header, _jsx("main", { id: "main", tabIndex: -1, children: children }), footer === undefined ? _jsx(KitFooter, { site: site }) : footer] }));
|
|
70
|
+
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
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";
|
package/dist/next/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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
|
|
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";
|
|
@@ -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
|
+
}
|
|
@@ -3,5 +3,7 @@ import { lookCss } from "../../content/look.js";
|
|
|
3
3
|
import { prepared } from "../data.js";
|
|
4
4
|
/** The site's Look as CSS variables, for its layout's <head> (content/look.ts). */
|
|
5
5
|
export function LookStyle({ look }) {
|
|
6
|
-
|
|
6
|
+
// And the Google fonts it uses, as the site serves them (fetched when it was built).
|
|
7
|
+
const p = prepared();
|
|
8
|
+
return _jsx("style", { dangerouslySetInnerHTML: { __html: [lookCss(p.site.look, look), p.fontFaces].filter(Boolean).join("\n") } });
|
|
7
9
|
}
|