@caltho/site-kit 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +146 -16
  2. package/dist/behaviors/accordion.d.ts +4 -0
  3. package/dist/behaviors/accordion.js +56 -0
  4. package/dist/behaviors/carousel.d.ts +4 -0
  5. package/dist/behaviors/carousel.js +127 -0
  6. package/dist/behaviors/filter.d.ts +1 -0
  7. package/dist/behaviors/filter.js +25 -0
  8. package/dist/behaviors/index.d.ts +14 -0
  9. package/dist/behaviors/index.js +78 -0
  10. package/dist/behaviors/motion.d.ts +13 -0
  11. package/dist/behaviors/motion.js +88 -0
  12. package/dist/behaviors/tabs.d.ts +3 -0
  13. package/dist/behaviors/tabs.js +55 -0
  14. package/dist/built/css.d.ts +2 -0
  15. package/dist/built/css.js +26 -0
  16. package/dist/built/theme.d.ts +19 -0
  17. package/dist/built/theme.js +86 -0
  18. package/dist/built/tree.d.ts +24 -0
  19. package/dist/built/tree.js +96 -0
  20. package/dist/built.test.d.ts +1 -0
  21. package/dist/built.test.js +257 -0
  22. package/dist/cli/check.js +31 -1
  23. package/dist/consent.d.ts +33 -0
  24. package/dist/consent.js +36 -0
  25. package/dist/consent.test.d.ts +1 -0
  26. package/dist/consent.test.js +61 -0
  27. package/dist/content/index.d.ts +450 -33
  28. package/dist/content/index.js +297 -15
  29. package/dist/content/lists.d.ts +69 -0
  30. package/dist/content/lists.js +77 -0
  31. package/dist/content/lists.test.d.ts +1 -0
  32. package/dist/content/lists.test.js +257 -0
  33. package/dist/content/look.d.ts +260 -9
  34. package/dist/content/look.js +386 -22
  35. package/dist/content-migrate.d.ts +51 -0
  36. package/dist/content-migrate.js +69 -0
  37. package/dist/finish.d.ts +1 -1
  38. package/dist/finish.js +27 -6
  39. package/dist/fonts/families/dm-sans/LICENSE +93 -0
  40. package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-italic.woff2 +0 -0
  41. package/dist/fonts/families/dm-sans/dm-sans-latin-ext-wght-normal.woff2 +0 -0
  42. package/dist/fonts/families/dm-sans/dm-sans-latin-wght-italic.woff2 +0 -0
  43. package/dist/fonts/families/dm-sans/dm-sans-latin-wght-normal.woff2 +0 -0
  44. package/dist/fonts/families/fraunces/LICENSE +93 -0
  45. package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-italic.woff2 +0 -0
  46. package/dist/fonts/families/fraunces/fraunces-latin-ext-wght-normal.woff2 +0 -0
  47. package/dist/fonts/families/fraunces/fraunces-latin-wght-italic.woff2 +0 -0
  48. package/dist/fonts/families/fraunces/fraunces-latin-wght-normal.woff2 +0 -0
  49. package/dist/fonts/families/inter/LICENSE +93 -0
  50. package/dist/fonts/families/inter/inter-latin-ext-wght-italic.woff2 +0 -0
  51. package/dist/fonts/families/inter/inter-latin-ext-wght-normal.woff2 +0 -0
  52. package/dist/fonts/families/inter/inter-latin-wght-italic.woff2 +0 -0
  53. package/dist/fonts/families/inter/inter-latin-wght-normal.woff2 +0 -0
  54. package/dist/fonts/families/lora/LICENSE +93 -0
  55. package/dist/fonts/families/lora/lora-latin-ext-wght-italic.woff2 +0 -0
  56. package/dist/fonts/families/lora/lora-latin-ext-wght-normal.woff2 +0 -0
  57. package/dist/fonts/families/lora/lora-latin-wght-italic.woff2 +0 -0
  58. package/dist/fonts/families/lora/lora-latin-wght-normal.woff2 +0 -0
  59. package/dist/fonts/families/manrope/LICENSE +93 -0
  60. package/dist/fonts/families/manrope/manrope-latin-ext-wght-normal.woff2 +0 -0
  61. package/dist/fonts/families/manrope/manrope-latin-wght-normal.woff2 +0 -0
  62. package/dist/fonts/families/newsreader/LICENSE +93 -0
  63. package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-italic.woff2 +0 -0
  64. package/dist/fonts/families/newsreader/newsreader-latin-ext-wght-normal.woff2 +0 -0
  65. package/dist/fonts/families/newsreader/newsreader-latin-wght-italic.woff2 +0 -0
  66. package/dist/fonts/families/newsreader/newsreader-latin-wght-normal.woff2 +0 -0
  67. package/dist/fonts/families/outfit/LICENSE +93 -0
  68. package/dist/fonts/families/outfit/outfit-latin-ext-wght-normal.woff2 +0 -0
  69. package/dist/fonts/families/outfit/outfit-latin-wght-normal.woff2 +0 -0
  70. package/dist/fonts/families/playfair-display/LICENSE +93 -0
  71. package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-italic.woff2 +0 -0
  72. package/dist/fonts/families/playfair-display/playfair-display-latin-ext-wght-normal.woff2 +0 -0
  73. package/dist/fonts/families/playfair-display/playfair-display-latin-wght-italic.woff2 +0 -0
  74. package/dist/fonts/families/playfair-display/playfair-display-latin-wght-normal.woff2 +0 -0
  75. package/dist/fonts/families/source-serif-4/LICENSE +93 -0
  76. package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
  77. package/dist/fonts/families/source-serif-4/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
  78. package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-italic.woff2 +0 -0
  79. package/dist/fonts/families/source-serif-4/source-serif-4-latin-wght-normal.woff2 +0 -0
  80. package/dist/fonts/families/work-sans/LICENSE +93 -0
  81. package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-italic.woff2 +0 -0
  82. package/dist/fonts/families/work-sans/work-sans-latin-ext-wght-normal.woff2 +0 -0
  83. package/dist/fonts/families/work-sans/work-sans-latin-wght-italic.woff2 +0 -0
  84. package/dist/fonts/families/work-sans/work-sans-latin-wght-normal.woff2 +0 -0
  85. package/dist/fonts/google.json +1 -0
  86. package/dist/fonts-google.d.ts +35 -0
  87. package/dist/fonts-google.js +70 -0
  88. package/dist/fonts.css +365 -0
  89. package/dist/frame.test.d.ts +1 -0
  90. package/dist/frame.test.js +58 -0
  91. package/dist/migrate.d.ts +45 -0
  92. package/dist/migrate.js +284 -0
  93. package/dist/next/block-mark.d.ts +7 -0
  94. package/dist/next/block-mark.js +16 -0
  95. package/dist/next/blocks.d.ts +11 -0
  96. package/dist/next/blocks.js +35 -0
  97. package/dist/next/built.d.ts +40 -0
  98. package/dist/next/built.js +318 -0
  99. package/dist/next/consent.d.ts +4 -0
  100. package/dist/next/consent.js +13 -0
  101. package/dist/next/data.d.ts +6 -2
  102. package/dist/next/data.js +10 -4
  103. package/dist/next/frame.d.ts +42 -0
  104. package/dist/next/frame.js +73 -0
  105. package/dist/next/index.d.ts +8 -2
  106. package/dist/next/index.js +9 -3
  107. package/dist/next/menu-group.d.ts +11 -0
  108. package/dist/next/menu-group.js +33 -0
  109. package/dist/next/metadata.js +3 -2
  110. package/dist/next/parts/Img.d.ts +6 -1
  111. package/dist/next/parts/Img.js +11 -3
  112. package/dist/next/parts/LookStyle.js +3 -1
  113. package/dist/next/preview.js +511 -27
  114. package/dist/next/sections.d.ts +19 -10
  115. package/dist/next/sections.js +134 -17
  116. package/dist/policies.js +22 -4
  117. package/dist/prepare.d.ts +15 -1
  118. package/dist/prepare.js +125 -3
  119. package/dist/preview-match.d.ts +29 -0
  120. package/dist/preview-match.js +58 -1
  121. package/dist/schema.d.ts +10 -0
  122. package/dist/schema.js +29 -1
  123. package/dist/sections/icons.d.ts +49 -0
  124. package/dist/sections/icons.js +55 -0
  125. package/dist/sections/index.d.ts +188 -12
  126. package/dist/sections/index.js +136 -10
  127. package/dist/sections/presets.d.ts +13 -0
  128. package/dist/sections/presets.js +300 -0
  129. package/dist/sections.css +1584 -41
  130. package/dist/sections.test.js +276 -14
  131. package/dist/seo.js +2 -1
  132. package/dist/tree/behaviors.d.ts +50 -0
  133. package/dist/tree/behaviors.js +67 -0
  134. package/dist/tree/classes.d.ts +24 -0
  135. package/dist/tree/classes.js +94 -0
  136. package/dist/tree/index.d.ts +145 -0
  137. package/dist/tree/index.js +196 -0
  138. package/dist/tree/messages.d.ts +45 -0
  139. package/dist/tree/messages.js +7 -0
  140. package/package.json +22 -3
