@weatherboard/cms 0.5.0 → 0.7.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 (49) hide show
  1. package/README.md +117 -0
  2. package/dist/contracts/query.d.cts +3 -0
  3. package/dist/contracts/query.d.mts +3 -0
  4. package/dist/contracts/query.d.ts +3 -0
  5. package/dist/index.cjs +5 -1
  6. package/dist/index.cjs.map +4 -4
  7. package/dist/index.d.cts +3 -0
  8. package/dist/index.d.mts +3 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.mjs +5 -1
  11. package/dist/index.mjs.map +4 -4
  12. package/dist/next.cjs +5 -1
  13. package/dist/next.cjs.map +4 -4
  14. package/dist/next.d.cts +3 -0
  15. package/dist/next.d.mts +3 -0
  16. package/dist/next.d.ts +3 -0
  17. package/dist/next.mjs +5 -1
  18. package/dist/next.mjs.map +4 -4
  19. package/dist/portable/menu-options.d.cts +8 -0
  20. package/dist/portable/menu-options.d.mts +8 -0
  21. package/dist/portable/menu-options.d.ts +8 -0
  22. package/dist/portable/menu.d.cts +69 -0
  23. package/dist/portable/menu.d.mts +69 -0
  24. package/dist/portable/menu.d.ts +69 -0
  25. package/dist/portable/sections.d.cts +50 -7
  26. package/dist/portable/sections.d.mts +50 -7
  27. package/dist/portable/sections.d.ts +50 -7
  28. package/dist/portable/site-content.d.cts +41 -10
  29. package/dist/portable/site-content.d.mts +41 -10
  30. package/dist/portable/site-content.d.ts +41 -10
  31. package/dist/sanity.cjs +1 -1
  32. package/dist/sanity.cjs.map +2 -2
  33. package/dist/sanity.mjs +1 -1
  34. package/dist/sanity.mjs.map +2 -2
  35. package/dist/server.cjs +5 -1
  36. package/dist/server.cjs.map +4 -4
  37. package/dist/server.d.cts +3 -0
  38. package/dist/server.d.mts +3 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/server.mjs +5 -1
  41. package/dist/server.mjs.map +4 -4
  42. package/dist/site-content.cjs +10 -3
  43. package/dist/site-content.cjs.map +4 -4
  44. package/dist/site-content.d.cts +2 -2
  45. package/dist/site-content.d.mts +2 -2
  46. package/dist/site-content.d.ts +2 -2
  47. package/dist/site-content.mjs +10 -3
  48. package/dist/site-content.mjs.map +4 -4
  49. package/package.json +2 -1
@@ -0,0 +1,69 @@
1
+ import { MENU_LAYOUTS, MENU_DECORATIONS } from "./menu-options.js";
2
+ export { MENU_LAYOUTS, MENU_DECORATIONS, MENU_LAYOUT_DEFAULTS } from "./menu-options.js";
3
+ export type MenuLayout = (typeof MENU_LAYOUTS)[number];
4
+ export type MenuDecoration = (typeof MENU_DECORATIONS)[number];
5
+ /** CMS-owned saved shapes. `_key` remains the mapper's stable array identity. */
6
+ export type MenuMoney = {
7
+ readonly minor: number;
8
+ readonly currency: string;
9
+ };
10
+ export type MenuPrice = {
11
+ readonly _key: string;
12
+ readonly amount: MenuMoney;
13
+ readonly label?: string;
14
+ };
15
+ export type MenuExtra = {
16
+ readonly _key: string;
17
+ readonly name: string;
18
+ readonly amount: MenuMoney;
19
+ };
20
+ export type MenuItem = {
21
+ readonly _key: string;
22
+ readonly name: string;
23
+ readonly description?: string;
24
+ readonly prices: readonly MenuPrice[];
25
+ readonly priceNote?: string;
26
+ readonly extras: readonly MenuExtra[];
27
+ readonly dietary: readonly string[];
28
+ readonly allergenNote?: string;
29
+ readonly availability: "available" | "sold-out" | "hidden";
30
+ };
31
+ export type MenuSection = {
32
+ readonly _key: string;
33
+ readonly title: string;
34
+ readonly service?: string;
35
+ readonly items: readonly MenuItem[];
36
+ };
37
+ export type MenuVariant = {
38
+ readonly _key: string;
39
+ readonly label: string;
40
+ readonly note?: string;
41
+ readonly sections: readonly MenuSection[];
42
+ };
43
+ export type MenuDocument = {
44
+ /** The schema discriminator is required by the consuming site's mapper. */
45
+ readonly _type: "menu";
46
+ readonly _id: string;
47
+ readonly _rev: string;
48
+ /** The real published update date, preserved verbatim for stale displays. */
49
+ readonly _updatedAt: string;
50
+ readonly title: string;
51
+ readonly locationKeys: readonly string[];
52
+ readonly variants: readonly MenuVariant[];
53
+ readonly note?: string;
54
+ /** Approval is opaque to this reader. The site validates it with its contract. */
55
+ readonly signOff?: unknown;
56
+ };
57
+ export type MenuSectionInput = {
58
+ readonly kind: "menu";
59
+ readonly layout: MenuLayout;
60
+ readonly decoration: MenuDecoration;
61
+ readonly menu: MenuDocument;
62
+ };
63
+ /** Uses the same scoped type/id tags as every other CMS query. */
64
+ export declare function menuTags(scope: string, id?: string): readonly string[];
65
+ /** Validate saved field shapes without inferring approval or changing copy/order. */
66
+ export declare function parseMenuDocument(document: unknown, where?: string): MenuDocument;
67
+ /** Page references use the same single snapshot before checking their identity. */
68
+ export declare function parseReferencedMenuDocument(document: unknown, where: string): MenuDocument | null;
69
+ export declare function validMenuFallback(value: unknown): value is MenuDocument | null;
@@ -2,8 +2,8 @@
2
2
  * A-1915 (A-1739 M1c). The reader's half of the shared section set that
