@caltho/site-kit 0.2.9 → 0.2.10

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 CHANGED
@@ -74,6 +74,12 @@ them in its editor like the site's own, and Cal can add one. Today: **gallery**
74
74
  each with an optional caption, opening its largest copy; 2 to 4 across, square, landscape, portrait
75
75
  or as taken) and **slideshow** (photos one at a time, fading from one to the next, with captions).
76
76
 
77
+ **Your own section** (`own`) is built in CalOS rather than in code: one to four columns (or a wide one
78
+ beside a narrow one), each a list of parts (heading, text, photo, button, list, quote, facts in a row,
79
+ price, photos in a grid, slideshow, space), left or centred, plain or in the brand colour. Its parts
80
+ wear the site's own classes, so what's built looks like the site. One that's wanted again is saved by
81
+ name in the content's `saved` list, and CalOS offers it beside the site's own kinds.
82
+
77
83
  `Slideshow` (from `@caltho/site-kit/next`) is also a building block a site's own sections use: SCV's
78
84
  hero moves through its photos behind its words with `<Slideshow fill label="…">{photos}</Slideshow>`.
79
85
  It waits while it's off screen, pointed at or focused, never moves for someone who's asked for less
@@ -96,7 +102,7 @@ import "@caltho/site-kit/sections.css";
96
102
 
97
103
  From then on each section the kit adds reaches the site with `cal sites kit update`. They're themed by
98
104
  the site's Look (`--look-*`); a site with a design of its own gives its own classes so they sit in
99
- it like its own: `<KitSection s={block} classes={{ section: "", wrap: "mx-auto max-w-5xl px-4 py-12", heading: "font-display text-3xl" }} />`.
105
+ it like its own: `<KitSection s={block} classes={{ section: "", wrap: "mx-auto max-w-5xl px-4 py-12", heading: "font-display text-3xl", text: "…", button: "…", buttonQuiet: "…" }} />`.
100
106
 
101
107
  ## Share cards
102
108
 
@@ -242,6 +242,10 @@ export declare function defineSite<const B extends Record<string, z.ZodObject>>(
242
242
  shareText: z.ZodOptional<z.ZodString>;
243
243
  noindex: z.ZodDefault<z.ZodBoolean>;
244
244
  }, z.core.$strip>>;
245
+ saved: z.ZodDefault<z.ZodArray<z.ZodObject<{
246
+ name: z.ZodString;
247
+ block: z.ZodDiscriminatedUnion<[z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>, ...z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>[]], "type">;
248
+ }, z.core.$strip>>>;
245
249
  look?: z.ZodOptional<z.ZodObject<{
246
250
  brand: z.ZodDefault<z.ZodString>;
247
251
  accent: z.ZodDefault<z.ZodString>;
@@ -359,6 +363,10 @@ export declare function defineSite<const B extends Record<string, z.ZodObject>>(
359
363
  shareText: z.ZodOptional<z.ZodString>;
360
364
  noindex: z.ZodDefault<z.ZodBoolean>;
361
365
  }, z.core.$strip>>;
366
+ saved: z.ZodDefault<z.ZodArray<z.ZodObject<{
367
+ name: z.ZodString;
368
+ block: z.ZodDiscriminatedUnion<[z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>, ...z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>[]], "type">;
369
+ }, z.core.$strip>>>;
362
370
  look?: z.ZodOptional<z.ZodObject<{
363
371
  brand: z.ZodDefault<z.ZodString>;
364
372
  accent: z.ZodDefault<z.ZodString>;
@@ -172,6 +172,12 @@ export function defineSite(blocks, o = {}) {
172
172
  ...(o.look ? { look: lookSchema(o.look) } : {}),
173
173
  ...o.extend,
174
174
  pages: z.array(page).min(1),
175
+ /** Sections the business put together and named, to add again anywhere ("Package card"): CalOS offers them beside the site's own kinds. */
176
+ saved: z
177
+ .array(z.object({ name: text.meta({ title: "Name" }), block: union }))
178
+ .max(30)
179
+ .default([])
180
+ .meta({ title: "Saved sections" }),
175
181
  })
176
182
  .superRefine((site, ctx) => {
177
183
  const s = site;
@@ -3,11 +3,17 @@ export type KitBlock = {
3
3
  id?: string;
4
4
  placement?: Record<string, unknown>;
5
5
  } & Record<string, unknown>;
6
- /** The site's own classes for a kit section's parts, each in place of the kit's: its band, its container, its heading. */
6
+ /**
7
+ * The site's own classes for a kit section's parts, each in place of the kit's: its band, its
8
+ * container, its heading, its words, and its buttons (the main one and a quieter one).
9
+ */
7
10
  export type KitClasses = {
8
11
  section?: string;
9
12
  wrap?: string;
10
13
  heading?: string;
14
+ text?: string;
15
+ button?: string;
16
+ buttonQuiet?: string;
11
17
  };
12
18
  /** A block of one of the kit's types, drawn; null for any other (the site's own to draw). */
13
19
  export declare function KitSection({ s, level, classes }: {
@@ -11,6 +11,8 @@ export function KitSection({ s, level = 2, classes = {} }) {
11
11
  return _jsx(Gallery, { s: s, level: level, classes: classes });
12
12
  case "slideshow":
13
13
  return _jsx(Slides, { s: s, level: level, classes: classes });
14
+ case "own":
15
+ return _jsx(Own, { s: s, level: level, classes: classes });
14
16
  default:
15
17
  return null;
16
18
  }
@@ -36,3 +38,50 @@ function Slides({ s, level, classes }) {
36
38
  const headingId = s.heading ? `${s.id ?? "slideshow"}-heading` : undefined;
37
39
  return (_jsx("section", { className: [classes.section ?? "kit-section", "kit-slideshow", placementClass(s)].filter(Boolean).join(" "), id: s.id, "aria-labelledby": headingId, children: _jsxs("div", { className: classes.wrap ?? "kit-wrap", children: [s.heading && (_jsx(Heading, { level: level, id: headingId, className: classes.heading ?? "kit-heading", children: s.heading })), _jsx(Slideshow, { label: s.heading ?? "Photos", seconds: s.seconds, className: SLIDE_RATIO[s.shape ?? "wide"], children: s.images.map((img, i) => (_jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: "(min-width: 1200px) 1200px, 100vw", eager: i === 0 }), img.caption && _jsx("figcaption", { children: img.caption })] }, img.src))) })] }) }));
38
40
  }
