astroidjs 0.1.2 → 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 +240 -5
- package/bin/astroid.mjs +185 -9
- package/dist/analytics/index.d.ts +37 -0
- package/dist/analytics/index.js +108 -0
- package/dist/astro/csp.d.ts +64 -0
- package/dist/astro/csp.js +173 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/auth/index.d.ts +27 -0
- package/dist/auth/index.js +59 -0
- package/dist/commerce/adapters.d.ts +60 -0
- package/dist/commerce/adapters.js +90 -0
- package/dist/commerce/checkout-scaffold.d.ts +42 -0
- package/dist/commerce/checkout-scaffold.js +306 -0
- package/dist/commerce/checkout.d.ts +72 -0
- package/dist/commerce/checkout.js +124 -0
- package/dist/commerce/index.d.ts +8 -0
- package/dist/commerce/index.js +9 -0
- package/dist/commerce/loader.d.ts +71 -0
- package/dist/commerce/loader.js +90 -0
- package/dist/commerce/mirror.d.ts +69 -0
- package/dist/commerce/mirror.js +214 -0
- package/dist/commerce/roles.d.ts +38 -0
- package/dist/commerce/roles.js +93 -0
- package/dist/commerce/secrets.d.ts +74 -0
- package/dist/commerce/secrets.js +129 -0
- package/dist/commerce/sync.d.ts +86 -0
- package/dist/commerce/sync.js +154 -0
- package/dist/components/sections.d.ts +577 -0
- package/dist/components/sections.js +425 -0
- package/dist/config.d.ts +239 -12
- package/dist/config.js +49 -1
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +5 -0
- package/dist/email/inquiry.d.ts +33 -0
- package/dist/email/inquiry.js +63 -0
- package/dist/email/send.d.ts +120 -0
- package/dist/email/send.js +196 -0
- package/dist/email/templates.d.ts +24 -0
- package/dist/email/templates.js +184 -0
- package/dist/email/theme.d.ts +24 -0
- package/dist/email/theme.js +150 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/map/index.d.ts +3 -0
- package/dist/map/index.js +4 -0
- package/dist/map/pmtiles.d.ts +92 -0
- package/dist/map/pmtiles.js +130 -0
- package/dist/map/scaffold.d.ts +29 -0
- package/dist/map/scaffold.js +212 -0
- package/dist/map/style.d.ts +58 -0
- package/dist/map/style.js +154 -0
- package/dist/portal/config.d.ts +26 -0
- package/dist/portal/config.js +56 -0
- package/dist/portal/guard.d.ts +54 -0
- package/dist/portal/guard.js +64 -0
- package/dist/portal/index.d.ts +5 -0
- package/dist/portal/index.js +6 -0
- package/dist/portal/nav.d.ts +26 -0
- package/dist/portal/nav.js +35 -0
- package/dist/portal/scaffold.d.ts +28 -0
- package/dist/portal/scaffold.js +140 -0
- package/dist/portal/session.d.ts +36 -0
- package/dist/portal/session.js +86 -0
- package/dist/portfolio/index.d.ts +1 -0
- package/dist/portfolio/index.js +4 -0
- package/dist/portfolio/scaffold.d.ts +9 -0
- package/dist/portfolio/scaffold.js +93 -0
- package/dist/project/actions.d.ts +3 -0
- package/dist/project/actions.js +121 -0
- package/dist/project/generate.d.ts +15 -0
- package/dist/project/generate.js +144 -2
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/scaffold.d.ts +29 -0
- package/dist/project/scaffold.js +166 -0
- package/dist/pwa/generate.d.ts +49 -0
- package/dist/pwa/generate.js +218 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa/index.js +2 -0
- package/dist/queues/consumer.d.ts +29 -0
- package/dist/queues/consumer.js +37 -0
- package/dist/queues/index.d.ts +4 -0
- package/dist/queues/index.js +5 -0
- package/dist/queues/messages.d.ts +60 -0
- package/dist/queues/messages.js +71 -0
- package/dist/queues/scaffold.d.ts +44 -0
- package/dist/queues/scaffold.js +204 -0
- package/dist/queues/webhook.d.ts +60 -0
- package/dist/queues/webhook.js +81 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/scaffold.d.ts +30 -0
- package/dist/realtime/scaffold.js +159 -0
- package/dist/schema/collections.d.ts +41 -7
- package/dist/schema/collections.js +110 -12
- package/dist/schema/framework.js +5 -0
- package/dist/schema/generate.js +17 -1
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +80 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/rate-rules.d.ts +21 -0
- package/dist/security/rate-rules.js +110 -0
- package/dist/seo/index.d.ts +3 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/resolve.d.ts +68 -0
- package/dist/seo/resolve.js +73 -0
- package/dist/seo/routes.d.ts +44 -0
- package/dist/seo/routes.js +104 -0
- package/dist/seo/structured-data.d.ts +51 -0
- package/dist/seo/structured-data.js +105 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +113 -0
- package/dist/worker/generate.d.ts +18 -10
- package/dist/worker/generate.js +353 -42
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +42 -0
- package/dist/workflow/advance.d.ts +102 -0
- package/dist/workflow/advance.js +145 -0
- package/dist/workflow/config.d.ts +60 -0
- package/dist/workflow/config.js +73 -0
- package/dist/workflow/generate.d.ts +22 -0
- package/dist/workflow/generate.js +138 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +4 -0
- package/package.json +21 -4
- package/src/components/Editable.astro +33 -9
- package/src/components/JustifiedGallery.astro +254 -0
- package/src/components/MediaSlot.astro +178 -0
- package/src/components/PortalShell.astro +80 -0
- package/src/components/RegisterSW.astro +45 -0
- package/src/components/Section.astro +101 -35
- package/src/components/Sections.astro +64 -0
- package/src/components/Seo.astro +57 -0
- package/src/components/StageBar.astro +137 -0
- package/src/components/StructuredData.astro +33 -0
- package/src/components/justify.ts +170 -0
- package/src/components/media-meta.ts +174 -0
- package/src/components/sections/AboutIntro.astro +46 -0
- package/src/components/sections/Banner.astro +31 -0
- package/src/components/sections/Contact.astro +22 -9
- package/src/components/sections/Cta.astro +33 -10
- package/src/components/sections/Faq.astro +50 -0
- package/src/components/sections/FeatureGrid.astro +40 -11
- package/src/components/sections/Gallery.astro +46 -0
- package/src/components/sections/Hero.astro +40 -12
- package/src/components/sections/LocationHours.astro +59 -0
- package/src/components/sections/Media.astro +44 -0
- package/src/components/sections/PricingTiers.astro +79 -0
- package/src/components/sections/ProductGrid.astro +73 -0
- package/src/components/sections/SplitImage.astro +61 -0
- package/src/components/sections/Steps.astro +58 -0
- package/src/components/sections/Testimonial.astro +51 -0
- package/src/components/sections.ts +452 -67
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The section library's schema + render contract (ADR 0003 primitives, ADR 0005
|
|
4
|
+
// model).
|
|
5
|
+
//
|
|
6
|
+
// This file used to define a parallel universe: a `SectionProps` union
|
|
7
|
+
// discriminated on `kind`, with `colorway`/`align` as component props. Louise's
|
|
8
|
+
// actual model — the one the on-canvas editor and the write-time validator both
|
|
9
|
+
// read — is different in every particular, and it is the one that wins:
|
|
10
|
+
//
|
|
11
|
+
// • a section is a stored `SectionItem`: `{ _type, blocks?, _layout?,
|
|
12
|
+
// _settings?, ...fields }`. The discriminant is `_type`, not `kind`.
|
|
13
|
+
// • its shape is declared once as a `SectionDef` in a `SectionCatalog`, which
|
|
14
|
+
// is SCHEMA ONLY. The same object drives `mountSections` (the editor) and
|
|
15
|
+
// `validateSections` (the server), so a field can't be editable-but-invalid
|
|
16
|
+
// or validated-but-uneditable.
|
|
17
|
+
// • presentation choices are `_settings` / `_layout` **tokens**. Louise stores
|
|
18
|
+
// the token; the site maps it to CSS. That's why COLORWAY_CLASS below stays
|
|
19
|
+
// — it is exactly the site-owned half of that contract — while `colorway`
|
|
20
|
+
// stops being a prop and becomes a stored setting.
|
|
21
|
+
//
|
|
22
|
+
// ADR 0005 §2 names this file's job outright: "<Section> reads `_layout` /
|
|
23
|
+
// `_settings` and slots its children, and <Editable> already owns the
|
|
24
|
+
// `data-louise-*` marker contract, so a site author writes `<Editable
|
|
25
|
+
// field="heading">` and never hand-stamps the deeper path."
|
|
26
|
+
//
|
|
27
|
+
// Self-contained on purpose: this module ships as SOURCE (the `.astro` files
|
|
28
|
+
// beside it import it directly), so it must not reach back into astroid's built
|
|
29
|
+
// `src/*` — only siblings and external packages. The `louise-toolkit/content`
|
|
30
|
+
// import is TYPE-ONLY, so it erases at build and never drags the validator (or
|
|
31
|
+
// drizzle, which that entry pulls in) into a page bundle.
|
|
32
|
+
/**
|
|
33
|
+
* Colorway → daisyUI surface classes, keyed to the `Theme.colors` set
|
|
34
|
+
* (brand/secondary/tertiary) plus the neutral base.
|
|
35
|
+
*
|
|
36
|
+
* This map is the site-owned half of the token contract: Louise stores
|
|
37
|
+
* `_settings.colorway = "brand"` and never learns what that renders as, so a
|
|
38
|
+
* brand re-theme is a change here and no content rewrite anywhere.
|
|
39
|
+
*/
|
|
40
|
+
export const COLORWAY_CLASS = {
|
|
41
|
+
brand: "bg-primary text-primary-content",
|
|
42
|
+
secondary: "bg-secondary text-secondary-content",
|
|
43
|
+
tertiary: "bg-accent text-accent-content",
|
|
44
|
+
base: "bg-base-100 text-base-content",
|
|
45
|
+
};
|
|
46
|
+
/** Content alignment → flex/text-alignment utilities. Same token contract. */
|
|
47
|
+
export const ALIGN_CLASS = {
|
|
48
|
+
start: "text-start items-start",
|
|
49
|
+
center: "text-center items-center",
|
|
50
|
+
end: "text-end items-end",
|
|
51
|
+
};
|
|
52
|
+
/** Resolve a colorway token to its class string (default: neutral base). */
|
|
53
|
+
export function colorwayClass(colorway = "base") {
|
|
54
|
+
return COLORWAY_CLASS[colorway] ?? COLORWAY_CLASS.base;
|
|
55
|
+
}
|
|
56
|
+
/** Resolve an alignment token to its class string (default: start). */
|
|
57
|
+
export function alignClass(align = "start") {
|
|
58
|
+
return ALIGN_CLASS[align] ?? ALIGN_CLASS.start;
|
|
59
|
+
}
|
|
60
|
+
/** Title-case a token for a picker label ("brand" → "Brand"). */
|
|
61
|
+
const label = (token) => token.charAt(0).toUpperCase() + token.slice(1);
|
|
62
|
+
/**
|
|
63
|
+
* Turn a token→class map into `select` options.
|
|
64
|
+
*
|
|
65
|
+
* Deriving them is the point: the options a picker offers and the tokens the
|
|
66
|
+
* site can actually render are then the same list by construction, so adding a
|
|
67
|
+
* colorway is one edit to `COLORWAY_CLASS` rather than an edit plus a
|
|
68
|
+
* remembered second edit here that silently offers a token nothing maps.
|
|
69
|
+
*/
|
|
70
|
+
const tokenOptions = (map) => Object.keys(map).map((value) => ({ value, label: label(value) }));
|
|
71
|
+
/**
|
|
72
|
+
* The shared `_settings` every section in the catalog accepts.
|
|
73
|
+
*
|
|
74
|
+
* These are closed token sets, declared as `select` (#272) so the inspector
|
|
75
|
+
* renders a picker and an unknown token is rejected on write. They used to be
|
|
76
|
+
* `text` with the valid values stuffed into `placeholder` — which meant a typo
|
|
77
|
+
* wasn't a validation error at all, just a silent fallback to the default
|
|
78
|
+
* inside `colorwayClass` at render time.
|
|
79
|
+
*/
|
|
80
|
+
export const SECTION_SETTINGS = {
|
|
81
|
+
colorway: {
|
|
82
|
+
type: "select",
|
|
83
|
+
label: "Colorway",
|
|
84
|
+
inline: false,
|
|
85
|
+
options: tokenOptions(COLORWAY_CLASS),
|
|
86
|
+
// An opaque hint — the schema layer doesn't know what a swatch looks like;
|
|
87
|
+
// a renderer that doesn't support it just shows a normal picker.
|
|
88
|
+
display: "swatch",
|
|
89
|
+
},
|
|
90
|
+
align: {
|
|
91
|
+
type: "select",
|
|
92
|
+
label: "Alignment",
|
|
93
|
+
inline: false,
|
|
94
|
+
options: tokenOptions(ALIGN_CLASS),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
/** Read a `_settings` token as a string, or a fallback. Tolerant by design: a
|
|
98
|
+
* stored setting is untrusted JSON, and a bad token should degrade to the
|
|
99
|
+
* default rather than throw during render. */
|
|
100
|
+
export function setting(item, key, fallback) {
|
|
101
|
+
const value = item._settings?.[key];
|
|
102
|
+
return typeof value === "string" && value !== "" ? value : fallback;
|
|
103
|
+
}
|
|
104
|
+
/** Read a section field as a string (the common case for text/richText). */
|
|
105
|
+
export function field(item, key) {
|
|
106
|
+
const value = item[key];
|
|
107
|
+
return typeof value === "string" ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Read an `array` field as a list of objects, or `[]`. Stored arrays are
|
|
110
|
+
* untrusted, so a non-array or a scalar entry is dropped rather than rendered. */
|
|
111
|
+
export function list(item, key) {
|
|
112
|
+
const value = item[key];
|
|
113
|
+
if (!Array.isArray(value))
|
|
114
|
+
return [];
|
|
115
|
+
return value.filter((v) => typeof v === "object" && v !== null && !Array.isArray(v));
|
|
116
|
+
}
|
|
117
|
+
/** Read a string off an array item (see {@link list}). */
|
|
118
|
+
export function itemField(row, key) {
|
|
119
|
+
const value = row[key];
|
|
120
|
+
return typeof value === "string" ? value : undefined;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Alt text for an image, resolving the ASSET-LEVEL fallback.
|
|
124
|
+
*
|
|
125
|
+
* The precedence is the whole reason `<Sections>` does its media lookup: a
|
|
126
|
+
* per-usage `alt` on the section wins, because the same photo means something
|
|
127
|
+
* different in a hero than in a thumbnail strip — but when there isn't one, the
|
|
128
|
+
* alt an editor typed once in the media library is used. That's what makes
|
|
129
|
+
* fixing alt text a single edit that propagates everywhere the asset appears,
|
|
130
|
+
* instead of a hunt through every page that embeds it.
|
|
131
|
+
*
|
|
132
|
+
* Returns `""` rather than undefined when neither exists: an image with no
|
|
133
|
+
* description is decorative as far as a screen reader is concerned, and an
|
|
134
|
+
* empty alt says exactly that. A missing attribute makes it read the filename.
|
|
135
|
+
*/
|
|
136
|
+
export function mediaAlt(mediaMeta, src, override) {
|
|
137
|
+
if (override !== undefined && override !== "")
|
|
138
|
+
return override;
|
|
139
|
+
return (src ? mediaMeta?.[src]?.alt : undefined) ?? "";
|
|
140
|
+
}
|
|
141
|
+
/** Caption for an image, same precedence as {@link mediaAlt}. Undefined when
|
|
142
|
+
* there is none — a missing caption renders nothing, unlike a missing alt. */
|
|
143
|
+
export function mediaCaption(mediaMeta, src, override) {
|
|
144
|
+
if (override !== undefined && override !== "")
|
|
145
|
+
return override;
|
|
146
|
+
return src ? mediaMeta?.[src]?.caption : undefined;
|
|
147
|
+
}
|
|
148
|
+
// --- The catalog -----------------------------------------------------------
|
|
149
|
+
// Schema only. Each entry declares what an editor can change and how it is
|
|
150
|
+
// validated; the matching `.astro` component owns every pixel.
|
|
151
|
+
/**
|
|
152
|
+
* Every section type Astroid ships.
|
|
153
|
+
*
|
|
154
|
+
* `satisfies` rather than a `: SectionCatalog` annotation, and it matters:
|
|
155
|
+
* `SectionCatalog` is `Record<string, SectionDef>`, so annotating would widen
|
|
156
|
+
* `keyof typeof` to `string` and throw away the literal keys. Those keys are
|
|
157
|
+
* the project's whole section vocabulary — `SectionKind` is derived from them
|
|
158
|
+
* (config.ts), `isRenderableSection` narrows to them, and `<Section>` indexes
|
|
159
|
+
* its component map with them. Annotate this and all three silently degrade to
|
|
160
|
+
* "any string", which is how the dispatcher lost its type safety once already.
|
|
161
|
+
*/
|
|
162
|
+
export const astroidSectionCatalog = {
|
|
163
|
+
hero: {
|
|
164
|
+
label: "Hero",
|
|
165
|
+
icon: "hero",
|
|
166
|
+
fields: {
|
|
167
|
+
heading: { type: "text", label: "Heading", validation: (r) => r.required().max(120) },
|
|
168
|
+
subheading: { type: "textarea", label: "Subheading" },
|
|
169
|
+
// A link URL is something you can't point at on the page, so it is not
|
|
170
|
+
// inline — it belongs in the inspector, which is what `inline: false` says.
|
|
171
|
+
ctaLabel: { type: "text", label: "Button label" },
|
|
172
|
+
ctaHref: { type: "text", label: "Button link", inline: false },
|
|
173
|
+
},
|
|
174
|
+
settings: SECTION_SETTINGS,
|
|
175
|
+
},
|
|
176
|
+
featureGrid: {
|
|
177
|
+
label: "Feature grid",
|
|
178
|
+
icon: "grid",
|
|
179
|
+
fields: {
|
|
180
|
+
heading: { type: "text", label: "Heading" },
|
|
181
|
+
items: {
|
|
182
|
+
type: "array",
|
|
183
|
+
label: "Features",
|
|
184
|
+
itemLabel: "Feature",
|
|
185
|
+
itemFields: {
|
|
186
|
+
title: { type: "text", label: "Title", validation: (r) => r.required() },
|
|
187
|
+
body: { type: "textarea", label: "Body" },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
settings: SECTION_SETTINGS,
|
|
192
|
+
},
|
|
193
|
+
cta: {
|
|
194
|
+
label: "Call to action",
|
|
195
|
+
icon: "cta",
|
|
196
|
+
fields: {
|
|
197
|
+
heading: { type: "text", label: "Heading", validation: (r) => r.required() },
|
|
198
|
+
body: { type: "textarea", label: "Body" },
|
|
199
|
+
ctaLabel: { type: "text", label: "Button label", validation: (r) => r.required() },
|
|
200
|
+
ctaHref: { type: "text", label: "Button link", inline: false },
|
|
201
|
+
},
|
|
202
|
+
settings: SECTION_SETTINGS,
|
|
203
|
+
},
|
|
204
|
+
// --- media ---------------------------------------------------------------
|
|
205
|
+
// Every `image` field is declared as such rather than as text, which is what
|
|
206
|
+
// lets `collectSectionMediaUrls` find it and the page resolve alt/caption in
|
|
207
|
+
// one lookup. A per-usage `alt` sits beside each one as an OVERRIDE: left
|
|
208
|
+
// empty it falls back to the asset's own alt, so an editor fixes it once in
|
|
209
|
+
// the media library (see `mediaAlt`).
|
|
210
|
+
gallery: {
|
|
211
|
+
label: "Gallery",
|
|
212
|
+
icon: "gallery",
|
|
213
|
+
fields: {
|
|
214
|
+
heading: { type: "text", label: "Heading" },
|
|
215
|
+
items: {
|
|
216
|
+
type: "array",
|
|
217
|
+
label: "Images",
|
|
218
|
+
itemLabel: "Image",
|
|
219
|
+
itemFields: {
|
|
220
|
+
image: { type: "image", label: "Image" },
|
|
221
|
+
alt: { type: "text", label: "Alt text (overrides the asset's)", inline: false },
|
|
222
|
+
caption: { type: "text", label: "Caption" },
|
|
223
|
+
href: { type: "text", label: "Links to", inline: false },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
settings: SECTION_SETTINGS,
|
|
228
|
+
},
|
|
229
|
+
media: {
|
|
230
|
+
label: "Media",
|
|
231
|
+
icon: "image",
|
|
232
|
+
fields: {
|
|
233
|
+
heading: { type: "text", label: "Heading" },
|
|
234
|
+
image: { type: "image", label: "Image" },
|
|
235
|
+
alt: { type: "text", label: "Alt text (overrides the asset's)", inline: false },
|
|
236
|
+
caption: { type: "text", label: "Caption" },
|
|
237
|
+
},
|
|
238
|
+
settings: SECTION_SETTINGS,
|
|
239
|
+
},
|
|
240
|
+
splitImage: {
|
|
241
|
+
label: "Split image",
|
|
242
|
+
icon: "columns",
|
|
243
|
+
fields: {
|
|
244
|
+
heading: { type: "text", label: "Heading", validation: (r) => r.required() },
|
|
245
|
+
body: { type: "richText", label: "Body" },
|
|
246
|
+
image: { type: "image", label: "Image" },
|
|
247
|
+
alt: { type: "text", label: "Alt text (overrides the asset's)", inline: false },
|
|
248
|
+
ctaLabel: { type: "text", label: "Button label" },
|
|
249
|
+
ctaHref: { type: "text", label: "Button link", inline: false },
|
|
250
|
+
},
|
|
251
|
+
// Which side the image sits on is a LAYOUT, not a setting: it's a named
|
|
252
|
+
// arrangement of the same content, which is exactly what `_layout` is for.
|
|
253
|
+
layouts: { imageStart: { label: "Image left" }, imageEnd: { label: "Image right" } },
|
|
254
|
+
settings: SECTION_SETTINGS,
|
|
255
|
+
},
|
|
256
|
+
// --- structured copy -----------------------------------------------------
|
|
257
|
+
steps: {
|
|
258
|
+
label: "Steps",
|
|
259
|
+
icon: "list-ordered",
|
|
260
|
+
fields: {
|
|
261
|
+
heading: { type: "text", label: "Heading" },
|
|
262
|
+
items: {
|
|
263
|
+
type: "array",
|
|
264
|
+
label: "Steps",
|
|
265
|
+
itemLabel: "Step",
|
|
266
|
+
itemFields: {
|
|
267
|
+
title: { type: "text", label: "Title", validation: (r) => r.required() },
|
|
268
|
+
body: { type: "textarea", label: "Body" },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
settings: SECTION_SETTINGS,
|
|
273
|
+
},
|
|
274
|
+
banner: {
|
|
275
|
+
label: "Banner",
|
|
276
|
+
icon: "megaphone",
|
|
277
|
+
fields: {
|
|
278
|
+
text: { type: "text", label: "Text", validation: (r) => r.required().max(200) },
|
|
279
|
+
ctaLabel: { type: "text", label: "Button label" },
|
|
280
|
+
ctaHref: { type: "text", label: "Button link", inline: false },
|
|
281
|
+
},
|
|
282
|
+
settings: SECTION_SETTINGS,
|
|
283
|
+
},
|
|
284
|
+
faq: {
|
|
285
|
+
label: "FAQ",
|
|
286
|
+
icon: "help",
|
|
287
|
+
fields: {
|
|
288
|
+
heading: { type: "text", label: "Heading" },
|
|
289
|
+
items: {
|
|
290
|
+
type: "array",
|
|
291
|
+
label: "Questions",
|
|
292
|
+
itemLabel: "Question",
|
|
293
|
+
itemFields: {
|
|
294
|
+
question: { type: "text", label: "Question", validation: (r) => r.required() },
|
|
295
|
+
// richText, not textarea: an answer routinely wants a link.
|
|
296
|
+
answer: { type: "richText", label: "Answer" },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
settings: SECTION_SETTINGS,
|
|
301
|
+
},
|
|
302
|
+
pricingTiers: {
|
|
303
|
+
label: "Pricing tiers",
|
|
304
|
+
icon: "tag",
|
|
305
|
+
fields: {
|
|
306
|
+
heading: { type: "text", label: "Heading" },
|
|
307
|
+
items: {
|
|
308
|
+
type: "array",
|
|
309
|
+
label: "Tiers",
|
|
310
|
+
itemLabel: "Tier",
|
|
311
|
+
itemFields: {
|
|
312
|
+
name: { type: "text", label: "Name", validation: (r) => r.required() },
|
|
313
|
+
price: { type: "text", label: "Price" },
|
|
314
|
+
period: { type: "text", label: "Period (e.g. /mo)" },
|
|
315
|
+
// A list of strings isn't expressible — array items are objects — so
|
|
316
|
+
// each feature is a one-field row. That also leaves room to add an
|
|
317
|
+
// `included` flag later without a data migration.
|
|
318
|
+
features: {
|
|
319
|
+
type: "array",
|
|
320
|
+
label: "Features",
|
|
321
|
+
itemLabel: "Feature",
|
|
322
|
+
itemFields: { text: { type: "text", label: "Feature" } },
|
|
323
|
+
},
|
|
324
|
+
ctaLabel: { type: "text", label: "Button label" },
|
|
325
|
+
ctaHref: { type: "text", label: "Button link", inline: false },
|
|
326
|
+
featured: {
|
|
327
|
+
type: "select",
|
|
328
|
+
label: "Highlight this tier",
|
|
329
|
+
inline: false,
|
|
330
|
+
options: [
|
|
331
|
+
{ value: "yes", label: "Yes" },
|
|
332
|
+
{ value: "no", label: "No" },
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
settings: SECTION_SETTINGS,
|
|
339
|
+
},
|
|
340
|
+
testimonial: {
|
|
341
|
+
label: "Testimonial",
|
|
342
|
+
icon: "quote",
|
|
343
|
+
fields: {
|
|
344
|
+
quote: { type: "textarea", label: "Quote", validation: (r) => r.required() },
|
|
345
|
+
attribution: { type: "text", label: "Who said it" },
|
|
346
|
+
role: { type: "text", label: "Their role" },
|
|
347
|
+
image: { type: "image", label: "Portrait" },
|
|
348
|
+
alt: { type: "text", label: "Alt text (overrides the asset's)", inline: false },
|
|
349
|
+
},
|
|
350
|
+
settings: SECTION_SETTINGS,
|
|
351
|
+
},
|
|
352
|
+
aboutIntro: {
|
|
353
|
+
label: "About intro",
|
|
354
|
+
icon: "user",
|
|
355
|
+
fields: {
|
|
356
|
+
heading: { type: "text", label: "Heading", validation: (r) => r.required() },
|
|
357
|
+
body: { type: "richText", label: "Body" },
|
|
358
|
+
image: { type: "image", label: "Image" },
|
|
359
|
+
alt: { type: "text", label: "Alt text (overrides the asset's)", inline: false },
|
|
360
|
+
},
|
|
361
|
+
settings: SECTION_SETTINGS,
|
|
362
|
+
},
|
|
363
|
+
// --- module-adjacent -----------------------------------------------------
|
|
364
|
+
productGrid: {
|
|
365
|
+
label: "Product grid",
|
|
366
|
+
icon: "shopping-bag",
|
|
367
|
+
fields: {
|
|
368
|
+
heading: { type: "text", label: "Heading" },
|
|
369
|
+
// Deliberately hand-authored rows rather than a live catalog read. A
|
|
370
|
+
// section is stored content, and the commerce mirror is a separate
|
|
371
|
+
// concern with its own loader — a site that wants the live catalog renders
|
|
372
|
+
// `readCatalog` in its own page, not through the page-builder.
|
|
373
|
+
items: {
|
|
374
|
+
type: "array",
|
|
375
|
+
label: "Products",
|
|
376
|
+
itemLabel: "Product",
|
|
377
|
+
itemFields: {
|
|
378
|
+
name: { type: "text", label: "Name", validation: (r) => r.required() },
|
|
379
|
+
price: { type: "text", label: "Price" },
|
|
380
|
+
image: { type: "image", label: "Image" },
|
|
381
|
+
alt: { type: "text", label: "Alt text (overrides the asset's)", inline: false },
|
|
382
|
+
href: { type: "text", label: "Links to", inline: false },
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
settings: SECTION_SETTINGS,
|
|
387
|
+
},
|
|
388
|
+
locationHours: {
|
|
389
|
+
label: "Location & hours",
|
|
390
|
+
icon: "map-pin",
|
|
391
|
+
fields: {
|
|
392
|
+
heading: { type: "text", label: "Heading" },
|
|
393
|
+
address: { type: "textarea", label: "Address" },
|
|
394
|
+
phone: { type: "text", label: "Phone" },
|
|
395
|
+
items: {
|
|
396
|
+
type: "array",
|
|
397
|
+
label: "Hours",
|
|
398
|
+
itemLabel: "Day",
|
|
399
|
+
itemFields: {
|
|
400
|
+
day: { type: "text", label: "Day", validation: (r) => r.required() },
|
|
401
|
+
hours: { type: "text", label: "Hours" },
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
settings: SECTION_SETTINGS,
|
|
406
|
+
},
|
|
407
|
+
contact: {
|
|
408
|
+
label: "Contact",
|
|
409
|
+
icon: "mail",
|
|
410
|
+
fields: {
|
|
411
|
+
heading: { type: "text", label: "Heading" },
|
|
412
|
+
blurb: { type: "textarea", label: "Blurb" },
|
|
413
|
+
},
|
|
414
|
+
settings: SECTION_SETTINGS,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* Does this `_type` have a shipped component? Narrows the untrusted `_type` of a
|
|
419
|
+
* stored item, so `<Sections>` can skip an unknown one instead of rendering a
|
|
420
|
+
* hole. (Unknown types are legitimate mid-migration, and `validateSections`
|
|
421
|
+
* already rejects them on write.)
|
|
422
|
+
*/
|
|
423
|
+
export function isRenderableSection(type) {
|
|
424
|
+
return Object.hasOwn(astroidSectionCatalog, type);
|
|
425
|
+
}
|