package/dist/cli/check.js CHANGED
@@ -39,7 +39,12 @@ export async function check(dir, o = {}) {
39
39
  for (const [screen, viewport] of [
40
40
  ["computer", { width: 1280, height: 800 }],
41
41
  ["phone", { width: 390, height: 844 }],
42
+ // Sizes in between, looked at for what runs past the screen's edge only.
43
+ ["tablet", { width: 768, height: 1024 }],
44
+ ["small laptop", { width: 1024, height: 768 }],
45
+ ["wide screen", { width: 1600, height: 900 }],
42
46
  ]) {
47
+ const fullCheck = screen === "computer" || screen === "phone";
43
48
  const context = await browser.newContext({ viewport });
44
49
  const page = await context.newPage();
45
50
  let where = "";
@@ -54,6 +59,31 @@ export async function check(dir, o = {}) {
54
59
  for (const path of paths) {
55
60
  where = path;
56
61
  await page.goto(base + path, { waitUntil: "networkidle" });
62
+ // Anything drawn past the screen's edge (words out of a card, a row too wide), unless a box
63
+ // around it clips what it holds on purpose (a slideshow, a scrolling row).
64
+ const over = await page.evaluate(() => {
65
+ const edge = document.documentElement.clientWidth;
66
+ const clipped = (el) => {
67
+ for (let at = el.parentElement; at && at !== document.body; at = at.parentElement) {
68
+ const c = getComputedStyle(at);
69
+ if (c.overflowX !== "visible")
70
+ return true;
71
+ }
72
+ return false;
73
+ };
74
+ return [...document.querySelectorAll("body *")]
75
+ .filter((el) => {
76
+ const r = el.getBoundingClientRect();
77
+ // Across the edge, not wholly off the screen (a field kept out of sight on purpose).
78
+ return r.width > 0 && r.height > 0 && ((r.right > edge + 1 && r.left < edge) || (r.left < -1 && r.right > 0)) && !clipped(el);
79
+ })
80
+ .slice(0, 3)
81
+ .map((el) => `${el.tagName.toLowerCase()}${el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/)[0]}` : ""} "${(el.textContent ?? "").trim().slice(0, 40)}"`);
82
+ });
83
+ if (over.length)
84
+ problems.push(`${path} (${screen}, ${viewport.width}px): runs past the screen's edge: ${over.join(", ")}`);
85
+ if (!fullCheck)
86
+ continue;
57
87
  // Through the browser's devtools channel: the page's own policy (rightly) blocks an injected <script>.
58
88
  await page.evaluate(axeSource);
59
89
  const result = (await page.evaluate(() =>
@@ -75,7 +105,7 @@ export async function check(dir, o = {}) {
75
105
  if (problems.length)
76
106
  log(`✗ ${dir}: ${problems.length} problem${problems.length === 1 ? "" : "s"}${list(problems)}`);
77
107
  else
78
- log(`✓ ${dir}: ${paths.length} pages, on a computer and a phone: nothing blocked or broken`);
108
+ log(`✓ ${dir}: ${paths.length} pages, on a computer and a phone: nothing blocked or broken, nothing past the screen's edge from a phone to a wide screen`);
79
109
  if (found.length)
80
110
  log(`${o.strict ? "✗" : "•"} ${dir}: accessibility, ${found.length} thing${found.length === 1 ? "" : "s"} to look at${o.strict ? "" : " (the site's to weigh; --strict fails on them)"}${list(found)}`);
81
111
  else
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ /** Where the visitor's choice is kept, in their own browser: "yes" or "no". */
3
+ export declare const CONSENT_KEY = "kit-consent";
4
+ /** The setting: whether the site asks, the banner's words and buttons, where it sits, and its design. */
5
+ export declare const consent: z.ZodObject<{
6
+ on: z.ZodDefault<z.ZodBoolean>;
7
+ text: z.ZodDefault<z.ZodString>;
8
+ accept: z.ZodDefault<z.ZodString>;
9
+ decline: z.ZodDefault<z.ZodString>;
10
+ policy: z.ZodOptional<z.ZodObject<{
11
+ label: z.ZodString;
12
+ href: z.ZodString;
13
+ }, z.core.$strip>>;
14
+ place: z.ZodDefault<z.ZodEnum<{
15
+ top: "top";
16
+ bottom: "bottom";
17
+ "bottom-left": "bottom-left";
18
+ "bottom-right": "bottom-right";
19
+ }>>;
20
+ design: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>;
22
+ export type Consent = z.output<typeof consent>;
23
+ /** Whether the site asks before counting. */
24
+ export declare const asksConsent: (site: {
25
+ consent?: unknown;
26
+ }) => boolean;
27
+ /**
28
+ * The banner's script: shows the banner where no choice has been made, keeps the choice when a
29
+ * button is pressed, tells the page (a `kit:consent` event, "yes" or "no"), and shows the banner
30
+ * again from anything marked data-kit-consent="change". Without scripting, the banner stays hidden
31
+ * and nothing is counted.
32
+ */
33
+ export declare function consentScript(): string;
@@ -0,0 +1,36 @@
1
+ // A cookie banner as a site's configuration, not its code (the site's `consent` setting). Off by
2
+ // default: a site that counts its visits does it without cookies (the kit's counter, ./finish.ts),
3
+ // which needs no banner. On, the site asks first: a banner in its words and place, in the kit's
4
+ // design or one of the site's components (a built tree with the slots text, accept, decline and
5
+ // policy), and its visits are counted only once a visitor accepts. The choice is kept in the
6
+ // visitor's browser (localStorage, not a cookie), and any element marked data-kit-consent="change"
7
+ // (a footer link) brings the banner back. The privacy policy says which (./policies.ts).
8
+ //
9
+ // Pure: the schema, the scripts finish puts in each page, and whether a site asks.
10
+ import { z } from "zod";
11
+ /** Where the visitor's choice is kept, in their own browser: "yes" or "no". */
12
+ export const CONSENT_KEY = "kit-consent";
13
+ /** The setting: whether the site asks, the banner's words and buttons, where it sits, and its design. */
14
+ export const consent = z
15
+ .object({
16
+ on: z.boolean().default(false).meta({ title: "Ask before counting visits" }),
17
+ text: z.string().trim().min(1).max(600).default("We'd like to count visits to this site, to see what's useful. Is that all right?").meta({ title: "What it says", "x-ui": "markdown" }),
18
+ accept: z.string().trim().min(1).max(40).default("Accept").meta({ title: "Accept button" }),
19
+ decline: z.string().trim().min(1).max(40).default("Decline").meta({ title: "Decline button" }),
20
+ policy: z.object({ label: z.string().trim().min(1).max(60), href: z.string().regex(/^\/[\w\-./#]*$/) }).optional().meta({ title: "Link to the privacy policy" }),
21
+ place: z.enum(["bottom", "bottom-left", "bottom-right", "top"]).default("bottom").meta({ title: "Where it sits", "x-labels": { bottom: "Along the bottom", "bottom-left": "Bottom left corner", "bottom-right": "Bottom right corner", top: "Along the top" } }),
22
+ /** One of the site's components drawn as the banner, its slots filled (text, accept, decline, policy); the kit's own design without one. */
23
+ design: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/).optional().meta({ title: "Design (a component)" }),
24
+ })
25
+ .meta({ title: "Cookie banner" });
26
+ /** Whether the site asks before counting. */
27
+ export const asksConsent = (site) => !!site.consent?.on;
28
+ /**
29
+ * The banner's script: shows the banner where no choice has been made, keeps the choice when a
30
+ * button is pressed, tells the page (a `kit:consent` event, "yes" or "no"), and shows the banner
31
+ * again from anything marked data-kit-consent="change". Without scripting, the banner stays hidden
32
+ * and nothing is counted.
33
+ */
34
+ export function consentScript() {
35
+ return `(()=>{const K=${JSON.stringify(CONSENT_KEY)};const get=()=>{try{return localStorage.getItem(K)}catch{return null}};const banner=()=>document.querySelector("[data-kit-consent-banner]");const go=()=>{const b=banner();if(b&&!get())b.hidden=false;document.addEventListener("click",(e)=>{const t=e.target.closest&&e.target.closest("[data-kit-consent]");if(!t)return;const v=t.getAttribute("data-kit-consent");if(v==="change"){e.preventDefault();const b=banner();if(b){b.hidden=false;b.querySelector("[data-kit-consent=accept]")?.focus()}return}if(v!=="accept"&&v!=="decline")return;e.preventDefault();const yes=v==="accept"?"yes":"no";try{localStorage.setItem(K,yes)}catch{}const b=banner();if(b)b.hidden=true;dispatchEvent(new CustomEvent("kit:consent",{detail:yes}))})};if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",go,{once:true});else go();})();`;
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "bun:test";
3
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { renderToStaticMarkup } from "react-dom/server";
7
+ import { consentScript } from "./consent.js";
8
+ import { defineSite } from "./content/index.js";
9
+ import { counted, finish } from "./finish.js";
10
+ import { KitConsent } from "./next/consent.js";
11
+ import { setPrepared } from "./next/data.js";
12
+ import { policy } from "./policies.js";
13
+ import { sections } from "./sections/index.js";
14
+ // A cookie banner as configuration: off unless the site turns it on; on, visits are counted only once accepted.
15
+ const base = { url: "https://acme.example", lang: "en", business: { name: "Acme" }, redirects: [], pages: [{ path: "/", title: "Acme", description: "Acme does things.", blocks: [{ type: "gallery", images: [{ src: "a.jpg", alt: "A" }] }] }] };
16
+ const analytics = { client: "5c2bbecb-9956-4f13-a458-9528dd049210", api: "https://analytics-api.caltho.com", script: "https://analytics.caltho.com/op1.js" };
17
+ describe("consent", () => {
18
+ it("is off unless turned on, with its words and place defaulted, and its design one of the site's components", () => {
19
+ const site = defineSite({ ...sections });
20
+ const off = site.parse(base);
21
+ expect(off.site?.consent).toBeUndefined();
22
+ const on = site.parse({ ...base, consent: { on: true } });
23
+ expect(on.site?.consent).toMatchObject({ on: true, accept: "Accept", decline: "Decline", place: "bottom" });
24
+ expect(site.parse({ ...base, consent: { on: true, design: "banner" } }).problems?.join(" ")).toMatch(/no component called banner/);
25
+ });
26
+ it("starts the counter only once accepted, and puts the banner's script in each page", async () => {
27
+ const gated = counted("<html><head></head><body></body></html>", analytics, true);
28
+ expect(gated).not.toContain('<script src=');
29
+ expect(gated).toContain('localStorage.getItem("kit-consent")==="yes"');
30
+ expect(gated).toContain('addEventListener("kit:consent"');
31
+ expect(() => new Function(/<script>([\s\S]*?)<\/script>/.exec(gated)[1])).not.toThrow();
32
+ expect(() => new Function(consentScript())).not.toThrow();
33
+ const root = await mkdtemp(join(tmpdir(), "finish-"));
34
+ await mkdir(join(root, ".site"), { recursive: true });
35
+ await mkdir(join(root, "out"), { recursive: true });
36
+ await writeFile(join(root, ".site", "prepared.json"), JSON.stringify({ site: { ...base, consent: { on: true } }, images: {}, csp: {}, analytics }));
37
+ await writeFile(join(root, "out", "index.html"), "<html><head></head><body></body></html>");
38
+ await finish(root, join(root, "out"), {}, () => { });
39
+ const html = await readFile(join(root, "out", "index.html"), "utf8");
40
+ expect(html).toContain("data-kit-consent-banner");
41
+ expect(html).toContain("script-src 'self' 'sha256-");
42
+ });
43
+ it("draws the banner hidden for the script to show, in the kit's design or the site's component, its buttons marked", () => {
44
+ const components = [{ key: "banner", name: "Banner", root: { id: "ban0000001", tag: "div", class: "p-4 bg-ink", children: [{ id: "txt0000001", tag: "p", slot: "text", text: "x" }, { id: "yes0000001", tag: "button", slot: "accept", text: "x" }, { id: "no00000001", tag: "button", slot: "decline", text: "x" }] } }];
45
+ setPrepared({ site: { ...base, components }, images: {}, csp: {} }, false);
46
+ const kit = renderToStaticMarkup(_jsx(KitConsent, { site: { consent: { on: true, text: "Count **visits**?", accept: "OK", decline: "No thanks", place: "bottom-right", policy: { label: "Privacy", href: "/privacy/" } } } }));
47
+ expect(kit).toContain('class="kit-consent kit-consent-bottom-right" data-kit-consent-banner="" role="region" aria-label="Cookie choices" hidden=""');
48
+ expect(kit).toContain("Count <strong>visits</strong>?");
49
+ expect(kit).toContain('data-kit-consent="accept">OK</button>');
50
+ expect(kit).toContain('<a class="kit-consent-policy" href="/privacy/">Privacy</a>');
51
+ const own = renderToStaticMarkup(_jsx(KitConsent, { site: { consent: { on: true, text: "Cookies?", accept: "Yes", decline: "No", place: "bottom", design: "banner" } } }));
52
+ expect(own).toContain('<p>Cookies?</p><button data-kit-consent="accept" type="button">Yes</button><button data-kit-consent="decline" type="button">No</button>');
53
+ expect(renderToStaticMarkup(_jsx(KitConsent, { site: {} }))).toBe("");
54
+ });
55
+ it("says in the privacy policy that visits are counted only with agreement", () => {
56
+ const asks = policy({ ...base, countsVisits: true, consent: { on: true } }, "privacy").body;
57
+ expect(asks).toContain("only if you agree when it asks");
58
+ expect(asks).not.toContain("doesn't use cookies");
59
+ expect(policy({ ...base, countsVisits: true }, "privacy").body).toContain("This site doesn't use cookies or advertising trackers.");
60
+ });
61
+ });