41
+ const SHAPE = { "as-taken": undefined, landscape: "4/3", square: "1/1", portrait: "4/5" };
42
+ const COLUMNS = { one: 1, two: 2, three: 3, four: 4, "wide-left": 2, "wide-right": 2 };
43
+ /**
44
+ * A section of the business's own: its columns of parts, laid out as chosen, each part in the site's
45
+ * own classes where it gave them and the kit's otherwise. A heading is the page's next level down.
46
+ */
47
+ function Own({ s, level, classes }) {
48
+ const n = COLUMNS[s.layout ?? "one"] ?? 1;
49
+ const columns = s.columns.slice(0, n);
50
+ return (_jsx("section", { className: [classes.section ?? "kit-section", "kit-own", `kit-own-${s.tone ?? "plain"}`, placementClass(s)].filter(Boolean).join(" "), id: s.id, children: _jsx("div", { className: classes.wrap ?? "kit-wrap", children: _jsx("div", { className: `kit-own-grid kit-own-${s.layout ?? "one"} kit-own-align-${s.align ?? "left"}`, children: columns.map((c, i) => (_jsx("div", { className: "kit-own-column", children: c.parts.map((p, j) => (_jsx(OwnPart, { p: p, level: level, classes: classes, columns: n }, j))) }, i))) }) }) }));
51
+ }
52
+ function OwnPart({ p, level, classes, columns }) {
53
+ const width = `(min-width: 1024px) ${Math.round(100 / columns)}vw, 100vw`;
54
+ switch (p.type) {
55
+ case "heading": {
56
+ // Large is the section's own heading level; medium and small the levels under it.
57
+ const at = level + (p.size === "large" ? 0 : p.size === "small" ? 2 : 1);
58
+ return (_jsx(Heading, { level: at, className: [classes.heading ?? "kit-heading", `kit-own-h-${p.size ?? "medium"}`].join(" "), children: p.text }));
59
+ }
60
+ case "text":
61
+ return _jsx(Md, { text: p.text, top: level + 1, className: ["kit-own-text", classes.text].filter(Boolean).join(" ") });
62
+ case "photo":
63
+ return (_jsxs("figure", { className: "kit-own-photo", children: [_jsx(Img, { src: p.image.src, alt: p.image.alt, sizes: width, ratio: SHAPE[p.shape ?? "as-taken"] }), p.image.caption && _jsx("figcaption", { children: p.image.caption })] }));
64
+ case "button": {
65
+ const quiet = p.style === "quiet";
66
+ return (_jsx("p", { className: "kit-own-action", children: _jsx("a", { href: p.href, className: (quiet ? classes.buttonQuiet : classes.button) ?? (quiet ? "kit-button kit-button-quiet" : "kit-button"), children: p.label }) }));
67
+ }
68
+ case "list": {
69
+ const L = p.style === "numbers" ? "ol" : "ul";
70
+ return (_jsx(L, { className: ["kit-own-list", `kit-own-list-${p.style ?? "bullets"}`, classes.text].filter(Boolean).join(" "), children: p.items.map((item, i) => (_jsx("li", { children: item }, i))) }));
71
+ }
72
+ case "quote":
73
+ return (_jsxs("figure", { className: "kit-own-quote", children: [_jsx("blockquote", { className: classes.text, children: p.text }), p.who && _jsx("figcaption", { children: p.who })] }));
74
+ case "stats":
75
+ return (_jsx("dl", { className: "kit-own-stats", children: p.items.map((it, i) => (_jsxs("div", { children: [_jsx("dt", { children: it.label }), _jsx("dd", { children: it.value })] }, i))) }));
76
+ case "price":
77
+ return (_jsxs("p", { className: "kit-own-price", children: [_jsx("span", { className: "kit-own-amount", children: p.amount }), p.per && _jsxs("span", { className: "kit-own-per", children: [" ", p.per] }), p.note && _jsx("span", { className: "kit-own-note", children: p.note })] }));
78
+ case "gallery": {
79
+ const n = Number(p.columns ?? "3");
80
+ return (_jsx("ul", { className: `kit-gallery-grid kit-cols-${n}`, children: p.images.map((img) => (_jsx("li", { children: _jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: `(min-width: 1024px) ${Math.round(100 / (n * columns))}vw, 50vw`, ratio: "1/1" }), img.caption && _jsx("figcaption", { children: img.caption })] }) }, img.src))) }));
81
+ }
82
+ case "slideshow":
83
+ return (_jsx(Slideshow, { label: "Photos", seconds: p.seconds, className: "kit-shape-landscape", children: p.images.map((img, i) => (_jsxs("figure", { children: [_jsx(Img, { src: img.src, alt: img.alt, sizes: width, eager: i === 0 }), img.caption && _jsx("figcaption", { children: img.caption })] }, img.src))) }));
84
+ case "space":
85
+ return _jsx("div", { "aria-hidden": "true", className: `kit-own-space kit-own-space-${p.size ?? "medium"}` });
86
+ }
87
+ }
@@ -35,6 +35,113 @@ export declare const slideshow: z.ZodObject<{
35
35
  }>>;