3
3
  * `@weatherboard/sanity-schema-base` declares in `src/sections.ts` (A-1889):
4
4
  * `page.sections[]`, drawn from the four home section types plus
5
- * `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock` and
6
- * `contactBlock`.
5
+ * `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock`,
6
+ * `contactBlock`, `textItemsBlock`, `stepsBlock` and `linkListBlock`.
7
7
  *
8
8
  * ===========================================================================
9
9
  * LAYOUTS ARE THE A-1876 VARIANT NAMES, EXACTLY
@@ -33,7 +33,8 @@
33
33
  * whose every photo was dropped, a contact
34
34
  * section switched off and empty) is skipped
35
35
  */
36
- import type { ActionInput, CompositionBlockInput, CopyInput } from "./site-content.cjs";
36
+ import type { MenuSectionInput } from "./menu.cjs";
37
+ import type { ActionInput, CompositionBlockInput, CopyInput, RichBody } from "./site-content.cjs";
37
38
  /** `Split`: text with media. */
38
39
  export declare const SPLIT_LAYOUTS: readonly ["media-end", "media-start", "media-below"];
39
40
  /** `HeadingList`: a heading column and a list column. */
@@ -59,6 +60,10 @@ export declare const READER_SECTION_KINDS: {
59
60
  readonly faqBlock: "faq";
60
61
  readonly galleryBlock: "gallery";
61
62
  readonly contactBlock: "contact";
63
+ readonly textItemsBlock: "text_items";
64
+ readonly stepsBlock: "steps";
65
+ readonly linkListBlock: "link_list";
66
+ readonly menuBlock: "menu";
62
67
  };
63
68
  /** A gallery is a `StaggerRow`, which holds two to four items. */
64
69
  export declare const READER_GALLERY_PHOTO_MIN = 2;
@@ -69,6 +74,7 @@ export declare const SECTION_WARNINGS: {
69
74
  readonly imageMissingAlt: "section.image.missing-alt";
70
75
  readonly imageMissingAsset: "section.image.missing-asset";
71
76
  readonly emptySkipped: "section.empty.skipped";
77
+ readonly menuUnavailable: "section.menu.unavailable";
72
78
  };
73
79
  export type SectionWarningCode = (typeof SECTION_WARNINGS)[keyof typeof SECTION_WARNINGS];
74
80
  /** A named warning and the path in the stored document it is about. Never copy. */
@@ -77,7 +83,10 @@ export type SectionWarning = {
77
83
  readonly where: string;
78
84
  readonly type: string;
79
85
  } | {
80
- readonly code: Exclude<SectionWarningCode, typeof SECTION_WARNINGS.unknownType>;
86
+ readonly code: typeof SECTION_WARNINGS.menuUnavailable;
87
+ readonly where: string;
88
+ } | {
89
+ readonly code: Exclude<SectionWarningCode, typeof SECTION_WARNINGS.unknownType | typeof SECTION_WARNINGS.menuUnavailable>;
81
90
  readonly where: string;
82
91
  };
83
92
  /** A Sanity image as a site needs it. `asset` is the image asset's id; `alt` is never empty. */
