@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.
- package/README.md +146 -16
- package/dist/behaviors/accordion.d.ts +4 -0
- package/dist/behaviors/accordion.js +56 -0
- package/dist/behaviors/carousel.d.ts +4 -0
- package/dist/behaviors/carousel.js +127 -0
- package/dist/behaviors/filter.d.ts +1 -0
- package/dist/behaviors/filter.js +25 -0
- package/dist/behaviors/index.d.ts +14 -0
- package/dist/behaviors/index.js +78 -0
- package/dist/behaviors/motion.d.ts +13 -0
- package/dist/behaviors/motion.js +88 -0
- package/dist/behaviors/tabs.d.ts +3 -0
- package/dist/behaviors/tabs.js +55 -0
- package/dist/built/css.d.ts +2 -0
- package/dist/built/css.js +26 -0
- package/dist/built/theme.d.ts +19 -0
- package/dist/built/theme.js +86 -0
- package/dist/built/tree.d.ts +24 -0
- package/dist/built/tree.js +96 -0
- package/dist/built.test.d.ts +1 -0
- package/dist/built.test.js +257 -0
- package/dist/cli/check.js +31 -1
- package/dist/consent.d.ts +33 -0
- package/dist/consent.js +36 -0
- package/dist/consent.test.d.ts +1 -0
- package/dist/consent.test.js +61 -0
- package/dist/content/index.d.ts +450 -33
- package/dist/content/index.js +297 -15
- package/dist/content/lists.d.ts +69 -0
- package/dist/content/lists.js +77 -0
- package/dist/content/lists.test.d.ts +1 -0
- package/dist/content/lists.test.js +257 -0
- package/dist/content/look.d.ts +260 -9
- package/dist/content/look.js +386 -22
- package/dist/content-migrate.d.ts +51 -0
- package/dist/content-migrate.js +69 -0
- package/dist/finish.d.ts +1 -1
- package/dist/finish.js +27 -6
- 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 +58 -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/built.d.ts +40 -0
- package/dist/next/built.js +318 -0
- package/dist/next/consent.d.ts +4 -0
- package/dist/next/consent.js +13 -0
- package/dist/next/data.d.ts +6 -2
- package/dist/next/data.js +10 -4
- package/dist/next/frame.d.ts +42 -0
- package/dist/next/frame.js +73 -0
- package/dist/next/index.d.ts +8 -2
- package/dist/next/index.js +9 -3
- package/dist/next/menu-group.d.ts +11 -0
- package/dist/next/menu-group.js +33 -0
- package/dist/next/metadata.js +3 -2
- package/dist/next/parts/Img.d.ts +6 -1
- package/dist/next/parts/Img.js +11 -3
- package/dist/next/parts/LookStyle.js +3 -1
- package/dist/next/preview.js +511 -27
- package/dist/next/sections.d.ts +19 -10
- package/dist/next/sections.js +134 -17
- package/dist/policies.js +22 -4
- package/dist/prepare.d.ts +15 -1
- package/dist/prepare.js +125 -3
- package/dist/preview-match.d.ts +29 -0
- package/dist/preview-match.js +58 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +29 -1
- package/dist/sections/icons.d.ts +49 -0
- package/dist/sections/icons.js +55 -0
- package/dist/sections/index.d.ts +188 -12
- package/dist/sections/index.js +136 -10
- package/dist/sections/presets.d.ts +13 -0
- package/dist/sections/presets.js +300 -0
- package/dist/sections.css +1584 -41
- package/dist/sections.test.js +276 -14
- package/dist/seo.js +2 -1
- package/dist/tree/behaviors.d.ts +50 -0
- package/dist/tree/behaviors.js +67 -0
- package/dist/tree/classes.d.ts +24 -0
- package/dist/tree/classes.js +94 -0
- package/dist/tree/index.d.ts +145 -0
- package/dist/tree/index.js +196 -0
- package/dist/tree/messages.d.ts +45 -0
- package/dist/tree/messages.js +7 -0
- package/package.json +22 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { behavior } from "./behaviors.ts";
|
|
3
|
+
export { checkClasses, classProblem, classTokens, safeClasses, splitVariants, VARIANTS, type ClassCheck } from "./classes.ts";
|
|
4
|
+
export { behavior, BEHAVIORS, type Behavior, type BehaviorName } from "./behaviors.ts";
|
|
5
|
+
export * from "./messages.ts";
|
|
6
|
+
/** The elements a tree may hold: what a page's sections are made of, never script, style or embeds. */
|
|
7
|
+
export declare const TAGS: readonly ["section", "div", "header", "footer", "nav", "article", "aside", "h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "strong", "em", "small", "a", "button", "ul", "ol", "li", "figure", "figcaption", "img", "blockquote", "hr", "br", "icon", "video", "form", "label", "input", "textarea", "select", "option"];
|
|
8
|
+
export type Tag = (typeof TAGS)[number];
|
|
9
|
+
/** Elements that hold no children and no words. */
|
|
10
|
+
export declare const VOID_TAGS: readonly Tag[];
|
|
11
|
+
/** A form's fields: an input, a longer one, a list of choices. Each only inside a form. */
|
|
12
|
+
export declare const FIELD_TAGS: readonly Tag[];
|
|
13
|
+
/** What a field holds: the kinds of input a visitor fills in or picks. */
|
|
14
|
+
export declare const INPUT_KINDS: readonly ["text", "email", "tel", "number", "date", "url", "checkbox", "radio"];
|
|
15
|
+
/**
|
|
16
|
+
* A form field's own settings: the name its answer is sent as (CalOS takes each by its name: email,
|
|
17
|
+
* phone, name, message, or a field's key), what it holds, whether it must be filled, the words shown
|
|
18
|
+
* while it's empty, and a radio's or checkbox's value. Never a name starting _ (the kit's own: its spam trap).
|
|
19
|
+
*/
|
|
20
|
+
export declare const fieldSpec: z.ZodObject<{
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
number: "number";
|
|
24
|
+
date: "date";
|
|
25
|
+
text: "text";
|
|
26
|
+
email: "email";
|
|
27
|
+
tel: "tel";
|
|
28
|
+
url: "url";
|
|
29
|
+
checkbox: "checkbox";
|
|
30
|
+
radio: "radio";
|
|
31
|
+
}>>;
|
|
32
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
placeholder: z.ZodOptional<z.ZodString>;
|
|
34
|
+
value: z.ZodOptional<z.ZodString>;
|
|
35
|
+
rows: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
checked: z.ZodOptional<z.ZodBoolean>;
|
|
37
|
+
}, z.core.$strict>;
|
|
38
|
+
export type FieldSpec = z.infer<typeof fieldSpec>;
|
|
39
|
+
/** How deep a tree goes, how many nodes it has, how long a node's classes and words are. */
|
|
40
|
+
export declare const LIMITS: {
|
|
41
|
+
readonly depth: 16;
|
|
42
|
+
readonly nodes: 600;
|
|
43
|
+
readonly classLength: 1200;
|
|
44
|
+
readonly textLength: 4000;
|
|
45
|
+
};
|
|
46
|
+
/** A node's stable id: made by CalOS when a node is added, kept as it moves; the preview reports clicks by it. */
|
|
47
|
+
export declare const nodeId: z.ZodString;
|
|
48
|
+
/** Where a link goes: a page of the site, a part of a page, or another site over https. Nothing else (no javascript:, data:). */
|
|
49
|
+
export declare const safeHref: z.ZodString;
|
|
50
|
+
export declare const imageRef: z.ZodObject<{
|
|
51
|
+
src: z.ZodString;
|
|
52
|
+
alt: z.ZodDefault<z.ZodString>;
|
|
53
|
+
phoneSrc: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
export type ImageRef = z.infer<typeof imageRef>;
|
|
56
|
+
/**
|
|
57
|
+
* Shared content a node reads: a dotted path into the site's content ("company.email",
|
|
58
|
+
* "caseStudies"). With `repeat`, a list whose items the node is copied for; inside it, `bind` paths
|
|
59
|
+
* starting with "item." read the item ("item.name").
|
|
60
|
+
*/
|
|
61
|
+
export declare const contentPath: z.ZodString;
|
|
62
|
+
export type Node = {
|
|
63
|
+
id: string;
|
|
64
|
+
tag: Tag;
|
|
65
|
+
/** Tailwind classes (classes.ts says which are allowed). */
|
|
66
|
+
class?: string;
|
|
67
|
+
/** Its words, light Markdown (bold, italic, links). Not on void tags. */
|
|
68
|
+
text?: string;
|
|
69
|
+
href?: string;
|
|
70
|
+
/** Opens in a new tab (an outside link). */
|
|
71
|
+
external?: boolean;
|
|
72
|
+
image?: ImageRef;
|
|
73
|
+
/** icon: a name from the kit's icon set (../sections/icons.ts). */
|
|
74
|
+
icon?: string;
|
|
75
|
+
/** video: a YouTube or Vimeo video by its id; the kit builds the embed's address itself. */
|
|
76
|
+
video?: {
|
|
77
|
+
provider: "youtube" | "vimeo";
|
|
78
|
+
id: string;
|
|
79
|
+
};
|
|
80
|
+
/** What it's called in the editor's layers ("Slide", "Card"); its tag otherwise. */
|
|
81
|
+
name?: string;
|
|
82
|
+
/** Shows a shared value in place of `text`, `href` or `image` (whichever the value is). */
|
|
83
|
+
bind?: {
|
|
84
|
+
path: string;
|
|
85
|
+
as?: "text" | "href" | "image";
|
|
86
|
+
};
|
|
87
|
+
/** One copy of this node per item of a shared list; inside, binds start with "item.". */
|
|
88
|
+
repeat?: {
|
|
89
|
+
path: string;
|
|
90
|
+
limit?: number;
|
|
91
|
+
};
|
|
92
|
+
behavior?: z.infer<typeof behavior>;
|
|
93
|
+
/** A slot of a component, filled where it's used ({ type: "use" } blocks). */
|
|
94
|
+
slot?: string;
|
|
95
|
+
/** Hidden on the page, kept in the tree (a draft part). */
|
|
96
|
+
hidden?: boolean;
|
|
97
|
+
/** Accessibility: its label where its words don't say what it is (an icon button). */
|
|
98
|
+
label?: string;
|
|
99
|
+
/** A form field's settings (input, textarea, select). */
|
|
100
|
+
field?: FieldSpec;
|
|
101
|
+
/** A label: the field it names, by its id (in the same tree). */
|
|
102
|
+
for?: string;
|
|
103
|
+
/** A form's: the business's form it sends to, by key. */
|
|
104
|
+
sendTo?: string;
|
|
105
|
+
children?: Node[];
|
|
106
|
+
};
|
|
107
|
+
export declare const node: z.ZodType<Node>;
|
|
108
|
+
/** Problems with a whole tree beyond each node's own: depth, size, ids used twice. */
|
|
109
|
+
export declare function treeProblems(root: Node): string[];
|
|
110
|
+
/** The section: a tree, as a block of a page. */
|
|
111
|
+
export declare const built: z.ZodObject<{
|
|
112
|
+
name: z.ZodOptional<z.ZodString>;
|
|
113
|
+
root: z.ZodType<Node, unknown, z.core.$ZodTypeInternals<Node, unknown>>;
|
|
114
|
+
}, z.core.$strip>;
|
|
115
|
+
/** A tree kept once and used by name, its slots filled where it's used. */
|
|
116
|
+
export declare const component: z.ZodObject<{
|
|
117
|
+
key: z.ZodString;
|
|
118
|
+
name: z.ZodString;
|
|
119
|
+
root: z.ZodType<Node, unknown, z.core.$ZodTypeInternals<Node, unknown>>;
|
|
120
|
+
}, z.core.$strip>;
|
|
121
|
+
export type Component = z.infer<typeof component>;
|
|
122
|
+
/** A use of a component: its slots' words, links and pictures. */
|
|
123
|
+
export declare const use: z.ZodObject<{
|
|
124
|
+
component: z.ZodString;
|
|
125
|
+
slots: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
126
|
+
text: z.ZodOptional<z.ZodString>;
|
|
127
|
+
href: z.ZodOptional<z.ZodString>;
|
|
128
|
+
image: z.ZodOptional<z.ZodObject<{
|
|
129
|
+
src: z.ZodString;
|
|
130
|
+
alt: z.ZodDefault<z.ZodString>;
|
|
131
|
+
phoneSrc: z.ZodOptional<z.ZodString>;
|
|
132
|
+
}, z.core.$strip>>;
|
|
133
|
+
}, z.core.$strip>>>;
|
|
134
|
+
}, z.core.$strip>;
|
|
135
|
+
/** Every node of a tree, depth first, with its parent and place. */
|
|
136
|
+
export declare function walk(root: Node, parent?: Node | null, index?: number): Generator<{
|
|
137
|
+
node: Node;
|
|
138
|
+
parent: Node | null;
|
|
139
|
+
index: number;
|
|
140
|
+
depth: number;
|
|
141
|
+
}>;
|
|
142
|
+
/** The node with this id, if the tree has it. */
|
|
143
|
+
export declare function findNode(root: Node, id: string): Node | null;
|
|
144
|
+
/** A new node id, as CalOS makes them. */
|
|
145
|
+
export declare function newNodeId(): string;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// A section built in CalOS as a tree of elements styled with Tailwind (cal-os docs/design/site-builder.md):
|
|
2
|
+
// anything a designer builds with HTML and Tailwind, a business (or Cal for it) builds and changes
|
|
3
|
+
// without code. This file is the contract every side keeps to: the kit draws a tree (server and
|
|
4
|
+
// preview), CalOS edits it, Cal proposes changes to it, and all of them check it the same way.
|
|
5
|
+
//
|
|
6
|
+
// A tree is content like any other block: { type: "built", root: Node }. A component is a tree kept
|
|
7
|
+
// once in the site's content (`components`) and used by name with its slots filled.
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { checkClasses } from "./classes.js";
|
|
10
|
+
import { behavior } from "./behaviors.js";
|
|
11
|
+
export { checkClasses, classProblem, classTokens, safeClasses, splitVariants, VARIANTS } from "./classes.js";
|
|
12
|
+
export { behavior, BEHAVIORS } from "./behaviors.js";
|
|
13
|
+
export * from "./messages.js";
|
|
14
|
+
/** The elements a tree may hold: what a page's sections are made of, never script, style or embeds. */
|
|
15
|
+
export const TAGS = ["section", "div", "header", "footer", "nav", "article", "aside", "h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "strong", "em", "small", "a", "button", "ul", "ol", "li", "figure", "figcaption", "img", "blockquote", "hr", "br", "icon", "video", "form", "label", "input", "textarea", "select", "option"];
|
|
16
|
+
/** Elements that hold no children and no words. */
|
|
17
|
+
export const VOID_TAGS = ["img", "hr", "br", "icon", "video", "input", "textarea"];
|
|
18
|
+
/** A form's fields: an input, a longer one, a list of choices. Each only inside a form. */
|
|
19
|
+
export const FIELD_TAGS = ["input", "textarea", "select"];
|
|
20
|
+
/** What a field holds: the kinds of input a visitor fills in or picks. */
|
|
21
|
+
export const INPUT_KINDS = ["text", "email", "tel", "number", "date", "url", "checkbox", "radio"];
|
|
22
|
+
/**
|
|
23
|
+
* A form field's own settings: the name its answer is sent as (CalOS takes each by its name: email,
|
|
24
|
+
* phone, name, message, or a field's key), what it holds, whether it must be filled, the words shown
|
|
25
|
+
* while it's empty, and a radio's or checkbox's value. Never a name starting _ (the kit's own: its spam trap).
|
|
26
|
+
*/
|
|
27
|
+
export const fieldSpec = z
|
|
28
|
+
.object({
|
|
29
|
+
name: z.string().regex(/^[a-z][a-z0-9_]{0,39}$/, { error: "A field's name is lowercase letters, numbers and _, like email or best_time" }),
|
|
30
|
+
type: z.enum(INPUT_KINDS).optional(),
|
|
31
|
+
required: z.boolean().optional(),
|
|
32
|
+
placeholder: z.string().max(120).optional(),
|
|
33
|
+
value: z.string().max(120).optional(),
|
|
34
|
+
rows: z.number().int().min(2).max(20).optional(),
|
|
35
|
+
/** A tick box or a choice (radio): chosen at first. */
|
|
36
|
+
checked: z.boolean().optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
/** How deep a tree goes, how many nodes it has, how long a node's classes and words are. */
|
|
40
|
+
export const LIMITS = { depth: 16, nodes: 600, classLength: 1200, textLength: 4000 };
|
|
41
|
+
/** A node's stable id: made by CalOS when a node is added, kept as it moves; the preview reports clicks by it. */
|
|
42
|
+
export const nodeId = z.string().regex(/^[a-z0-9]{6,24}$/);
|
|
43
|
+
/** Where a link goes: a page of the site, a part of a page, or another site over https. Nothing else (no javascript:, data:). */
|
|
44
|
+
export const safeHref = z
|
|
45
|
+
.string()
|
|
46
|
+
.max(500)
|
|
47
|
+
.refine((h) => /^\/(?!\/)[^\s]*$/.test(h) || /^#[\w-]+$/.test(h) || /^https?:\/\/[^\s]+$/i.test(h) || /^(mailto|tel):[^\s]+$/i.test(h), "A page path, #part, http(s) address, mailto: or tel:");
|
|
48
|
+
/** A picture: one of the site's own (uploads/… or its own public files), never another site's address. */
|
|
49
|
+
const ownPicture = z.string().max(500).refine((s) => !/^[a-z][a-z0-9+.-]*:/i.test(s) && !s.startsWith("//"), "One of the site's own pictures");
|
|
50
|
+
export const imageRef = z.object({
|
|
51
|
+
src: ownPicture,
|
|
52
|
+
alt: z.string().max(300).default(""),
|
|
53
|
+
/** Another picture of the same thing for phones (a tall screenshot of the phone layout), shown on small screens in its place. */
|
|
54
|
+
phoneSrc: ownPicture.optional(),
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Shared content a node reads: a dotted path into the site's content ("company.email",
|
|
58
|
+
* "caseStudies"). With `repeat`, a list whose items the node is copied for; inside it, `bind` paths
|
|
59
|
+
* starting with "item." read the item ("item.name").
|
|
60
|
+
*/
|
|
61
|
+
export const contentPath = z.string().regex(/^(item\.)?[a-zA-Z][\w]*(\.[a-zA-Z0-9_]+)*$/).max(120);
|
|
62
|
+
const classes = z
|
|
63
|
+
.string()
|
|
64
|
+
.max(LIMITS.classLength)
|
|
65
|
+
.superRefine((c, ctx) => {
|
|
66
|
+
const r = checkClasses(c);
|
|
67
|
+
if (!r.ok)
|
|
68
|
+
ctx.addIssue({ code: "custom", message: `Not allowed: ${r.bad.map((b) => `${b.token} (${b.why})`).join(", ")}` });
|
|
69
|
+
});
|
|
70
|
+
export const node = z.lazy(() => z
|
|
71
|
+
.object({
|
|
72
|
+
id: nodeId,
|
|
73
|
+
tag: z.enum(TAGS),
|
|
74
|
+
class: classes.optional(),
|
|
75
|
+
text: z.string().max(LIMITS.textLength).optional(),
|
|
76
|
+
href: safeHref.optional(),
|
|
77
|
+
external: z.boolean().optional(),
|
|
78
|
+
image: imageRef.optional(),
|
|
79
|
+
icon: z.string().max(60).optional(),
|
|
80
|
+
video: z.object({ provider: z.enum(["youtube", "vimeo"]), id: z.string().regex(/^[\w-]{6,20}$/) }).strict().optional(),
|
|
81
|
+
name: z.string().max(60).optional(),
|
|
82
|
+
bind: z.object({ path: contentPath, as: z.enum(["text", "href", "image"]).optional() }).optional(),
|
|
83
|
+
repeat: z.object({ path: contentPath, limit: z.number().int().min(1).max(100).optional() }).optional(),
|
|
84
|
+
behavior: behavior.optional(),
|
|
85
|
+
slot: z.string().regex(/^[a-z][a-z0-9_]{0,39}$/).optional(),
|
|
86
|
+
hidden: z.boolean().optional(),
|
|
87
|
+
label: z.string().max(200).optional(),
|
|
88
|
+
field: fieldSpec.optional(),
|
|
89
|
+
for: nodeId.optional(),
|
|
90
|
+
/** A form's: which of the business's forms it sends to, by its key (contact), never an address. */
|
|
91
|
+
sendTo: z.string().regex(/^[a-z0-9][a-z0-9-]{0,59}$/, { error: "A form's key is lowercase letters, numbers and -, like contact" }).optional(),
|
|
92
|
+
children: z.array(node).max(200).optional(),
|
|
93
|
+
})
|
|
94
|
+
.strict()
|
|
95
|
+
.superRefine((n, ctx) => {
|
|
96
|
+
if (VOID_TAGS.includes(n.tag) && (n.children?.length || n.text))
|
|
97
|
+
ctx.addIssue({ code: "custom", message: `A ${n.tag} holds no words or children` });
|
|
98
|
+
if (n.tag === "img" && !n.image && !n.bind)
|
|
99
|
+
ctx.addIssue({ code: "custom", message: "A picture needs its picture" });
|
|
100
|
+
if (n.tag === "icon" && !n.icon)
|
|
101
|
+
ctx.addIssue({ code: "custom", message: "An icon needs its name" });
|
|
102
|
+
if (FIELD_TAGS.includes(n.tag) && !n.field)
|
|
103
|
+
ctx.addIssue({ code: "custom", message: `A ${n.tag === "select" ? "list of choices" : "field"} needs its name (field.name)` });
|
|
104
|
+
if (n.field && !FIELD_TAGS.includes(n.tag))
|
|
105
|
+
ctx.addIssue({ code: "custom", message: "Only a form's fields (input, textarea, select) have field settings" });
|
|
106
|
+
if (n.sendTo && n.tag !== "form")
|
|
107
|
+
ctx.addIssue({ code: "custom", message: "Only a form sends to one of the business's forms (sendTo)" });
|
|
108
|
+
if (n.for && n.tag !== "label")
|
|
109
|
+
ctx.addIssue({ code: "custom", message: "Only a label names a field (for)" });
|
|
110
|
+
if (n.tag === "select" && n.children?.some((c) => c.tag !== "option"))
|
|
111
|
+
ctx.addIssue({ code: "custom", message: "A list of choices holds only its choices (option)" });
|
|
112
|
+
}));
|
|
113
|
+
/** Problems with a whole tree beyond each node's own: depth, size, ids used twice. */
|
|
114
|
+
export function treeProblems(root) {
|
|
115
|
+
const out = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
let count = 0;
|
|
118
|
+
const fields = new Set();
|
|
119
|
+
const labels = [];
|
|
120
|
+
const walk = (n, depth, inForm = false) => {
|
|
121
|
+
count++;
|
|
122
|
+
if (depth > LIMITS.depth)
|
|
123
|
+
out.push(`Nested more than ${LIMITS.depth} deep at ${n.id}`);
|
|
124
|
+
// A form's parts belong in a form, and a form in no other.
|
|
125
|
+
if (n.tag === "form" && inForm)
|
|
126
|
+
out.push(`A form can't be inside another form (${n.id})`);
|
|
127
|
+
if ((FIELD_TAGS.includes(n.tag) || n.tag === "option") && !inForm && n.tag !== "option")
|
|
128
|
+
out.push(`A field belongs inside a form (${n.id})`);
|
|
129
|
+
if (FIELD_TAGS.includes(n.tag))
|
|
130
|
+
fields.add(n.id);
|
|
131
|
+
if (n.tag === "label" && n.for)
|
|
132
|
+
labels.push({ id: n.id, for: n.for });
|
|
133
|
+
if (seen.has(n.id))
|
|
134
|
+
out.push(`The id ${n.id} is used twice`);
|
|
135
|
+
seen.add(n.id);
|
|
136
|
+
for (const c of n.children ?? [])
|
|
137
|
+
walk(c, depth + 1, inForm || n.tag === "form");
|
|
138
|
+
};
|
|
139
|
+
walk(root, 1);
|
|
140
|
+
for (const l of labels)
|
|
141
|
+
if (!fields.has(l.for))
|
|
142
|
+
out.push(`The label ${l.id} names ${l.for}, which isn't a field of this tree`);
|
|
143
|
+
if (count > LIMITS.nodes)
|
|
144
|
+
out.push(`More than ${LIMITS.nodes} elements`);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** The section: a tree, as a block of a page. */
|
|
148
|
+
export const built = z
|
|
149
|
+
.object({
|
|
150
|
+
name: z.string().max(80).optional().meta({ title: "Name" }),
|
|
151
|
+
root: node,
|
|
152
|
+
})
|
|
153
|
+
.superRefine((b, ctx) => {
|
|
154
|
+
for (const p of treeProblems(b.root))
|
|
155
|
+
ctx.addIssue({ code: "custom", message: p });
|
|
156
|
+
})
|
|
157
|
+
.meta({ title: "Built section", description: "Built from elements and Tailwind classes, in the editor or by Cal" });
|
|
158
|
+
/** A tree kept once and used by name, its slots filled where it's used. */
|
|
159
|
+
export const component = z.object({
|
|
160
|
+
key: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/),
|
|
161
|
+
name: z.string().max(80),
|
|
162
|
+
root: node,
|
|
163
|
+
});
|
|
164
|
+
/** A use of a component: its slots' words, links and pictures. */
|
|
165
|
+
export const use = z
|
|
166
|
+
.object({
|
|
167
|
+
component: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/),
|
|
168
|
+
slots: z.record(z.string(), z.object({ text: z.string().max(LIMITS.textLength).optional(), href: safeHref.optional(), image: imageRef.optional() })).default({}),
|
|
169
|
+
})
|
|
170
|
+
.meta({ title: "Component", description: "One of the site's own components, its words and pictures filled in" });
|
|
171
|
+
/** Every node of a tree, depth first, with its parent and place. */
|
|
172
|
+
export function* walk(root, parent = null, index = 0) {
|
|
173
|
+
const stack = [{ node: root, parent, index, depth: 0 }];
|
|
174
|
+
while (stack.length) {
|
|
175
|
+
const at = stack.pop();
|
|
176
|
+
yield at;
|
|
177
|
+
const kids = at.node.children ?? [];
|
|
178
|
+
for (let i = kids.length - 1; i >= 0; i--)
|
|
179
|
+
stack.push({ node: kids[i], parent: at.node, index: i, depth: at.depth + 1 });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** The node with this id, if the tree has it. */
|
|
183
|
+
export function findNode(root, id) {
|
|
184
|
+
for (const { node: n } of walk(root))
|
|
185
|
+
if (n.id === id)
|
|
186
|
+
return n;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/** A new node id, as CalOS makes them. */
|
|
190
|
+
export function newNodeId() {
|
|
191
|
+
const a = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
192
|
+
let s = "";
|
|
193
|
+
for (let i = 0; i < 10; i++)
|
|
194
|
+
s += a[Math.floor(Math.random() * a.length)];
|
|
195
|
+
return s;
|
|
196
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** From the preview to CalOS. */
|
|
2
|
+
export type PreviewToEditor =
|
|
3
|
+
/** An element of a built section was clicked: its block's place on the page and its node's id. */
|
|
4
|
+
{
|
|
5
|
+
type: "calos:built-select";
|
|
6
|
+
block: number;
|
|
7
|
+
node: string;
|
|
8
|
+
shift: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The pointer is over an element (for the editor to outline it in its layers). */
|
|
11
|
+
| {
|
|
12
|
+
type: "calos:built-hover";
|
|
13
|
+
block: number;
|
|
14
|
+
node: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** Where each built element sits on screen, for the editor's overlays (handles, spacing guides). */
|
|
17
|
+
| {
|
|
18
|
+
type: "calos:built-boxes";
|
|
19
|
+
block: number;
|
|
20
|
+
boxes: {
|
|
21
|
+
node: string;
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
w: number;
|
|
25
|
+
h: number;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
/** From CalOS to the preview. */
|
|
29
|
+
export type EditorToPreview =
|
|
30
|
+
/** Outline these nodes (the selection), scrolled into view. */
|
|
31
|
+
{
|
|
32
|
+
type: "calos:built-highlight";
|
|
33
|
+
block: number;
|
|
34
|
+
nodes: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Draw a node as hovered or focused, while its hover or focus classes are edited. */
|
|
37
|
+
| {
|
|
38
|
+
type: "calos:built-state";
|
|
39
|
+
block: number;
|
|
40
|
+
node: string;
|
|
41
|
+
state: "normal" | "hover" | "focus";
|
|
42
|
+
};
|
|
43
|
+
export declare const isPreviewToEditor: (m: unknown) => m is PreviewToEditor;
|
|
44
|
+
/** The attribute that marks a built element with its node's id in the preview. */
|
|
45
|
+
export declare const NODE_ATTR = "data-node";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// What CalOS's editor and the site's preview page (/calos-preview) say to each other about built
|
|
2
|
+
// sections, by postMessage. The preview draws a tree with each element marked by its node's id
|
|
3
|
+
// (data-node, in the preview only), so a click is never guessed from its words, as it is for a coded
|
|
4
|
+
// section (../preview-match.ts).
|
|
5
|
+
export const isPreviewToEditor = (m) => !!m && typeof m === "object" && typeof m.type === "string" && m.type.startsWith("calos:built-");
|
|
6
|
+
/** The attribute that marks a built element with its node's id in the preview. */
|
|
7
|
+
export const NODE_ATTR = "data-node";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caltho/site-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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,22 +49,41 @@
|
|
|
49
49
|
"types": "./dist/sections/index.d.ts",
|
|
50
50
|
"default": "./dist/sections/index.js"
|
|
51
51
|
},
|
|
52
|
-
"./
|
|
52
|
+
"./tree": {
|
|
53
|
+
"types": "./dist/tree/index.d.ts",
|
|
54
|
+
"default": "./dist/tree/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./content-migrate": {
|
|
57
|
+
"types": "./dist/content-migrate.d.ts",
|
|
58
|
+
"default": "./dist/content-migrate.js"
|
|
59
|
+
},
|
|
60
|
+
"./theme": {
|
|
61
|
+
"types": "./dist/built/theme.d.ts",
|
|
62
|
+
"default": "./dist/built/theme.js"
|
|
63
|
+
},
|
|
64
|
+
"./migrate": {
|
|
65
|
+
"types": "./dist/migrate.d.ts",
|
|
66
|
+
"default": "./dist/migrate.js"
|
|
67
|
+
},
|
|
68
|
+
"./sections.css": "./dist/sections.css",
|
|
69
|
+
"./fonts.css": "./dist/fonts.css"
|
|
53
70
|
},
|
|
54
71
|
"engines": {
|
|
55
72
|
"node": ">=22.18"
|
|
56
73
|
},
|
|
57
74
|
"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",
|
|
75
|
+
"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
76
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
60
77
|
"test": "bun test src",
|
|
61
78
|
"prepack": "bun run build"
|
|
62
79
|
},
|
|
63
80
|
"dependencies": {
|
|
64
81
|
"@resvg/resvg-js": "2.6.2",
|
|
82
|
+
"@tailwindcss/browser": "4.3.3",
|
|
65
83
|
"marked": "18.1.0",
|
|
66
84
|
"satori": "0.33.4",
|
|
67
85
|
"sharp": "0.35.5",
|
|
86
|
+
"tailwindcss": "4.3.3",
|
|
68
87
|
"zod": "4.6.7"
|
|
69
88
|
},
|
|
70
89
|
"peerDependencies": {
|