36
36
  seconds: z.ZodDefault<z.ZodNumber>;
37
37
  }, z.core.$strip>;
38
+ export declare const parts: {
39
+ readonly heading: z.ZodObject<{
40
+ text: z.ZodString;
41
+ size: z.ZodDefault<z.ZodEnum<{
42
+ small: "small";
43
+ large: "large";
44
+ medium: "medium";
45
+ }>>;
46
+ }, z.core.$strip>;
47
+ readonly text: z.ZodObject<{
48
+ text: z.ZodString;
49
+ }, z.core.$strip>;
50
+ readonly photo: z.ZodObject<{
51
+ image: z.ZodObject<{
52
+ src: z.ZodString;
53
+ alt: z.ZodString;
54
+ caption: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strip>;
56
+ shape: z.ZodDefault<z.ZodEnum<{
57
+ square: "square";
58
+ landscape: "landscape";
59
+ portrait: "portrait";
60
+ "as-taken": "as-taken";
61
+ }>>;
62
+ }, z.core.$strip>;
63
+ readonly button: z.ZodObject<{
64
+ label: z.ZodString;
65
+ href: z.ZodString;
66
+ style: z.ZodDefault<z.ZodEnum<{
67
+ main: "main";
68
+ quiet: "quiet";
69
+ }>>;
70
+ }, z.core.$strip>;
71
+ readonly list: z.ZodObject<{
72
+ items: z.ZodArray<z.ZodString>;
73
+ style: z.ZodDefault<z.ZodEnum<{
74
+ checks: "checks";
75
+ bullets: "bullets";
76
+ numbers: "numbers";
77
+ }>>;
78
+ }, z.core.$strip>;
79
+ readonly quote: z.ZodObject<{
80
+ text: z.ZodString;
81
+ who: z.ZodOptional<z.ZodString>;
82
+ }, z.core.$strip>;
83
+ readonly stats: z.ZodObject<{
84
+ items: z.ZodArray<z.ZodObject<{
85
+ value: z.ZodString;
86
+ label: z.ZodString;
87
+ }, z.core.$strip>>;
88
+ }, z.core.$strip>;
89
+ readonly price: z.ZodObject<{
90
+ amount: z.ZodString;
91
+ per: z.ZodOptional<z.ZodString>;
92
+ note: z.ZodOptional<z.ZodString>;
93
+ }, z.core.$strip>;
94
+ readonly gallery: z.ZodObject<{
95
+ images: z.ZodArray<z.ZodObject<{
96
+ src: z.ZodString;
97
+ alt: z.ZodString;
98
+ caption: z.ZodOptional<z.ZodString>;
99
+ }, z.core.$strip>>;
100
+ columns: z.ZodDefault<z.ZodEnum<{
101
+ 2: "2";
102
+ 3: "3";
103
+ 4: "4";
104
+ }>>;
105
+ }, z.core.$strip>;
106
+ readonly slideshow: z.ZodObject<{
107
+ images: z.ZodArray<z.ZodObject<{
108
+ src: z.ZodString;
109
+ alt: z.ZodString;
110
+ caption: z.ZodOptional<z.ZodString>;
111
+ }, z.core.$strip>>;
112
+ seconds: z.ZodDefault<z.ZodNumber>;
113
+ }, z.core.$strip>;
114
+ readonly space: z.ZodObject<{
115
+ size: z.ZodDefault<z.ZodEnum<{
116
+ small: "small";
117
+ large: "large";
118
+ medium: "medium";
119
+ }>>;
120
+ }, z.core.$strip>;
121
+ };
122
+ export type PartType = keyof typeof parts;
123
+ /** A section of the business's own: columns of parts, laid out as chosen, in the site's design. */
124
+ export declare const own: z.ZodObject<{
125
+ layout: z.ZodDefault<z.ZodEnum<{
126
+ one: "one";
127
+ two: "two";
128
+ three: "three";
129
+ four: "four";
130
+ "wide-left": "wide-left";
131
+ "wide-right": "wide-right";
132
+ }>>;
133
+ align: z.ZodDefault<z.ZodEnum<{
134
+ left: "left";
135
+ center: "center";
136
+ }>>;
137
+ tone: z.ZodDefault<z.ZodEnum<{
138
+ brand: "brand";
139
+ plain: "plain";
140
+ }>>;
141
+ columns: z.ZodArray<z.ZodObject<{
142
+ parts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>, ...z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>[]], "type">>;
143
+ }, z.core.$strip>>;
144
+ }, z.core.$strip>;
38
145
  /** Every section the kit draws, by the type a block names. */