@@ -105,12 +114,15 @@ export type FaqSectionInput = {
105
114
  readonly kind: "faq";
106
115
  readonly anchor: string | null;
107
116
  readonly layout: HeadingListLayout;
108
- readonly heading: CopyInput;
117
+ readonly heading: CopyInput | null;
109
118
  readonly tail: CopyInput | null;
110
119
  readonly intro: CopyInput | null;
111
120
  readonly items: readonly {
112
121
  readonly question: CopyInput;
113
- readonly answer: CopyInput;
122
+ readonly answer: CopyInput | {
123
+ readonly kind: "rich";
124
+ readonly body: RichBody;
125
+ };
114
126
  }[];
115
127
  };
116
128
  export type GallerySectionInput = {
@@ -133,7 +145,38 @@ export type ContactSectionInput = {
133
145
  readonly action: ActionInput | null;
134
146
  readonly image: SectionImageInput | null;
135
147
  };
136
- export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput;
148
+ export type TextItemsSectionInput = {
149
+ readonly kind: "text_items";
150
+ readonly anchor: string | null;
151
+ readonly layout: StaggerRowLayout;
152
+ readonly items: readonly {
153
+ readonly title: CopyInput;
154
+ readonly body: CopyInput;
155
+ }[];
156
+ };
157
+ export type StepsSectionInput = {
158
+ readonly kind: "steps";
159
+ readonly anchor: string | null;
160
+ readonly layout: HeadingListLayout;
161
+ readonly heading: CopyInput;
162
+ readonly intro: CopyInput;
163
+ readonly items: readonly {
164
+ readonly title: CopyInput;
165
+ readonly body: CopyInput;
166
+ }[];
167
+ };
168
+ export type LinkListSectionInput = {
169
+ readonly kind: "link_list";
170
+ readonly anchor: string | null;
171
+ readonly layout: HeadingListLayout;
172
+ readonly heading: CopyInput;
173
+ readonly tail: CopyInput | null;
174
+ readonly links: readonly {
175
+ readonly title: string;
176
+ readonly href: string;
177
+ }[];
178
+ };
179
+ export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput | TextItemsSectionInput | StepsSectionInput | LinkListSectionInput | MenuSectionInput;
137
180
  /** One entry of `PageContent.sections`: a home section kind or one of the new kinds. */
138
181
  export type PageSectionInput = Exclude<CompositionBlockInput, {
139
182
  readonly kind: "campaign";
@@ -2,8 +2,8 @@
2
2
  * A-1915 (A-1739 M1c). The reader's half of the shared section set that
3
3
  * `@weatherboard/sanity-schema-base` declares in `src/sections.ts` (A-1889):
4
4
  * `page.sections[]`, drawn from the four home section types plus
5
- * `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock` and
6
- * `contactBlock`.
5
+ * `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock`,
6
+ * `contactBlock`, `textItemsBlock`, `stepsBlock` and `linkListBlock`.
7
7
  *
8
8
  * ===========================================================================
9
9
  * LAYOUTS ARE THE A-1876 VARIANT NAMES, EXACTLY
@@ -33,7 +33,8 @@
33
33
  * whose every photo was dropped, a contact
34
34
  * section switched off and empty) is skipped
35
35
  */
36
- import type { ActionInput, CompositionBlockInput, CopyInput } from "./site-content.mjs";
36
+ import type { MenuSectionInput } from "./menu.mjs";
37
+ import type { ActionInput, CompositionBlockInput, CopyInput, RichBody } from "./site-content.mjs";
37
38
  /** `Split`: text with media. */
38
39
  export declare const SPLIT_LAYOUTS: readonly ["media-end", "media-start", "media-below"];
39
40
  /** `HeadingList`: a heading column and a list column. */
@@ -59,6 +60,10 @@ export declare const READER_SECTION_KINDS: {
59
60
  readonly faqBlock: "faq";
60
61
  readonly galleryBlock: "gallery";
61
62
  readonly contactBlock: "contact";
63
+ readonly textItemsBlock: "text_items";
64
+ readonly stepsBlock: "steps";
65
+ readonly linkListBlock: "link_list";
66
+ readonly menuBlock: "menu";
62
67
  };
63
68
  /** A gallery is a `StaggerRow`, which holds two to four items. */
64
69
  export declare const READER_GALLERY_PHOTO_MIN = 2;
@@ -69,6 +74,7 @@ export declare const SECTION_WARNINGS: {
69
74
  readonly imageMissingAlt: "section.image.missing-alt";
70
75
  readonly imageMissingAsset: "section.image.missing-asset";
71
76
  readonly emptySkipped: "section.empty.skipped";
77
+ readonly menuUnavailable: "section.menu.unavailable";
72
78
  };
73
79
  export type SectionWarningCode = (typeof SECTION_WARNINGS)[keyof typeof SECTION_WARNINGS];
74
80
  /** A named warning and the path in the stored document it is about. Never copy. */
@@ -77,7 +83,10 @@ export type SectionWarning = {
77
83
  readonly where: string;
78
84
  readonly type: string;
79
85
  } | {
80
- readonly code: Exclude<SectionWarningCode, typeof SECTION_WARNINGS.unknownType>;
86
+ readonly code: typeof SECTION_WARNINGS.menuUnavailable;
87
+ readonly where: string;
88
+ } | {
89
+ readonly code: Exclude<SectionWarningCode, typeof SECTION_WARNINGS.unknownType | typeof SECTION_WARNINGS.menuUnavailable>;
81
90
  readonly where: string;
82
91
  };
83
92
  /** A Sanity image as a site needs it. `asset` is the image asset's id; `alt` is never empty. */
@@ -105,12 +114,15 @@ export type FaqSectionInput = {
105
114
  readonly kind: "faq";
106
115
  readonly anchor: string | null;
107
116
  readonly layout: HeadingListLayout;
108
- readonly heading: CopyInput;
117
+ readonly heading: CopyInput | null;
109
118
  readonly tail: CopyInput | null;
110
119
  readonly intro: CopyInput | null;
111
120
  readonly items: readonly {
112
121
  readonly question: CopyInput;
113
- readonly answer: CopyInput;
122
+ readonly answer: CopyInput | {
123
+ readonly kind: "rich";
124
+ readonly body: RichBody;
125
+ };
114
126
  }[];
115
127
  };
116
128
  export type GallerySectionInput = {
@@ -133,7 +145,38 @@ export type ContactSectionInput = {
133
145
  readonly action: ActionInput | null;
134
146
  readonly image: SectionImageInput | null;
135
147
  };
136
- export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput;
148
+ export type TextItemsSectionInput = {
149
+ readonly kind: "text_items";
150
+ readonly anchor: string | null;
151
+ readonly layout: StaggerRowLayout;
152
+ readonly items: readonly {
153
+ readonly title: CopyInput;
154
+ readonly body: CopyInput;
155
+ }[];
156
+ };
157
+ export type StepsSectionInput = {
158
+ readonly kind: "steps";
159
+ readonly anchor: string | null;
160
+ readonly layout: HeadingListLayout;
161
+ readonly heading: CopyInput;
162
+ readonly intro: CopyInput;
163
+ readonly items: readonly {
164
+ readonly title: CopyInput;
165
+ readonly body: CopyInput;
166
+ }[];
167
+ };
168
+ export type LinkListSectionInput = {
169
+ readonly kind: "link_list";
170
+ readonly anchor: string | null;
171
+ readonly layout: HeadingListLayout;
172
+ readonly heading: CopyInput;
173
+ readonly tail: CopyInput | null;
174
+ readonly links: readonly {
175
+ readonly title: string;
176
+ readonly href: string;
177
+ }[];
178
+ };
179
+ export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput | TextItemsSectionInput | StepsSectionInput | LinkListSectionInput | MenuSectionInput;
137
180
  /** One entry of `PageContent.sections`: a home section kind or one of the new kinds. */
138
181
  export type PageSectionInput = Exclude<CompositionBlockInput, {
139
182
  readonly kind: "campaign";
@@ -2,8 +2,8 @@
2
2
  * A-1915 (A-1739 M1c). The reader's half of the shared section set that
3
3
  * `@weatherboard/sanity-schema-base` declares in `src/sections.ts` (A-1889):
4
4
  * `page.sections[]`, drawn from the four home section types plus
5
- * `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock` and
6
- * `contactBlock`.
5
+ * `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock`,
6
+ * `contactBlock`, `textItemsBlock`, `stepsBlock` and `linkListBlock`.
7
7
  *
8
8
  * ===========================================================================
9
9
  * LAYOUTS ARE THE A-1876 VARIANT NAMES, EXACTLY
@@ -33,7 +33,8 @@
33
33
  * whose every photo was dropped, a contact
34
34
  * section switched off and empty) is skipped
35
35
  */
36
- import type { ActionInput, CompositionBlockInput, CopyInput } from "./site-content.js";
36
+ import type { MenuSectionInput } from "./menu.js";
37
+ import type { ActionInput, CompositionBlockInput, CopyInput, RichBody } from "./site-content.js";
37
38
  /** `Split`: text with media. */
38
39
  export declare const SPLIT_LAYOUTS: readonly ["media-end", "media-start", "media-below"];
39
40
  /** `HeadingList`: a heading column and a list column. */
@@ -59,6 +60,10 @@ export declare const READER_SECTION_KINDS: {
59
60
  readonly faqBlock: "faq";
60
61
  readonly galleryBlock: "gallery";
61
62
  readonly contactBlock: "contact";
63
+ readonly textItemsBlock: "text_items";
64
+ readonly stepsBlock: "steps";
65
+ readonly linkListBlock: "link_list";
66
+ readonly menuBlock: "menu";
62
67
  };
63
68
  /** A gallery is a `StaggerRow`, which holds two to four items. */
64
69
  export declare const READER_GALLERY_PHOTO_MIN = 2;
@@ -69,6 +74,7 @@ export declare const SECTION_WARNINGS: {
69
74
  readonly imageMissingAlt: "section.image.missing-alt";
70
75
  readonly imageMissingAsset: "section.image.missing-asset";
71
76
  readonly emptySkipped: "section.empty.skipped";
77
+ readonly menuUnavailable: "section.menu.unavailable";
72
78
  };
73
79
  export type SectionWarningCode = (typeof SECTION_WARNINGS)[keyof typeof SECTION_WARNINGS];
74
80
  /** A named warning and the path in the stored document it is about. Never copy. */
@@ -77,7 +83,10 @@ export type SectionWarning = {
77
83
  readonly where: string;
78
84
  readonly type: string;
79
85
  } | {
80
- readonly code: Exclude<SectionWarningCode, typeof SECTION_WARNINGS.unknownType>;
86
+ readonly code: typeof SECTION_WARNINGS.menuUnavailable;
87
+ readonly where: string;
88
+ } | {
89
+ readonly code: Exclude<SectionWarningCode, typeof SECTION_WARNINGS.unknownType | typeof SECTION_WARNINGS.menuUnavailable>;
81
90
  readonly where: string;
82
91
  };
83
92
  /** A Sanity image as a site needs it. `asset` is the image asset's id; `alt` is never empty. */
@@ -105,12 +114,15 @@ export type FaqSectionInput = {
105
114
  readonly kind: "faq";
106
115
  readonly anchor: string | null;
107
116
  readonly layout: HeadingListLayout;
108
- readonly heading: CopyInput;
117
+ readonly heading: CopyInput | null;
109
118
  readonly tail: CopyInput | null;
110
119
  readonly intro: CopyInput | null;
111
120
  readonly items: readonly {
112
121
  readonly question: CopyInput;
113
- readonly answer: CopyInput;
122
+ readonly answer: CopyInput | {
123
+ readonly kind: "rich";
124
+ readonly body: RichBody;
125
+ };
114
126
  }[];
115
127
  };
116
128
  export type GallerySectionInput = {
@@ -133,7 +145,38 @@ export type ContactSectionInput = {
133
145
  readonly action: ActionInput | null;
134
146
  readonly image: SectionImageInput | null;
135
147
  };
136
- export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput;
148
+ export type TextItemsSectionInput = {
149
+ readonly kind: "text_items";
150
+ readonly anchor: string | null;
151
+ readonly layout: StaggerRowLayout;
152
+ readonly items: readonly {
153
+ readonly title: CopyInput;
154
+ readonly body: CopyInput;
155
+ }[];
156
+ };
157
+ export type StepsSectionInput = {
158
+ readonly kind: "steps";
159
+ readonly anchor: string | null;
160
+ readonly layout: HeadingListLayout;
161
+ readonly heading: CopyInput;
162
+ readonly intro: CopyInput;
163
+ readonly items: readonly {
164
+ readonly title: CopyInput;
165
+ readonly body: CopyInput;
166
+ }[];
167
+ };
168
+ export type LinkListSectionInput = {
169
+ readonly kind: "link_list";
170
+ readonly anchor: string | null;
171
+ readonly layout: HeadingListLayout;
172
+ readonly heading: CopyInput;
173
+ readonly tail: CopyInput | null;
174
+ readonly links: readonly {
175
+ readonly title: string;
176
+ readonly href: string;
177
+ }[];
178
+ };
179
+ export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput | TextItemsSectionInput | StepsSectionInput | LinkListSectionInput | MenuSectionInput;
137
180
  /** One entry of `PageContent.sections`: a home section kind or one of the new kinds. */
138
181
  export type PageSectionInput = Exclude<CompositionBlockInput, {
139
182
  readonly kind: "campaign";
@@ -20,9 +20,9 @@
20
20
  * anything writing over the API skips them entirely. So every document is
21
21
  * checked here again, and a malformed one is refused BY NAME
22
22
  * (`MalformedContentError`, code `malformed-content`) listing every problem
23
- * with its path, rather than rendered half-empty. A page that renders with a
24
- * missing headline and a page whose content is broken look identical to a
25
- * visitor; a build that fails naming `hero.headline` does not.
23
+ * with its path, rather than rendered half-empty. A missing home headline
24
+ * uses siteHero only when the home document explicitly selects it and that
25
+ * document supplies a valid heading. Otherwise the home page is refused.
26
26
  *
27
27
  * Absent is not malformed: `null` from `home()` means nobody has created the
28
28
  * home page yet, and a site decides what to do about that.
@@ -60,7 +60,7 @@
60
60
  * kinds, the A-1876 layout names and the warning codes.
61
61
  *
62
62
  * Two places the composition cannot carry a proposed marker: a `cta` block's
63
- * plain strings, and the campaign's title and body (which travel as Markdown).
63
+ * plain strings, and the campaign's title and body (which travel as rich body content).
64
64
  * There the text is returned plain and the line is still listed in
65
65
  * `proposedCopy`, which is the list a dashboard and a site both read for
66
66
  * approval. Nothing is dropped silently.
@@ -71,6 +71,30 @@ export type CopyInput = string | {
71
71
  readonly text: string;
72
72
  readonly copySource: "captured" | "proposed";
73
73
  };
74
+ export type RichInline = {
75
+ readonly children: readonly {
76
+ readonly text: string;
77
+ readonly marks: readonly string[];
78
+ }[];
79
+ readonly markDefs: readonly {
80
+ readonly key: string;
81
+ readonly href: string;
82
+ }[];
83
+ };
84
+ export type RichBodyPart = {
85
+ readonly kind: "block";
86
+ readonly style: "normal" | "h2" | "h3" | "blockquote";
87
+ readonly listItem: "bullet" | "number" | null;
88
+ readonly level: number;
89
+ readonly content: RichInline;
90
+ } | {
91
+ readonly kind: "heading";
92
+ readonly text: string;
93
+ } | {
94
+ readonly kind: "table";
95
+ readonly rows: readonly (readonly RichInline[])[];
96
+ };
97
+ export type RichBody = string | readonly RichBodyPart[];
74
98
  export type ActionInput = {
75
99
  readonly label: CopyInput;
76
100
  readonly href: string;
@@ -110,12 +134,14 @@ export type CompositionBlockInput = {
110
134
  } | {
111
135
  readonly kind: "accent_band";
112
136
  readonly heading: CopyInput;
137
+ readonly tail?: CopyInput | null;
113
138
  readonly body: CopyInput;
114
139
  readonly action: ActionInput | null;
115
140
  readonly image: string | null;
116
141
  } | {
117
142
  readonly kind: "cta";
118
143
  readonly heading: string;
144
+ readonly image?: string | null;
119
145
  readonly body: string | null;
120
146
  readonly link: {
121
147
  readonly label: string;
@@ -136,16 +162,17 @@ export type ProposedCopy = {
136
162
  export type CampaignState = "shown" | "off" | "not-started" | "ended" | "none";
137
163
  export type HomeContent = {
138
164
  readonly composition: CompositionInput;
139
- /** The page body as Markdown: the campaign's title and text when it is shown, otherwise empty. */
140
- readonly body: string;
165
+ /** Campaign text as Markdown, or structured blocks and tables when it contains a table. */
166
+ readonly body: RichBody;
141
167
  readonly campaign: CampaignState;
142
168
  readonly proposedCopy: readonly ProposedCopy[];
143
169
  };
144
170
  export type PageContent = {
145
171
  readonly slug: string;
146
172
  readonly title: string;
173
+ readonly titleTail?: string;
147
174
  readonly composition: CompositionInput;
148
- readonly body: string;
175
+ readonly body: RichBody;
149
176
  readonly migrated: boolean;
150
177
  readonly proposedCopy: readonly ProposedCopy[];
151
178
  /**
@@ -191,8 +218,10 @@ export declare const READER_SITE_SETTINGS_ID = "siteSettings";
191
218
  * upload one yet (the dashboard slice decides how), so one arriving means
192
219
  * something wrote the document by hand, and a page silently missing a picture
193
220
  * is the failure this reader exists to prevent.
221
+ * Text-only input remains a Markdown string. Input with a table produces
222
+ * ordered structured blocks and cells that never enter a Markdown parser.
194
223
  */
195
- export declare function portableTextToMarkdown(value: unknown): string;
224
+ export declare function portableTextToMarkdown(value: unknown): RichBody;
196
225
  /** Whether a campaign is shown at `now`, and if not, why not. */
197
226
  export declare function campaignState(campaign: unknown, now: Date): CampaignState;
198
227
  /**
@@ -208,8 +237,10 @@ export declare function parseContentPage(document: unknown): PageContent;
208
237
  export declare function parseSiteSettings(document: unknown): SiteSettingsContent;
209
238
  /** By fixed id: a second home page at a random id is never read. */
210
239
  export declare const HOME_PAGE_DOCUMENT_QUERY = "*[_id == $id][0]";
211
- export declare const CONTENT_PAGES_QUERY = "*[_type == \"page\" && defined(slug.current)]|order(slug.current asc)";
212
- export declare const CONTENT_PAGE_BY_SLUG_QUERY = "*[_type == \"page\" && slug.current == $slug][0]";
240
+ /** One snapshot contains both possible headline sources. */
241
+ export declare const HOME_PAGE_WITH_SITE_HERO_QUERY = "*[_id == $id][0]{..., \"_siteHero\": *[_id == \"siteHero\" && _type == \"siteHero\"][0]{heading}}";
242
+ export declare const CONTENT_PAGES_QUERY = "*[_type == \"page\" && defined(slug.current)]|order(slug.current asc){..., \"sections\": coalesce(sections[]{..., _type == \"menuBlock\" => {menu->{_type, _id, _rev, _updatedAt, title, locationKeys, note, signOff,\n variants[]{_key, label, note, sections[]{_key, title, service, items[]{\n _key, name, description, priceNote, dietary, allergenNote, availability,\n prices[]{_key, label, amount{minor, currency}}, extras[]{_key, name, amount{minor, currency}}\n }}}}}, _type == \"linkListBlock\" => {links[]{..., \"page\": page->{title, \"slug\": slug.current}}}}, sections)}";
243
+ export declare const CONTENT_PAGE_BY_SLUG_QUERY = "*[_type == \"page\" && slug.current == $slug][0]{..., \"sections\": coalesce(sections[]{..., _type == \"menuBlock\" => {menu->{_type, _id, _rev, _updatedAt, title, locationKeys, note, signOff,\n variants[]{_key, label, note, sections[]{_key, title, service, items[]{\n _key, name, description, priceNote, dietary, allergenNote, availability,\n prices[]{_key, label, amount{minor, currency}}, extras[]{_key, name, amount{minor, currency}}\n }}}}}, _type == \"linkListBlock\" => {links[]{..., \"page\": page->{title, \"slug\": slug.current}}}}, sections)}";
213
244
  export interface SiteContentReader {
214
245
  /** `null` when no home page document exists yet. */
215
246
  home: (options: {
@@ -20,9 +20,9 @@
20
20
  * anything writing over the API skips them entirely. So every document is
21
21
  * checked here again, and a malformed one is refused BY NAME
22
22
  * (`MalformedContentError`, code `malformed-content`) listing every problem
23
- * with its path, rather than rendered half-empty. A page that renders with a
24
- * missing headline and a page whose content is broken look identical to a
25
- * visitor; a build that fails naming `hero.headline` does not.
23
+ * with its path, rather than rendered half-empty. A missing home headline
24
+ * uses siteHero only when the home document explicitly selects it and that
25
+ * document supplies a valid heading. Otherwise the home page is refused.
26
26
  *
27
27
  * Absent is not malformed: `null` from `home()` means nobody has created the
28
28
  * home page yet, and a site decides what to do about that.
@@ -60,7 +60,7 @@
60
60
  * kinds, the A-1876 layout names and the warning codes.
61
61
  *
62
62
  * Two places the composition cannot carry a proposed marker: a `cta` block's
63
- * plain strings, and the campaign's title and body (which travel as Markdown).
63
+ * plain strings, and the campaign's title and body (which travel as rich body content).
64
64
  * There the text is returned plain and the line is still listed in
65
65
  * `proposedCopy`, which is the list a dashboard and a site both read for
66
66
  * approval. Nothing is dropped silently.
@@ -71,6 +71,30 @@ export type CopyInput = string | {
71
71
  readonly text: string;
72
72
  readonly copySource: "captured" | "proposed";
73
73
  };
74
+ export type RichInline = {
75
+ readonly children: readonly {
76
+ readonly text: string;
77
+ readonly marks: readonly string[];
78
+ }[];
79
+ readonly markDefs: readonly {
80
+ readonly key: string;
81
+ readonly href: string;
82
+ }[];
83
+ };
84
+ export type RichBodyPart = {
85
+ readonly kind: "block";
86
+ readonly style: "normal" | "h2" | "h3" | "blockquote";
87
+ readonly listItem: "bullet" | "number" | null;
88
+ readonly level: number;
89
+ readonly content: RichInline;
90
+ } | {
91
+ readonly kind: "heading";
92
+ readonly text: string;
93
+ } | {
94
+ readonly kind: "table";
95
+ readonly rows: readonly (readonly RichInline[])[];
96
+ };
97
+ export type RichBody = string | readonly RichBodyPart[];
74
98
  export type ActionInput = {
75
99
  readonly label: CopyInput;
76
100
  readonly href: string;
@@ -110,12 +134,14 @@ export type CompositionBlockInput = {
110
134
  } | {
111
135
  readonly kind: "accent_band";
112
136
  readonly heading: CopyInput;
137
+ readonly tail?: CopyInput | null;
113
138
  readonly body: CopyInput;
114
139
  readonly action: ActionInput | null;
115
140
  readonly image: string | null;
116
141
  } | {
117
142
  readonly kind: "cta";
118
143
  readonly heading: string;
144
+ readonly image?: string | null;
119
145
  readonly body: string | null;
120
146
  readonly link: {
121
147
  readonly label: string;
@@ -136,16 +162,17 @@ export type ProposedCopy = {
136
162
  export type CampaignState = "shown" | "off" | "not-started" | "ended" | "none";
137
163
  export type HomeContent = {
138
164
  readonly composition: CompositionInput;
139
- /** The page body as Markdown: the campaign's title and text when it is shown, otherwise empty. */
140
- readonly body: string;
165
+ /** Campaign text as Markdown, or structured blocks and tables when it contains a table. */
166
+ readonly body: RichBody;
141
167
  readonly campaign: CampaignState;
142
168
  readonly proposedCopy: readonly ProposedCopy[];
143
169
  };
144
170
  export type PageContent = {
145
171
  readonly slug: string;
146
172
  readonly title: string;
173
+ readonly titleTail?: string;
147
174
  readonly composition: CompositionInput;
148
- readonly body: string;
175
+ readonly body: RichBody;
149
176
  readonly migrated: boolean;
150
177
  readonly proposedCopy: readonly ProposedCopy[];
151
178
  /**
@@ -191,8 +218,10 @@ export declare const READER_SITE_SETTINGS_ID = "siteSettings";
191
218
  * upload one yet (the dashboard slice decides how), so one arriving means
192
219
  * something wrote the document by hand, and a page silently missing a picture
193
220
  * is the failure this reader exists to prevent.
221
+ * Text-only input remains a Markdown string. Input with a table produces
222
+ * ordered structured blocks and cells that never enter a Markdown parser.
194
223
  */
195
- export declare function portableTextToMarkdown(value: unknown): string;
224
+ export declare function portableTextToMarkdown(value: unknown): RichBody;
196
225
  /** Whether a campaign is shown at `now`, and if not, why not. */
197
226
  export declare function campaignState(campaign: unknown, now: Date): CampaignState;
198
227
  /**
@@ -208,8 +237,10 @@ export declare function parseContentPage(document: unknown): PageContent;
208
237
  export declare function parseSiteSettings(document: unknown): SiteSettingsContent;
209
238
  /** By fixed id: a second home page at a random id is never read. */
210
239
  export declare const HOME_PAGE_DOCUMENT_QUERY = "*[_id == $id][0]";
211
- export declare const CONTENT_PAGES_QUERY = "*[_type == \"page\" && defined(slug.current)]|order(slug.current asc)";
212
- export declare const CONTENT_PAGE_BY_SLUG_QUERY = "*[_type == \"page\" && slug.current == $slug][0]";
240
+ /** One snapshot contains both possible headline sources. */
241
+ export declare const HOME_PAGE_WITH_SITE_HERO_QUERY = "*[_id == $id][0]{..., \"_siteHero\": *[_id == \"siteHero\" && _type == \"siteHero\"][0]{heading}}";
242
+ export declare const CONTENT_PAGES_QUERY = "*[_type == \"page\" && defined(slug.current)]|order(slug.current asc){..., \"sections\": coalesce(sections[]{..., _type == \"menuBlock\" => {menu->{_type, _id, _rev, _updatedAt, title, locationKeys, note, signOff,\n variants[]{_key, label, note, sections[]{_key, title, service, items[]{\n _key, name, description, priceNote, dietary, allergenNote, availability,\n prices[]{_key, label, amount{minor, currency}}, extras[]{_key, name, amount{minor, currency}}\n }}}}}, _type == \"linkListBlock\" => {links[]{..., \"page\": page->{title, \"slug\": slug.current}}}}, sections)}";
243
+ export declare const CONTENT_PAGE_BY_SLUG_QUERY = "*[_type == \"page\" && slug.current == $slug][0]{..., \"sections\": coalesce(sections[]{..., _type == \"menuBlock\" => {menu->{_type, _id, _rev, _updatedAt, title, locationKeys, note, signOff,\n variants[]{_key, label, note, sections[]{_key, title, service, items[]{\n _key, name, description, priceNote, dietary, allergenNote, availability,\n prices[]{_key, label, amount{minor, currency}}, extras[]{_key, name, amount{minor, currency}}\n }}}}}, _type == \"linkListBlock\" => {links[]{..., \"page\": page->{title, \"slug\": slug.current}}}}, sections)}";
213
244
  export interface SiteContentReader {
214
245
  /** `null` when no home page document exists yet. */
215
246
  home: (options: {