@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/sections.test.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { describe, expect, it } from "bun:test";
|
|
3
3
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
-
import { defineSite, z } from "./content/index.js";
|
|
4
|
+
import { defineSite, lookVars, z } from "./content/index.js";
|
|
5
|
+
import { KitBlocks } from "./next/blocks.js";
|
|
6
|
+
import { InPreview } from "./next/block-mark.js";
|
|
5
7
|
import { setPrepared } from "./next/data.js";
|
|
6
8
|
import { KitSection } from "./next/sections.js";
|
|
7
9
|
import { Slideshow } from "./next/slideshow.js";
|
|
8
|
-
import { gallery, isKitSection, sections, slideshow } from "./sections/index.js";
|
|
10
|
+
import { gallery, ICON_NAMES, isKitSection, own as ownSection, presets, sections, slideshow, videoEmbed } from "./sections/index.js";
|
|
11
|
+
/** A site's own styles for each thing kit sections draw, given in place of the kit's. */
|
|
12
|
+
const theme = { section: "s", wrap: "w", headingLarge: "h-l", headingMedium: "h-m", headingSmall: "h-s", text: "t", muted: "m", button: "b", buttonQuiet: "bq" };
|
|
9
13
|
// Sections any site can use: their fields, as a site takes them into its own, and drawn.
|
|
10
14
|
const own = { hero: z.object({ heading: z.string() }) };
|
|
11
15
|
describe("the kit's sections", () => {
|
|
@@ -36,9 +40,9 @@ describe("a gallery, drawn", () => {
|
|
|
36
40
|
csp: {},
|
|
37
41
|
});
|
|
38
42
|
it("is a grid of its photos, each opening its largest copy, with its heading, intro and captions", () => {
|
|
39
|
-
const html = renderToStaticMarkup(_jsx(KitSection, { s: { type: "gallery", id: "work", heading: "Our decks", intro: "Built this **summer**.", columns: "4", shape: "landscape", images: [{ src: "a.jpg", alt: "A cedar deck", caption: "Cedar, 2026" }, { src: "b.jpg", alt: "A composite deck" }] }, level: 2 }));
|
|
40
|
-
expect(html).toContain('<section class="
|
|
41
|
-
expect(html).toContain('<h2 id="work-heading" class="
|
|
43
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: { type: "gallery", id: "work", heading: "Our decks", intro: "Built this **summer**.", columns: "4", shape: "landscape", images: [{ src: "a.jpg", alt: "A cedar deck", caption: "Cedar, 2026" }, { src: "b.jpg", alt: "A composite deck" }] }, level: 2, classes: theme }));
|
|
44
|
+
expect(html).toContain('<section class="s kit-gallery" id="work" aria-labelledby="work-heading">');
|
|
45
|
+
expect(html).toContain('<h2 id="work-heading" class="h-l">Our decks</h2>');
|
|
42
46
|
expect(html).toContain("<strong>summer</strong>");
|
|
43
47
|
expect(html).toContain('class="kit-gallery-grid kit-cols-4"');
|
|
44
48
|
expect(html).toContain('<a href="/_img/a.1600.webp" class="kit-gallery-open">');
|
|
@@ -51,14 +55,14 @@ describe("a gallery, drawn", () => {
|
|
|
51
55
|
expect(html).not.toMatch(/\sstyle=/);
|
|
52
56
|
});
|
|
53
57
|
it("sits in a site with a design of its own: its section, container and heading classes in place of the kit's", () => {
|
|
54
|
-
const html = renderToStaticMarkup(_jsx(KitSection, { s: { type: "gallery", heading: "Inside", images: [{ src: "a.jpg", alt: "A cabin" }] }, classes: { section: "", wrap: "mx-auto max-w-5xl px-4",
|
|
58
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: { type: "gallery", heading: "Inside", images: [{ src: "a.jpg", alt: "A cabin" }] }, classes: { ...theme, section: "", wrap: "mx-auto max-w-5xl px-4", headingLarge: "font-display text-3xl" } }));
|
|
55
59
|
expect(html).toContain('<section class="kit-gallery"');
|
|
56
60
|
expect(html).toContain('<div class="mx-auto max-w-5xl px-4">');
|
|
57
61
|
expect(html).toContain('class="font-display text-3xl">Inside</h2>');
|
|
58
62
|
expect(html).not.toContain("kit-wrap");
|
|
59
63
|
});
|
|
60
64
|
it("draws nothing for a type that isn't the kit's: the site's own to draw", () => {
|
|
61
|
-
expect(renderToStaticMarkup(_jsx(KitSection, { s: { type: "hero", heading: "Hi" } }))).toBe("");
|
|
65
|
+
expect(renderToStaticMarkup(_jsx(KitSection, { s: { type: "hero", heading: "Hi" }, classes: theme }))).toBe("");
|
|
62
66
|
});
|
|
63
67
|
});
|
|
64
68
|
describe("a slideshow", () => {
|
|
@@ -86,9 +90,307 @@ describe("a slideshow", () => {
|
|
|
86
90
|
expect(ok.site.pages[0].blocks[0]).toMatchObject({ seconds: 5, shape: "wide" });
|
|
87
91
|
expect(site.parse(page([{ type: "slideshow", images: [{ src: "a.jpg", alt: "A" }] }])).problems?.join(" ")).toMatch(/images/);
|
|
88
92
|
expect(slideshow.meta()?.title).toBe("Slideshow");
|
|
89
|
-
const html = renderToStaticMarkup(_jsx(KitSection, { s: { type: "slideshow", heading: "On board", shape: "landscape", seconds: 4, images: [{ src: "a.jpg", alt: "A cabin", caption: "Cabin" }, { src: "b.jpg", alt: "The deck" }] } }));
|
|
93
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: { type: "slideshow", heading: "On board", shape: "landscape", seconds: 4, images: [{ src: "a.jpg", alt: "A cabin", caption: "Cabin" }, { src: "b.jpg", alt: "The deck" }] }, classes: theme }));
|
|
90
94
|
expect(html).toContain('class="kit-slides kit-shape-landscape"');
|
|
91
95
|
expect(html).toContain("<figcaption>Cabin</figcaption>");
|
|
92
96
|
expect(html).toContain('aria-label="On board"');
|
|
93
97
|
});
|
|
94
98
|
});
|
|
99
|
+
describe("a section of the business's own", () => {
|
|
100
|
+
const page = (blocks, saved = []) => ({ url: "https://acme.example", business: { name: "Acme" }, saved, pages: [{ path: "/", title: "Acme", description: "Acme does things.", blocks }] });
|
|
101
|
+
const block = {
|
|
102
|
+
type: "own",
|
|
103
|
+
layout: "two",
|
|
104
|
+
columns: [
|
|
105
|
+
{ parts: [{ type: "heading", text: "Our packages", size: "large" }, { type: "text", text: "Pick **the one** that suits." }, { type: "list", items: ["Captain", "Food"], style: "checks" }, { type: "button", label: "Book", href: "/" }] },
|
|
106
|
+
{ parts: [{ type: "price", amount: "$120", per: "a night", note: "per person" }, { type: "stats", items: [{ value: "8", label: "guests" }, { value: "4", label: "cabins" }] }, { type: "quote", text: "Best week ever.", who: "Ana" }] },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
it("is put together from parts, checked like any section, and can be saved by name to add again", () => {
|
|
110
|
+
const site = defineSite({ ...sections });
|
|
111
|
+
const ok = site.parse(page([block], [{ name: "Package card", block }]));
|
|
112
|
+
expect(ok.problems).toBeUndefined();
|
|
113
|
+
expect(ok.site.pages[0].blocks[0]).toMatchObject({ align: "left", tone: "plain" });
|
|
114
|
+
expect(ok.site.saved[0].name).toBe("Package card");
|
|
115
|
+
// A part it doesn't know, or a list with nothing in it, is said.
|
|
116
|
+
expect(site.parse(page([{ ...block, columns: [{ parts: [{ type: "marquee" }] }] }])).problems?.join(" ")).toMatch(/type/);
|
|
117
|
+
expect(site.parse(page([{ ...block, columns: [{ parts: [{ type: "list", items: [] }] }] }])).problems?.join(" ")).toMatch(/items/);
|
|
118
|
+
// Saved sections are optional: a site without any is as it was.
|
|
119
|
+
expect(site.parse(page([block])).site.saved).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
it("draws lines across a section's full width where its Layout says, strong in the text colour", async () => {
|
|
122
|
+
const { KitBlocks } = await import("./next/blocks.js");
|
|
123
|
+
const strip = { type: "own", columns: [{ parts: [{ type: "text", text: "Where we work" }] }], placement: { lines: "both", lineColour: "strong" } };
|
|
124
|
+
const html = renderToStaticMarkup(_jsx(KitBlocks, { blocks: [strip, { ...strip, placement: {} }], render: () => null }));
|
|
125
|
+
expect(html).toContain('<div class="kit-lines kit-lines-both kit-lines-strong">');
|
|
126
|
+
expect(html.match(/class="kit-lines /g)?.length).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
it("draws a heading at the Look's largest sizes, a part in the other font, and a column's parts at its foot", () => {
|
|
129
|
+
const own = { type: "own", layout: "two", columns: [{ place: "bottom", parts: [{ type: "heading", text: "The good outside.", size: "display" }, { type: "text", text: "Yard care.", font: "heading" }] }, { parts: [{ type: "items", font: "text", items: [{ title: "Yard care" }] }] }] };
|
|
130
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: own, level: 2, classes: theme }));
|
|
131
|
+
expect(html).toContain('<h2 class="h-l kit-type-display">The good outside.</h2>');
|
|
132
|
+
expect(html).toContain('class="kit-own-column kit-own-column-bottom"');
|
|
133
|
+
expect(html).toContain("kit-own-text t kit-font-heading");
|
|
134
|
+
expect(html).toContain('class="kit-own-items-title h-m kit-font-text"');
|
|
135
|
+
});
|
|
136
|
+
it("is drawn in the site's own classes: its heading, words and buttons", () => {
|
|
137
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: block, level: 2, classes: { ...theme, section: "", wrap: "site-wrap", headingLarge: "site-h", text: "site-text", button: "site-button", muted: "site-muted" } }));
|
|
138
|
+
expect(html).toContain('class="kit-own-grid kit-own-two kit-own-align-left"');
|
|
139
|
+
expect(html).toContain('<h2 class="site-h">Our packages</h2>');
|
|
140
|
+
expect(html).toContain("<strong>the one</strong>");
|
|
141
|
+
expect(html).toContain('class="kit-own-list kit-own-list-checks site-text"');
|
|
142
|
+
expect(html).toContain('<a href="/" class="site-button">Book</a>');
|
|
143
|
+
expect(html).toContain('<span class="kit-own-amount h-m">$120</span>');
|
|
144
|
+
expect(html).toContain('<dd class="h-m">8</dd>');
|
|
145
|
+
expect(html).toContain("<blockquote class=\"site-text\">Best week ever.</blockquote>");
|
|
146
|
+
expect(html).not.toMatch(/\sstyle=/);
|
|
147
|
+
});
|
|
148
|
+
it("keeps to its layout's columns, every button in the site's own button style", () => {
|
|
149
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: { ...block, layout: "one" }, classes: theme }));
|
|
150
|
+
expect(html.match(/kit-own-column/g)).toHaveLength(1);
|
|
151
|
+
expect(html).toContain('class="b"');
|
|
152
|
+
expect(html).not.toMatch(/kit-button|kit-heading/);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("a section of the business's own, its background and spacing", () => {
|
|
156
|
+
const one = (extra, parts = [{ type: "heading", text: "Hello" }, { type: "button", label: "Book", href: "/" }]) => ({ type: "own", columns: [{ parts }], ...extra });
|
|
157
|
+
it("is plain with the site's own spacing unless it says, and content from before still reads", () => {
|
|
158
|
+
const parsed = ownSection.parse({ columns: [{ parts: [{ type: "text", text: "Hi" }] }], tone: "brand" });
|
|
159
|
+
expect(parsed).toMatchObject({ layout: "one", align: "left", tone: "brand", spacing: "normal" });
|
|
160
|
+
expect(parsed.background).toBeUndefined();
|
|
161
|
+
expect(ownSection.safeParse({ columns: [{ parts: [] }], tone: "purple" }).success).toBe(false);
|
|
162
|
+
expect(ownSection.safeParse({ columns: [{ parts: [] }], spacing: "huge" }).success).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
it("draws its tone and spacing as classes, words and buttons kept readable on brand, dark or a photo", () => {
|
|
165
|
+
const plain = renderToStaticMarkup(_jsx(KitSection, { s: one({}), classes: theme }));
|
|
166
|
+
expect(plain).toContain('class="s kit-own kit-own-plain kit-own-pad-normal"');
|
|
167
|
+
const soft = renderToStaticMarkup(_jsx(KitSection, { s: one({ tone: "soft", spacing: "tight" }), classes: theme }));
|
|
168
|
+
expect(soft).toContain('class="s kit-own kit-own-soft kit-own-pad-tight kit-own-shape-card"');
|
|
169
|
+
for (const tone of ["brand", "dark"]) {
|
|
170
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one({ tone, spacing: "roomy" }), classes: { ...theme, button: "site-button" } }));
|
|
171
|
+
expect(html).toContain(`class="s kit-own kit-own-${tone} kit-own-pad-roomy kit-own-shape-card kit-own-inverse"`);
|
|
172
|
+
expect(html).toContain('<p class="kit-own-action"><a href="/" class="site-button">Book</a></p>');
|
|
173
|
+
}
|
|
174
|
+
expect(plain).not.toMatch(/\sstyle=/);
|
|
175
|
+
});
|
|
176
|
+
it("draws a photo behind it, under a scrim, its words light over it", () => {
|
|
177
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one({ background: { src: "a.jpg", alt: "" } }), classes: theme }));
|
|
178
|
+
expect(html).toContain("kit-own-behind kit-own-inverse");
|
|
179
|
+
expect(html).toMatch(/<div class="kit-own-bg" aria-hidden="true"><img [^>]*alt=""/);
|
|
180
|
+
expect(html).not.toMatch(/\sstyle=/);
|
|
181
|
+
});
|
|
182
|
+
it("keeps buttons one after another together, side by side", () => {
|
|
183
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one({}, [{ type: "button", label: "Call", href: "tel:+15550100000" }, { type: "button", label: "Write", href: "mailto:a@b.co", style: "quiet" }, { type: "text", text: "Or visit." }]), classes: theme }));
|
|
184
|
+
expect(html).toContain('<div class="kit-own-actions"><p class="kit-own-action"><a href="tel:+15550100000" class="b">Call</a></p><p class="kit-own-action kit-own-action-quiet"><a href="mailto:a@b.co" class="bq">Write</a></p></div>');
|
|
185
|
+
});
|
|
186
|
+
it("shows where a photo still to choose will go, in the preview", () => {
|
|
187
|
+
expect(ownSection.safeParse({ columns: [{ parts: [{ type: "photo" }] }] }).success).toBe(true);
|
|
188
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one({}, [{ type: "photo", shape: "square" }]), classes: theme }));
|
|
189
|
+
expect(html).toContain('<div class="kit-own-photo-empty kit-own-shape-square" aria-hidden="true"></div>');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe("ready-made sections", () => {
|
|
193
|
+
it("are each a valid section of the business's own, with a key, a name and a description", () => {
|
|
194
|
+
expect(presets.length).toBeGreaterThanOrEqual(9);
|
|
195
|
+
expect(new Set(presets.map((p) => p.key)).size).toBe(presets.length);
|
|
196
|
+
const site = defineSite({ ...sections });
|
|
197
|
+
for (const p of presets) {
|
|
198
|
+
expect(p.block.type).toBe("own");
|
|
199
|
+
const r = ownSection.safeParse(p.block);
|
|
200
|
+
if (!r.success)
|
|
201
|
+
throw new Error(`${p.key}: ${r.error.message}`);
|
|
202
|
+
const parsed = site.parse({ url: "https://acme.example", business: { name: "Acme" }, pages: [{ path: "/", title: "Acme", description: "Acme does things.", blocks: [p.block] }] });
|
|
203
|
+
expect(parsed.problems).toBeUndefined();
|
|
204
|
+
expect(p.name.length && p.description.length).toBeTruthy();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
it("read as plain business words: sentence case, no exclamation marks, no placeholder Latin", () => {
|
|
208
|
+
const words = JSON.stringify(presets);
|
|
209
|
+
expect(words).not.toMatch(/!/);
|
|
210
|
+
expect(words.toLowerCase()).not.toMatch(/lorem|ipsum/);
|
|
211
|
+
for (const p of presets)
|
|
212
|
+
expect(p.name[0]).toBe(p.name[0].toUpperCase());
|
|
213
|
+
});
|
|
214
|
+
it("draw", () => {
|
|
215
|
+
for (const p of presets)
|
|
216
|
+
expect(renderToStaticMarkup(_jsx(KitSection, { s: p.block, classes: theme }))).toContain("kit-own");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe("kit sections with no classes of the site's", () => {
|
|
220
|
+
const block = { type: "own", columns: [{ parts: [{ type: "heading", text: "Hello", size: "large" }, { type: "text", text: "Words." }, { type: "price", amount: "Price", per: "a visit" }, { type: "button", label: "Book", href: "/" }, { type: "button", label: "Call", href: "/", style: "quiet" }] }] };
|
|
221
|
+
it("are drawn in the kit's classes, which read the Look", () => {
|
|
222
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: block }));
|
|
223
|
+
expect(html).toContain('<section class="kit-section kit-own kit-own-plain kit-own-pad-normal"><div class="kit-wrap">');
|
|
224
|
+
expect(html).toContain('<h2 class="kit-h kit-h-large">Hello</h2>');
|
|
225
|
+
expect(html).toContain('kit-own-text kit-text"');
|
|
226
|
+
expect(html).toContain('<span class="kit-own-per kit-muted"> a visit</span>');
|
|
227
|
+
expect(html).toContain('<a href="/" class="kit-button">Book</a>');
|
|
228
|
+
expect(html).toContain('<a href="/" class="kit-button kit-button-quiet">Call</a>');
|
|
229
|
+
});
|
|
230
|
+
it("take any of the site's own in place of the kit's", () => {
|
|
231
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: block, classes: { headingLarge: "display", section: "" } }));
|
|
232
|
+
expect(html).toContain('<h2 class="display">Hello</h2>');
|
|
233
|
+
expect(html).toContain('<section class="kit-own');
|
|
234
|
+
expect(html).toContain('class="kit-button"');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe("a page's blocks, drawn by KitBlocks", () => {
|
|
238
|
+
const look = { brand: "#2b5f8a", palette: [{ name: "Sand", color: "#e8dcc4" }] };
|
|
239
|
+
const prepare = () => setPrepared({
|
|
240
|
+
site: { url: "https://acme.example", lang: "en", business: { name: "Acme" }, redirects: [], pages: [] },
|
|
241
|
+
images: { "a.jpg": { width: 1600, height: 1000, src: "/_img/a.1600.jpg", srcset: [] } },
|
|
242
|
+
csp: {},
|
|
243
|
+
look: lookVars(undefined, look),
|
|
244
|
+
});
|
|
245
|
+
const blocks = [
|
|
246
|
+
{ type: "hero", heading: "Hi" },
|
|
247
|
+
{ type: "own", columns: [{ parts: [{ type: "heading", text: "Ours" }] }], placement: { background: { color: "sand" } } },
|
|
248
|
+
{ type: "hero", heading: "Over a photo", placement: { background: { photo: { src: "a.jpg", alt: "A deck" }, overlay: "dark" } } },
|
|
249
|
+
{ type: "hero", heading: "Plain", placement: { align: "center" } },
|
|
250
|
+
];
|
|
251
|
+
const render = (b) => (b.type === "hero" ? _jsx("section", { className: "hero", children: String(b.heading) }) : null);
|
|
252
|
+
it("draws the site's own blocks, the kit's for the rest, and a frame only where there's a background", () => {
|
|
253
|
+
prepare();
|
|
254
|
+
const html = renderToStaticMarkup(_jsx(KitBlocks, { blocks: blocks, render: render }));
|
|
255
|
+
expect(html).toContain('<section class="hero">Hi</section>');
|
|
256
|
+
expect(html).toContain('<div class="kit-frame kit-bg-swatch-1"><section class="kit-section kit-own');
|
|
257
|
+
expect(html).toMatch(/<div class="kit-frame kit-frame-photo kit-overlay-dark kit-frame-dark"><div class="kit-frame-bg" aria-hidden="true"><img [^>]*alt=""[^>]*><\/div><section class="hero">Over a photo<\/section><\/div>/);
|
|
258
|
+
expect(html.match(/kit-frame"|kit-frame /g)).toHaveLength(2);
|
|
259
|
+
expect(html).toContain('<section class="hero">Plain</section>');
|
|
260
|
+
// No marks on the live site, and no style attribute anywhere (the content security policy blocks them).
|
|
261
|
+
expect(html).not.toContain("<template");
|
|
262
|
+
expect(html).not.toMatch(/\sstyle=/);
|
|
263
|
+
});
|
|
264
|
+
it("marks where each block starts in the preview, and gives each its heading level", () => {
|
|
265
|
+
prepare();
|
|
266
|
+
const html = renderToStaticMarkup(_jsx(InPreview.Provider, { value: true, children: _jsx(KitBlocks, { blocks: blocks.slice(0, 2), render: render, level: (i) => (i === 1 ? 1 : 2) }) }));
|
|
267
|
+
expect(html).toMatch(/^<template data-calos-block="0"><\/template><section class="hero">Hi<\/section><template data-calos-block="1"><\/template><div class="kit-frame/);
|
|
268
|
+
expect(html).toContain('<h2 class="kit-h kit-h-medium">Ours</h2>');
|
|
269
|
+
});
|
|
270
|
+
it("knows a background is dark from the Look as it is, so a light brand keeps dark words", () => {
|
|
271
|
+
setPrepared({ site: { pages: [] }, images: {}, csp: {}, look: lookVars(undefined, { brand: "#ffd400" }) });
|
|
272
|
+
const html = renderToStaticMarkup(_jsx(KitBlocks, { blocks: [{ type: "own", columns: [{ parts: [] }], placement: { background: { color: "brand" } } }], render: () => null }));
|
|
273
|
+
expect(html).toContain('<div class="kit-frame kit-bg-brand">');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe("the schema a site writes", () => {
|
|
277
|
+
it("carries the ready-made sections the site's own kind takes, at its root", () => {
|
|
278
|
+
const withOwn = defineSite({ ...sections, ...own }).jsonSchema();
|
|
279
|
+
expect(withOwn["x-presets"].map((p) => p.key)).toEqual(presets.map((p) => p.key));
|
|
280
|
+
expect(withOwn["x-presets"][0]).toMatchObject({ key: "hero", name: "Opening", block: { type: "own" } });
|
|
281
|
+
expect(defineSite(own).jsonSchema()["x-presets"]).toBeUndefined();
|
|
282
|
+
// Prices and numbers are never made up.
|
|
283
|
+
const words = JSON.stringify(withOwn["x-presets"]);
|
|
284
|
+
expect(words).not.toMatch(/"amount":"[^"]*\d/);
|
|
285
|
+
expect(words).not.toMatch(/"value":"[^"]*\d/);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe("a section of the business's own: tables, icons, logos and videos", () => {
|
|
289
|
+
const one = (parts, extra = {}) => ({ type: "own", columns: [{ parts }], ...extra });
|
|
290
|
+
const ok = (parts, extra = {}) => ownSection.safeParse(one(parts, extra));
|
|
291
|
+
const problems = (parts) => {
|
|
292
|
+
const r = ok(parts);
|
|
293
|
+
return r.success ? "" : r.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(" | ");
|
|
294
|
+
};
|
|
295
|
+
const table = { type: "table", columns: ["", "Basic", "Plus"], rows: [{ label: "Price", cells: ["Price", "Price"] }, { label: "Visits", cells: ["yes", "No"] }], highlight: 2 };
|
|
296
|
+
it("a table: two to five columns, a cell for each after the first, a column it picks out that's there", () => {
|
|
297
|
+
expect(ok([table]).success).toBe(true);
|
|
298
|
+
expect(ok([{ ...table, highlight: undefined }]).success).toBe(true);
|
|
299
|
+
expect(problems([{ ...table, columns: ["Only"] }])).toMatch(/columns/);
|
|
300
|
+
expect(problems([{ ...table, columns: ["", "a", "b", "c", "d", "e"] }])).toMatch(/columns/);
|
|
301
|
+
expect(problems([{ ...table, rows: [] }])).toMatch(/rows/);
|
|
302
|
+
expect(problems([{ ...table, rows: Array.from({ length: 21 }, () => ({ label: "Row", cells: ["a", "b"] })) }])).toMatch(/rows/);
|
|
303
|
+
expect(problems([{ ...table, rows: [{ label: "Price", cells: ["Price"] }] }])).toMatch(/rows.0.cells: One cell for each of the 2 columns/);
|
|
304
|
+
expect(problems([{ ...table, highlight: 3 }])).toMatch(/highlight/);
|
|
305
|
+
expect(problems([{ ...table, highlight: 0 }])).toMatch(/highlight/);
|
|
306
|
+
expect(problems([{ ...table, columns: ["", "Basic", ""] }])).toMatch(/columns.2: A column needs a heading/);
|
|
307
|
+
});
|
|
308
|
+
it("a table, drawn: a real table on a wider screen and a card for each row on a phone, ticks and dashes read as yes and no", () => {
|
|
309
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one([table], { heading: "Compare" }) }));
|
|
310
|
+
expect(html).toContain('<div class="kit-table-wide"><table class="kit-table kit-text"><thead><tr><td></td><th scope="col">Basic</th><th scope="col" class="kit-table-highlight">Plus</th></tr></thead>');
|
|
311
|
+
expect(html).toContain('<tr><th scope="row">Price</th><td>Price</td><td class="kit-table-highlight">Price</td></tr>');
|
|
312
|
+
expect(html).toMatch(/<td><span class="kit-table-yes"><svg class="kit-icon"[^>]*aria-hidden="true"[^>]*>.*?<\/svg><span class="kit-sr">Yes<\/span><\/span><\/td>/);
|
|
313
|
+
expect(html).toContain('<span class="kit-sr">No</span>');
|
|
314
|
+
// On a phone: each row a card, its label its heading (under the section's), each column's heading beside its cell.
|
|
315
|
+
expect(html).toContain('<ul class="kit-table-cards"><li class="kit-table-card"><h3 class="kit-h kit-h-small">Price</h3><dl class="kit-text"><div><dt>Basic</dt><dd>Price</dd></div><div class="kit-table-highlight"><dt>Plus</dt><dd>Price</dd></div></dl></li>');
|
|
316
|
+
expect(html).not.toMatch(/\sstyle=/);
|
|
317
|
+
});
|
|
318
|
+
it("an icon: one of the kit's, drawn inline in the theme's colour, never fetched", () => {
|
|
319
|
+
expect(ICON_NAMES.length).toBeGreaterThanOrEqual(40);
|
|
320
|
+
for (const name of ["check", "calendar", "map-pin", "arrow-right", "invoice", "warning", "grid"])
|
|
321
|
+
expect(ICON_NAMES).toContain(name);
|
|
322
|
+
expect(ICON_NAMES.some((n) => /sparkle/.test(n))).toBe(false);
|
|
323
|
+
expect(ok([{ type: "icon", name: "calendar" }]).success).toBe(true);
|
|
324
|
+
expect(problems([{ type: "icon", name: "sparkle" }])).toMatch(/name/);
|
|
325
|
+
expect(ownSection.parse(one([{ type: "icon", name: "clock" }])).columns[0].parts[0]).toMatchObject({ size: "medium" });
|
|
326
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one([{ type: "icon", name: "clock", size: "large" }]) }));
|
|
327
|
+
expect(html).toMatch(/<svg data-icon="clock" class="kit-icon kit-own-icon kit-own-icon-large" viewBox="0 0 24 24" fill="none" stroke="currentColor"[^>]*aria-hidden="true"[^>]*><path d="M12 22a10/);
|
|
328
|
+
expect(html).not.toMatch(/<img|<use|href=/);
|
|
329
|
+
});
|
|
330
|
+
it("logos: two to twelve, in a row with a line above if it has one", () => {
|
|
331
|
+
const logos = { type: "logos", title: "Who we work with", images: [{ src: "a.png", alt: "Acme" }, { src: "b.png", alt: "Bolt" }] };
|
|
332
|
+
expect(ok([logos]).success).toBe(true);
|
|
333
|
+
expect(problems([{ ...logos, images: [logos.images[0]] }])).toMatch(/images/);
|
|
334
|
+
expect(problems([{ ...logos, images: Array.from({ length: 13 }, () => logos.images[0]) }])).toMatch(/images/);
|
|
335
|
+
setPrepared({ site: { pages: [] }, images: { "a.png": { width: 200, height: 80, src: "/_img/a.png", srcset: [] }, "b.png": { width: 200, height: 80, src: "/_img/b.png", srcset: [] } }, csp: {} });
|
|
336
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one([logos]) }));
|
|
337
|
+
expect(html).toContain('<div class="kit-own-logos"><p class="kit-own-logos-title kit-muted">Who we work with</p><ul><li><img src="/_img/a.png"');
|
|
338
|
+
expect(html).toContain('alt="Bolt"');
|
|
339
|
+
});
|
|
340
|
+
it("a video: YouTube or Vimeo only, shown in the privacy-friendly player with its title", () => {
|
|
341
|
+
expect(videoEmbed("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=3")).toEqual({ src: "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ", origin: "https://www.youtube-nocookie.com" });
|
|
342
|
+
expect(videoEmbed("https://youtu.be/dQw4w9WgXcQ?si=x").src).toBe("https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ");
|
|
343
|
+
expect(videoEmbed("https://www.youtube.com/shorts/dQw4w9WgXcQ").src).toBe("https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ");
|
|
344
|
+
expect(videoEmbed("https://vimeo.com/76979871")).toEqual({ src: "https://player.vimeo.com/video/76979871?dnt=1", origin: "https://player.vimeo.com" });
|
|
345
|
+
expect(videoEmbed("https://vimeo.com/76979871/abc123").src).toBe("https://player.vimeo.com/video/76979871?h=abc123&dnt=1");
|
|
346
|
+
for (const bad of ["https://example.com/watch?v=dQw4w9WgXcQ", "http://youtu.be/dQw4w9WgXcQ", "https://youtu.be/short", "https://vimeo.com/channels/staff"]) {
|
|
347
|
+
expect(videoEmbed(bad)).toBeNull();
|
|
348
|
+
expect(problems([{ type: "video", url: bad, title: "A tour" }])).toMatch(/url: A YouTube or Vimeo address/);
|
|
349
|
+
}
|
|
350
|
+
expect(ok([{ type: "video", url: "https://youtu.be/dQw4w9WgXcQ", title: "A tour" }]).success).toBe(true);
|
|
351
|
+
expect(problems([{ type: "video", url: "https://youtu.be/dQw4w9WgXcQ" }])).toMatch(/title/);
|
|
352
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: one([{ type: "video", url: "https://youtu.be/dQw4w9WgXcQ", title: "A tour of the workshop" }]) }));
|
|
353
|
+
expect(html).toContain('<div class="kit-own-video"><iframe src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ" title="A tour of the workshop"');
|
|
354
|
+
expect(html).toContain('loading="lazy"');
|
|
355
|
+
// No address yet: a quiet box where it will go in the preview (the store holds a draft here), nothing on the site.
|
|
356
|
+
expect(renderToStaticMarkup(_jsx(KitSection, { s: one([{ type: "video", title: "A tour" }]) }))).toContain('<div class="kit-own-video kit-own-video-empty" aria-hidden="true"></div>');
|
|
357
|
+
});
|
|
358
|
+
it("columns drawn as cards, a heading and intro over them, and a toned section a card or a band", () => {
|
|
359
|
+
const block = {
|
|
360
|
+
type: "own",
|
|
361
|
+
id: "why",
|
|
362
|
+
layout: "two",
|
|
363
|
+
align: "center",
|
|
364
|
+
heading: "Why us",
|
|
365
|
+
intro: "Two **good** reasons.",
|
|
366
|
+
columns: [
|
|
367
|
+
{ card: true, parts: [{ type: "heading", text: "One", size: "large" }] },
|
|
368
|
+
{ parts: [{ type: "text", text: "Two" }] },
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
expect(ownSection.safeParse(block).success).toBe(true);
|
|
372
|
+
expect(ownSection.parse(block).shape).toBe("card");
|
|
373
|
+
expect(ownSection.safeParse({ ...block, shape: "circle" }).success).toBe(false);
|
|
374
|
+
const html = renderToStaticMarkup(_jsx(KitSection, { s: block, level: 2 }));
|
|
375
|
+
expect(html).toContain('<section class="kit-section kit-own kit-own-plain kit-own-pad-normal" id="why" aria-labelledby="why-heading"><div class="kit-wrap"><div class="kit-own-panel"><div class="kit-own-head kit-own-align-center"><h2 id="why-heading" class="kit-h kit-h-large">Why us</h2><div class="prose kit-own-intro kit-text"><p>Two <strong>good</strong> reasons.</p>');
|
|
376
|
+
// Under the section's heading, a part's large heading is a level down.
|
|
377
|
+
expect(html).toContain('<div class="kit-own-column kit-own-card"><h3 class="kit-h kit-h-large">One</h3>');
|
|
378
|
+
expect(html).toContain('<div class="kit-own-column"><div class="prose kit-own-text kit-text">');
|
|
379
|
+
// Plain has no shape; a toned one is a card unless it's a band; a photo behind is always across the page.
|
|
380
|
+
expect(html).not.toContain("kit-own-shape");
|
|
381
|
+
expect(renderToStaticMarkup(_jsx(KitSection, { s: { ...block, tone: "brand" } }))).toContain("kit-own-brand kit-own-pad-normal kit-own-shape-card kit-own-inverse");
|
|
382
|
+
expect(renderToStaticMarkup(_jsx(KitSection, { s: { ...block, tone: "soft", shape: "band" } }))).toContain("kit-own-soft kit-own-pad-normal kit-own-shape-band");
|
|
383
|
+
setPrepared({ site: { pages: [] }, images: { "a.jpg": { width: 1600, height: 1000, src: "/_img/a.jpg", srcset: [] } }, csp: {} });
|
|
384
|
+
expect(renderToStaticMarkup(_jsx(KitSection, { s: { ...block, tone: "dark", background: { src: "a.jpg", alt: "" } } }))).not.toContain("kit-own-shape");
|
|
385
|
+
});
|
|
386
|
+
it("the ready-made comparison, features with icons, logo strip and video", () => {
|
|
387
|
+
const keys = presets.map((p) => p.key);
|
|
388
|
+
for (const k of ["comparison", "features-icons", "logo-strip", "video"])
|
|
389
|
+
expect(keys).toContain(k);
|
|
390
|
+
const comparison = presets.find((p) => p.key === "comparison").block;
|
|
391
|
+
expect(comparison.heading).toBeTruthy();
|
|
392
|
+
expect(comparison.columns[0].parts[0]).toMatchObject({ type: "table", highlight: 2 });
|
|
393
|
+
const features = presets.find((p) => p.key === "features-icons").block;
|
|
394
|
+
expect(features.columns.map((c) => [c.card, c.parts.map((p) => p.type)])).toEqual(Array(3).fill([true, ["icon", "heading", "text"]]));
|
|
395
|
+
});
|
|
396
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caltho/site-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "What every Caltho site is built with: the contract with CalOS (content, and how a site declares what is editable), images, search and security, and the checks",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,13 +49,18 @@
|
|
|
49
49
|
"types": "./dist/sections/index.d.ts",
|
|
50
50
|
"default": "./dist/sections/index.js"
|
|
51
51
|
},
|
|
52
|
-
"./
|
|
52
|
+
"./migrate": {
|
|
53
|
+
"types": "./dist/migrate.d.ts",
|
|
54
|
+
"default": "./dist/migrate.js"
|
|
55
|
+
},
|
|
56
|
+
"./sections.css": "./dist/sections.css",
|
|
57
|
+
"./fonts.css": "./dist/fonts.css"
|
|
53
58
|
},
|
|
54
59
|
"engines": {
|
|
55
60
|
"node": ">=22.18"
|
|
56
61
|
},
|
|
57
62
|
"scripts": {
|
|
58
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp -R src/fonts dist/fonts && cp src/sections.css dist/sections.css && chmod +x dist/cli/index.js",
|
|
63
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/fonts && cp -R src/fonts/. dist/fonts/ && cp src/sections.css dist/sections.css && cp src/fonts.css dist/fonts.css && chmod +x dist/cli/index.js",
|
|
59
64
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
60
65
|
"test": "bun test src",
|
|
61
66
|
"prepack": "bun run build"
|