39
146
  export declare const sections: {
40
147
  readonly gallery: z.ZodObject<{
@@ -71,6 +178,27 @@ export declare const sections: {
71
178
  }>>;
72
179
  seconds: z.ZodDefault<z.ZodNumber>;
73
180
  }, z.core.$strip>;
181
+ readonly own: z.ZodObject<{
182
+ layout: z.ZodDefault<z.ZodEnum<{
183
+ one: "one";
184
+ two: "two";
185
+ three: "three";
186
+ four: "four";
187
+ "wide-left": "wide-left";
188
+ "wide-right": "wide-right";
189
+ }>>;
190
+ align: z.ZodDefault<z.ZodEnum<{
191
+ left: "left";
192
+ center: "center";
193
+ }>>;
194
+ tone: z.ZodDefault<z.ZodEnum<{
195
+ brand: "brand";
196
+ plain: "plain";
197
+ }>>;
198
+ columns: z.ZodArray<z.ZodObject<{
199
+ parts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>, ...z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>[]], "type">>;
200
+ }, z.core.$strip>>;
201
+ }, z.core.$strip>;
74
202
  };
75
203
  export type KitSectionType = keyof typeof sections;
76
204
  export declare const isKitSection: (type: string) => type is KitSectionType;
@@ -3,7 +3,7 @@
3
3
  // styled by sections.css, themed by the site's Look. A site takes them all into its blocks
4
4
  // (`defineSite({ ...blocks, ...sections })`); one of its own by the same name is kept over the kit's.
5
5
  import { z } from "zod";
6
- import { image, md, text } from "../content/index.js";
6
+ import { href, image, md, text } from "../content/index.js";
7
7
  /** Photos in a grid, each with a caption if it has one: their work, the place, the team at it. */
8
8
  export const gallery = z
9
9
  .object({
@@ -31,6 +31,39 @@ export const slideshow = z
31
31
  seconds: z.number().int().min(3).max(15).default(5).meta({ title: "Seconds on each photo" }),
32
32
  })
33
33
  .meta({ title: "Slideshow", description: "Photos one at a time, fading from one to the next" });
34
+ /**
35
+ * The parts a section of the business's own is put together from in CalOS: each drawn in the site's
36
+ * own type, buttons and widths (the classes it gives KitSection), so what's built looks like the
37
+ * site, not a template. A fixed set, no colours or fonts of their own.
38
+ */
39
+ const photo = image.extend({ caption: text.optional().meta({ title: "Caption" }) });
40
+ export const parts = {
41
+ heading: z.object({ text, size: z.enum(["large", "medium", "small"]).default("medium").meta({ title: "Size" }) }).meta({ title: "Heading" }),
42
+ text: z.object({ text: md }).meta({ title: "Text" }),
43
+ photo: z.object({ image: photo, shape: z.enum(["as-taken", "landscape", "square", "portrait"]).default("as-taken").meta({ title: "Shape" }) }).meta({ title: "Photo" }),
44
+ button: z.object({ label: text.meta({ title: "Label" }), href, style: z.enum(["main", "quiet"]).default("main").meta({ title: "Style" }) }).meta({ title: "Button" }),
45
+ list: z.object({ items: z.array(text).min(1).max(20).meta({ title: "Items" }), style: z.enum(["bullets", "checks", "numbers"]).default("bullets").meta({ title: "Style" }) }).meta({ title: "List" }),
46
+ quote: z.object({ text: text.meta({ title: "Quote" }), who: text.optional().meta({ title: "Who said it" }) }).meta({ title: "Quote" }),
47
+ stats: z.object({ items: z.array(z.object({ value: text.meta({ title: "Number" }), label: text.meta({ title: "What it is" }) })).min(1).max(6).meta({ title: "Facts" }) }).meta({ title: "Facts in a row", description: "8 guests · 4 cabins · 2 baths" }),
48
+ price: z.object({ amount: text.meta({ title: "Price" }), per: text.optional().meta({ title: "Per" }), note: text.optional().meta({ title: "Note" }) }).meta({ title: "Price" }),
49
+ gallery: z.object({ images: z.array(photo).min(1).max(30).meta({ title: "Photos" }), columns: z.enum(["2", "3", "4"]).default("3").meta({ title: "Photos across" }) }).meta({ title: "Photos in a grid" }),
50
+ slideshow: z.object({ images: z.array(photo).min(2).max(20).meta({ title: "Photos" }), seconds: z.number().int().min(3).max(15).default(5).meta({ title: "Seconds on each photo" }) }).meta({ title: "Slideshow" }),
51
+ space: z.object({ size: z.enum(["small", "medium", "large"]).default("medium").meta({ title: "Size" }) }).meta({ title: "Space" }),
52
+ };
53
+ const part = z.discriminatedUnion("type", Object.keys(parts).map((type) => z.object({ type: z.literal(type) }).extend(parts[type].shape).meta(parts[type].meta())));
54
+ /** A section of the business's own: columns of parts, laid out as chosen, in the site's design. */
55
+ export const own = z
56
+ .object({
57
+ layout: z.enum(["one", "two", "three", "four", "wide-left", "wide-right"]).default("one").meta({ title: "Columns" }),
58
+ align: z.enum(["left", "center"]).default("left").meta({ title: "Align" }),
59
+ tone: z.enum(["plain", "brand"]).default("plain").meta({ title: "Background" }),
60
+ columns: z
61
+ .array(z.object({ parts: z.array(part).max(12).meta({ title: "Parts" }) }).meta({ title: "Column" }))
62
+ .min(1)
63
+ .max(4)
64
+ .meta({ title: "Columns' parts" }),
65
+ })
66
+ .meta({ title: "Your own section", description: "Put together from headings, text, photos, buttons and more, in the site's design" });
34
67
  /** Every section the kit draws, by the type a block names. */
35
- export const sections = { gallery, slideshow };
68
+ export const sections = { gallery, slideshow, own };
36
69
  export const isKitSection = (type) => Object.prototype.hasOwnProperty.call(sections, type);
package/dist/sections.css CHANGED
@@ -191,3 +191,168 @@
191
191
  transition: none;
192
192
  }
193
193
  }
194
+
195
+ /*
196
+ * A section of the business's own: its columns side by side from a tablet up (stacked on a phone),
197
+ * its parts spaced evenly, in the site's type. Brand tone: the site's brand colour behind, its
198
+ * readable colour over it.
199
+ */
200
+ .kit-own-brand {
201
+ background: var(--look-brand, #111);
202
+ color: var(--look-on-brand, #fff);
203
+ }
204
+ .kit-own-grid {
205
+ display: grid;
206
+ gap: clamp(24px, 4vw, 48px);
207
+ }
208
+ @media (min-width: 720px) {
209
+ .kit-own-two,
210
+ .kit-own-four {
211
+ grid-template-columns: repeat(2, minmax(0, 1fr));
212
+ }
213
+ .kit-own-three {
214
+ grid-template-columns: repeat(3, minmax(0, 1fr));
215
+ }
216
+ .kit-own-wide-left {
217
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
218
+ }
219
+ .kit-own-wide-right {
220
+ grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
221
+ }
222
+ }
223
+ @media (min-width: 1024px) {
224
+ .kit-own-four {
225
+ grid-template-columns: repeat(4, minmax(0, 1fr));
226
+ }
227
+ }
228
+ .kit-own-align-center {
229
+ text-align: center;
230
+ }
231
+ .kit-own-align-center .kit-own-text,
232
+ .kit-own-align-center .kit-own-list {
233
+ margin-inline: auto;
234
+ }
235
+ .kit-own-column {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 16px;
239
+ min-width: 0;
240
+ }
241
+ .kit-own-column > * {
242
+ margin: 0;
243
+ }
244
+ .kit-own-h-large {
245
+ font-size: clamp(1.75rem, 3.5vw, 2.5rem);
246
+ }
247
+ .kit-own-h-small {
248
+ font-size: 1.15rem;
249
+ }
250
+ .kit-own-text {
251
+ max-width: 65ch;
252
+ }
253
+ .kit-own-photo {
254
+ margin: 0;
255
+ }
256
+ .kit-own-photo img {
257
+ display: block;
258
+ width: 100%;
259
+ height: auto;
260
+ border-radius: var(--look-radius, 8px);
261
+ object-fit: cover;
262
+ }
263
+ .kit-own-photo img.ratio-1-1 {
264
+ aspect-ratio: 1 / 1;
265
+ }
266
+ .kit-own-photo img.ratio-4-3 {
267
+ aspect-ratio: 4 / 3;
268
+ }
269
+ .kit-own-photo img.ratio-4-5 {
270
+ aspect-ratio: 4 / 5;
271
+ }
272
+ .kit-own-photo figcaption,
273
+ .kit-own-quote figcaption {
274
+ margin-top: 8px;
275
+ font-size: 0.9rem;
276
+ opacity: 0.75;
277
+ }
278
+ .kit-button {
279
+ display: inline-block;
280
+ padding: 10px 18px;
281
+ border: 1px solid var(--look-button-border, currentColor);
282
+ border-radius: var(--look-button-radius, 8px);
283
+ background: var(--look-button-bg, #111);
284
+ color: var(--look-button-fg, #fff);
285
+ font-weight: 500;
286
+ text-decoration: none;
287
+ }
288
+ .kit-button-quiet {
289
+ background: transparent;
290
+ color: inherit;
291
+ }
292
+ .kit-own-list {
293
+ max-width: 65ch;
294
+ padding-left: 1.25em;
295
+ }
296
+ .kit-own-list li + li {
297
+ margin-top: 6px;
298
+ }
299
+ .kit-own-list-checks {
300
+ list-style: none;
301
+ padding-left: 0;
302
+ }
303
+ .kit-own-list-checks li::before {
304
+ content: "✓";
305
+ margin-right: 8px;
306
+ color: var(--look-accent, currentColor);
307
+ }
308
+ .kit-own-quote {
309
+ margin: 0;
310
+ }
311
+ .kit-own-quote blockquote {
312
+ margin: 0;
313
+ font-size: 1.15rem;
314
+ line-height: 1.5;
315
+ }
316
+ .kit-own-stats {
317
+ display: flex;
318
+ flex-wrap: wrap;
319
+ gap: 16px 32px;
320
+ }
321
+ .kit-own-align-center .kit-own-stats {
322
+ justify-content: center;
323
+ }
324
+ .kit-own-stats div {
325
+ display: flex;
326
+ flex-direction: column-reverse;
327
+ }
328
+ .kit-own-stats dd {
329
+ margin: 0;
330
+ font-size: 1.6rem;
331
+ font-weight: 600;
332
+ line-height: 1.1;
333
+ }
334
+ .kit-own-stats dt {
335
+ font-size: 0.85rem;
336
+ opacity: 0.75;
337
+ }
338
+ .kit-own-amount {
339
+ font-size: 2rem;
340
+ font-weight: 600;
341
+ }
342
+ .kit-own-per {
343
+ opacity: 0.75;
344
+ }
345
+ .kit-own-note {
346
+ display: block;
347
+ font-size: 0.85rem;
348
+ opacity: 0.75;
349
+ }
350
+ .kit-own-space-small {
351
+ height: 8px;
352
+ }
353
+ .kit-own-space-medium {
354
+ height: 24px;
355
+ }
356
+ .kit-own-space-large {
357
+ height: 56px;
358
+ }
@@ -92,3 +92,43 @@ describe("a slideshow", () => {
92
92
  expect(html).toContain('aria-label="On board"');
93
93
  });
94
94
  });
95
+ describe("a section of the business's own", () => {
96
+ const page = (blocks, saved = []) => ({ url: "https://acme.example", business: { name: "Acme" }, saved, pages: [{ path: "/", title: "Acme", description: "Acme does things.", blocks }] });
97
+ const block = {
98
+ type: "own",
99
+ layout: "two",
100
+ columns: [
101
+ { 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: "/" }] },
102
+ { 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" }] },
103
+ ],
104
+ };
105
+ it("is put together from parts, checked like any section, and can be saved by name to add again", () => {
106
+ const site = defineSite({ ...sections });
107
+ const ok = site.parse(page([block], [{ name: "Package card", block }]));
108
+ expect(ok.problems).toBeUndefined();
109
+ expect(ok.site.pages[0].blocks[0]).toMatchObject({ align: "left", tone: "plain" });
110
+ expect(ok.site.saved[0].name).toBe("Package card");
111
+ // A part it doesn't know, or a list with nothing in it, is said.
112
+ expect(site.parse(page([{ ...block, columns: [{ parts: [{ type: "marquee" }] }] }])).problems?.join(" ")).toMatch(/type/);
113
+ expect(site.parse(page([{ ...block, columns: [{ parts: [{ type: "list", items: [] }] }] }])).problems?.join(" ")).toMatch(/items/);
114
+ // Saved sections are optional: a site without any is as it was.
115
+ expect(site.parse(page([block])).site.saved).toEqual([]);
116
+ });
117
+ it("is drawn in the site's own classes: its heading, words and buttons", () => {
118
+ const html = renderToStaticMarkup(_jsx(KitSection, { s: block, level: 2, classes: { section: "", wrap: "site-wrap", heading: "site-h", text: "site-text", button: "site-button" } }));
119
+ expect(html).toContain('class="kit-own-grid kit-own-two kit-own-align-left"');
120
+ expect(html).toContain('<h2 class="site-h kit-own-h-large">Our packages</h2>');
121
+ expect(html).toContain("<strong>the one</strong>");
122
+ expect(html).toContain('class="kit-own-list kit-own-list-checks site-text"');
123
+ expect(html).toContain('<a href="/" class="site-button">Book</a>');
124
+ expect(html).toContain('<span class="kit-own-amount">$120</span>');
125
+ expect(html).toContain("<dd>8</dd>");
126
+ expect(html).toContain("<blockquote class=\"site-text\">Best week ever.</blockquote>");
127
+ expect(html).not.toMatch(/\sstyle=/);
128
+ });
129
+ it("keeps to its layout's columns, and draws the kit's own button where the site gave none", () => {
130
+ const html = renderToStaticMarkup(_jsx(KitSection, { s: { ...block, layout: "one" } }));
131
+ expect(html.match(/kit-own-column/g)).toHaveLength(1);
132
+ expect(html).toContain('class="kit-button"');
133
+ });
134
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caltho/site-kit",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",