@port60/template-kit 0.6.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.
@@ -1,701 +1,710 @@
1
1
  {
2
- "description": "The content-section catalogue — the closed set of section types a page may hold and a template may render. Section type = content KIND (coarse); layout/variant is the template's job. Additive-only: types and fields are added, never removed or re-shaped, within a contract major. Every section is independently omittable — a template must degrade gracefully when any subset is absent.",
3
- "sections": [
4
- {
5
- "type": "hero",
6
- "label": "Hero banner",
7
- "pages": [
8
- "about"
9
- ],
10
- "description": "The page opener: a headline with an optional kicker and sanitised rich-text intro.",
11
- "fields": [
12
- {
13
- "name": "eyebrow",
14
- "kind": "text",
15
- "required": false,
16
- "description": "Short kicker above the headline."
17
- },
18
- {
19
- "name": "title",
20
- "kind": "text",
21
- "required": true,
22
- "description": "The headline."
23
- },
24
- {
25
- "name": "bodyHtml",
26
- "kind": "richtext",
27
- "required": false,
28
- "description": "Sanitised HTML intro — render with the raw filter."
29
- }
30
- ],
31
- "minimal": {
32
- "title": "Our story"
33
- },
34
- "sample": {
35
- "eyebrow": "About us",
36
- "title": "Generosity, put to work.",
37
- "bodyHtml": "<p>We began in 2019 around a simple belief: small, regular acts of kindness, pooled together, change lives.</p>"
38
- }
39
- },
40
- {
41
- "type": "values",
42
- "label": "Values / list",
43
- "pages": [
44
- "about"
45
- ],
46
- "description": "A set of principles shown as cards or a numbered list, per the template.",
47
- "fields": [
48
- {
49
- "name": "eyebrow",
50
- "kind": "text",
51
- "required": false,
52
- "description": "Short kicker above the heading."
53
- },
54
- {
55
- "name": "heading",
56
- "kind": "text",
57
- "required": false,
58
- "description": "The section heading."
59
- },
60
- {
61
- "name": "items",
62
- "kind": "items",
63
- "required": true,
64
- "description": "The value entries.",
65
- "itemFields": [
66
- {
67
- "name": "title",
68
- "kind": "text",
69
- "required": true,
70
- "description": "The value's name."
71
- },
72
- {
73
- "name": "description",
74
- "kind": "text",
75
- "required": true,
76
- "description": "One sentence describing it."
2
+ "description": "The content-section catalogue — the closed set of section types a page may hold and a template may render. Section type = content KIND (coarse); layout/variant is the template's job. Additive-only: types and fields are added, never removed or re-shaped, within a contract major. Every section is independently omittable — a template must degrade gracefully when any subset is absent.",
3
+ "sections": [
4
+ {
5
+ "type": "hero",
6
+ "label": "Hero banner",
7
+ "pages": [
8
+ "about"
9
+ ],
10
+ "description": "The page opener: a headline with an optional kicker and sanitised rich-text intro.",
11
+ "fields": [
12
+ {
13
+ "name": "eyebrow",
14
+ "kind": "text",
15
+ "required": false,
16
+ "description": "Short kicker above the headline."
17
+ },
18
+ {
19
+ "name": "title",
20
+ "kind": "text",
21
+ "required": true,
22
+ "description": "The headline."
23
+ },
24
+ {
25
+ "name": "bodyHtml",
26
+ "kind": "richtext",
27
+ "required": false,
28
+ "description": "Sanitised HTML intro — render with the raw filter."
29
+ }
30
+ ],
31
+ "minimal": {
32
+ "title": "Our story"
77
33
  },
78
- {
79
- "name": "icon",
80
- "kind": "icon",
81
- "required": false,
82
- "description": "Optional emoji/icon."
34
+ "sample": {
35
+ "eyebrow": "About us",
36
+ "title": "Generosity, put to work.",
37
+ "bodyHtml": "<p>We began in 2019 around a simple belief: small, regular acts of kindness, pooled together, change lives.</p>"
83
38
  }
84
- ]
85
- }
86
- ],
87
- "minimal": {
88
- "items": [
89
- {
90
- "title": "Transparency",
91
- "description": "Every pound is tracked and reported."
92
- }
93
- ]
94
- },
95
- "sample": {
96
- "eyebrow": "What we stand for",
97
- "heading": "Our values",
98
- "items": [
99
- {
100
- "title": "Transparency",
101
- "description": "Every pound is tracked and reported."
102
- },
103
- {
104
- "title": "Dignity",
105
- "description": "We work alongside communities, not for them."
106
- },
107
- {
108
- "title": "Impact",
109
- "description": "We fund what works and measure the difference."
110
- }
111
- ]
112
- }
113
- },
114
- {
115
- "type": "cta",
116
- "label": "Call to action",
117
- "pages": [
118
- "home",
119
- "about"
120
- ],
121
- "description": "A closing band: heading, optional line, one button.",
122
- "fields": [
123
- {
124
- "name": "heading",
125
- "kind": "text",
126
- "required": true,
127
- "description": "The band heading."
128
- },
129
- {
130
- "name": "text",
131
- "kind": "text",
132
- "required": false,
133
- "description": "A supporting line."
134
- },
135
- {
136
- "name": "buttonLabel",
137
- "kind": "text",
138
- "required": true,
139
- "description": "The button's label."
140
39
  },
141
40
  {
142
- "name": "buttonHref",
143
- "kind": "url",
144
- "required": true,
145
- "description": "The button's destination (scheme-guarded on save). The editor's cause picker writes /donate/{slug} here and records causeId beside it (editor bookkeeping — renderers use buttonHref only and may ignore causeId)."
146
- }
147
- ],
148
- "minimal": {
149
- "heading": "Ready to help?",
150
- "buttonLabel": "Donate",
151
- "buttonHref": "/donate"
152
- },
153
- "sample": {
154
- "heading": "Ready to help?",
155
- "text": "It takes less than a minute.",
156
- "buttonLabel": "Donate now",
157
- "buttonHref": "/#donate"
158
- }
159
- },
160
- {
161
- "type": "homeHero",
162
- "label": "Home hero",
163
- "pages": [
164
- "home"
165
- ],
166
- "description": "The home opener. EVERY field optional: eyebrow/title derive from branding when absent, the lead OMITS when absent (never a fabricated placeholder), buttons fall back to platform defaults.",
167
- "fields": [
168
- {
169
- "name": "eyebrow",
170
- "kind": "text",
171
- "required": false,
172
- "description": "Defaults to the charity's name."
173
- },
174
- {
175
- "name": "title",
176
- "kind": "text",
177
- "required": false,
178
- "description": "Defaults to the charity's tagline."
179
- },
180
- {
181
- "name": "lead",
182
- "kind": "text",
183
- "required": false,
184
- "description": "A lead paragraph; omitted entirely when blank."
185
- },
186
- {
187
- "name": "primaryLabel",
188
- "kind": "text",
189
- "required": false,
190
- "description": "Main button label (default: Donate now)."
191
- },
192
- {
193
- "name": "primaryHref",
194
- "kind": "url",
195
- "required": false,
196
- "description": "Main button link (default: #donate)."
197
- },
198
- {
199
- "name": "secondaryLabel",
200
- "kind": "text",
201
- "required": false,
202
- "description": "Second button label (default: Our story)."
203
- },
204
- {
205
- "name": "secondaryHref",
206
- "kind": "url",
207
- "required": false,
208
- "description": "Second button link (default: /about)."
209
- },
210
- {
211
- "name": "givingStyle",
212
- "kind": "select",
213
- "required": false,
214
- "options": [
215
- "form",
216
- "button"
217
- ],
218
- "description": "How this hero offers giving: 'form' (default) places the donation widget beside the copy; 'button' renders copy only — the primary button becomes the giving path (defaulting to /donate). Templates MUST render both."
219
- },
220
- {
221
- "name": "imageUrl",
222
- "kind": "image",
223
- "required": false,
224
- "description": "Background photograph for the hero (uploaded through the platform pipeline). Superseded by `images` when that carries any entry — render imageUrl only when images is absent/empty. The template must TREAT the photo (scrim/tint from its own palette variables), never place it raw, and omit for its designed no-photo state."
225
- },
226
- {
227
- "name": "photoFraming",
228
- "kind": "select",
229
- "required": false,
230
- "options": [
231
- "fill",
232
- "whole"
233
- ],
234
- "description": "How hero photographs meet the hero. 'fill' (default): the hero keeps its designed shape and photos crop to fill it (use each photo's focus to pick the surviving slice). 'whole': the hero adapts toward the photo's own shape (height-clamped) so the FULL scene stays visible — the vista treatment for wide landscape photographs; templates overlay or stack their copy accordingly."
235
- },
236
- {
237
- "name": "images",
238
- "kind": "items",
239
- "required": false,
240
- "description": "Hero photographs (0–6, uploaded through the platform pipeline). One = a main image; several = rotation material — a template with supports.heroImagery renders at least the first and MAY place the hero_carousel island (or a CSS scroll strip) for the rest. Like every field: absent means the template's designed no-photo state, never a placeholder.",
241
- "itemFields": [
242
- {
243
- "name": "imageUrl",
244
- "kind": "image",
245
- "required": true,
246
- "description": "The photograph."
41
+ "type": "values",
42
+ "label": "Values / list",
43
+ "pages": [
44
+ "about"
45
+ ],
46
+ "description": "A set of principles shown as cards or a numbered list, per the template.",
47
+ "fields": [
48
+ {
49
+ "name": "eyebrow",
50
+ "kind": "text",
51
+ "required": false,
52
+ "description": "Short kicker above the heading."
53
+ },
54
+ {
55
+ "name": "heading",
56
+ "kind": "text",
57
+ "required": false,
58
+ "description": "The section heading."
59
+ },
60
+ {
61
+ "name": "items",
62
+ "kind": "items",
63
+ "required": true,
64
+ "description": "The value entries.",
65
+ "itemFields": [
66
+ {
67
+ "name": "title",
68
+ "kind": "text",
69
+ "required": true,
70
+ "description": "The value's name."
71
+ },
72
+ {
73
+ "name": "description",
74
+ "kind": "text",
75
+ "required": true,
76
+ "description": "One sentence describing it."
77
+ },
78
+ {
79
+ "name": "icon",
80
+ "kind": "icon",
81
+ "required": false,
82
+ "description": "Optional emoji/icon."
83
+ }
84
+ ]
85
+ }
86
+ ],
87
+ "minimal": {
88
+ "items": [
89
+ {
90
+ "title": "Transparency",
91
+ "description": "Every pound is tracked and reported."
92
+ }
93
+ ]
247
94
  },
248
- {
249
- "name": "alt",
250
- "kind": "text",
251
- "required": false,
252
- "description": "Accessible description; empty renders as decorative (alt=\"\")."
253
- },
254
- {
255
- "name": "focus",
256
- "kind": "select",
257
- "required": false,
258
- "options": [
259
- "centre",
260
- "top",
261
- "bottom",
262
- "left",
263
- "right"
264
- ],
265
- "description": "Which part of the photograph survives the fill-crop (templates render photos cover-style, so an aspect mismatch crops — this picks the slice that stays). Maps to background-position/object-position; absent means centre."
95
+ "sample": {
96
+ "eyebrow": "What we stand for",
97
+ "heading": "Our values",
98
+ "items": [
99
+ {
100
+ "title": "Transparency",
101
+ "description": "Every pound is tracked and reported."
102
+ },
103
+ {
104
+ "title": "Dignity",
105
+ "description": "We work alongside communities, not for them."
106
+ },
107
+ {
108
+ "title": "Impact",
109
+ "description": "We fund what works and measure the difference."
110
+ }
111
+ ]
266
112
  }
267
- ]
268
- }
269
- ],
270
- "minimal": {},
271
- "sample": {
272
- "lead": "We turn everyday generosity into practical help for the families who need it most.",
273
- "givingStyle": "button",
274
- "images": [
275
- {
276
- "imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 90'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23355e4b'/%3E%3Cstop offset='1' stop-color='%23103528'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='160' height='90' fill='url(%23g)'/%3E%3Ccircle cx='118' cy='28' r='14' fill='%23baa769' opacity='.7'/%3E%3Ctext x='10' y='78' font-family='sans-serif' font-size='9' fill='%23ffffff' opacity='.75'%3EHero photo one%3C/text%3E%3C/svg%3E",
277
- "alt": "Volunteers serving meals at dusk"
278
- },
279
- {
280
- "imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 90'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23445a78'/%3E%3Cstop offset='1' stop-color='%23141d2c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='160' height='90' fill='url(%23g)'/%3E%3Ccircle cx='40' cy='30' r='12' fill='%23e8e2d4' opacity='.5'/%3E%3Ctext x='10' y='78' font-family='sans-serif' font-size='9' fill='%23ffffff' opacity='.75'%3EHero photo two%3C/text%3E%3C/svg%3E",
281
- "alt": "The community hall on open day"
282
- }
283
- ]
284
- }
285
- },
286
- {
287
- "type": "impact",
288
- "label": "Impact cards",
289
- "pages": [
290
- "home"
291
- ],
292
- "description": "What gifts make possible — optional intro plus icon/title/description cards. Claims must be truthful for the tenant.",
293
- "fields": [
294
- {
295
- "name": "eyebrow",
296
- "kind": "text",
297
- "required": false,
298
- "description": "Short kicker."
299
113
  },
300
114
  {
301
- "name": "heading",
302
- "kind": "text",
303
- "required": false,
304
- "description": "The section heading."
115
+ "type": "cta",
116
+ "label": "Call to action",
117
+ "pages": [
118
+ "home",
119
+ "about"
120
+ ],
121
+ "description": "A closing band: heading, optional line, one button.",
122
+ "fields": [
123
+ {
124
+ "name": "heading",
125
+ "kind": "text",
126
+ "required": true,
127
+ "description": "The band heading."
128
+ },
129
+ {
130
+ "name": "text",
131
+ "kind": "text",
132
+ "required": false,
133
+ "description": "A supporting line."
134
+ },
135
+ {
136
+ "name": "buttonLabel",
137
+ "kind": "text",
138
+ "required": true,
139
+ "description": "The button's label."
140
+ },
141
+ {
142
+ "name": "buttonHref",
143
+ "kind": "url",
144
+ "required": true,
145
+ "description": "The button's destination (scheme-guarded on save). The editor's cause picker writes /donate/{slug} here and records causeId beside it (editor bookkeeping — renderers use buttonHref only and may ignore causeId)."
146
+ }
147
+ ],
148
+ "minimal": {
149
+ "heading": "Ready to help?",
150
+ "buttonLabel": "Donate",
151
+ "buttonHref": "/donate"
152
+ },
153
+ "sample": {
154
+ "heading": "Ready to help?",
155
+ "text": "It takes less than a minute.",
156
+ "buttonLabel": "Donate now",
157
+ "buttonHref": "/#donate"
158
+ }
305
159
  },
306
160
  {
307
- "name": "text",
308
- "kind": "text",
309
- "required": false,
310
- "description": "An intro line."
161
+ "type": "homeHero",
162
+ "label": "Home hero",
163
+ "pages": [
164
+ "home"
165
+ ],
166
+ "description": "The home opener. EVERY field optional: eyebrow/title derive from branding when absent, the lead OMITS when absent (never a fabricated placeholder), buttons fall back to platform defaults.",
167
+ "fields": [
168
+ {
169
+ "name": "eyebrow",
170
+ "kind": "text",
171
+ "required": false,
172
+ "description": "Defaults to the charity's name."
173
+ },
174
+ {
175
+ "name": "title",
176
+ "kind": "text",
177
+ "required": false,
178
+ "description": "Defaults to the charity's tagline."
179
+ },
180
+ {
181
+ "name": "lead",
182
+ "kind": "text",
183
+ "required": false,
184
+ "description": "A lead paragraph; omitted entirely when blank."
185
+ },
186
+ {
187
+ "name": "primaryLabel",
188
+ "kind": "text",
189
+ "required": false,
190
+ "description": "Main button label (default: Donate now)."
191
+ },
192
+ {
193
+ "name": "primaryHref",
194
+ "kind": "url",
195
+ "required": false,
196
+ "description": "Main button link (default: #donate)."
197
+ },
198
+ {
199
+ "name": "secondaryLabel",
200
+ "kind": "text",
201
+ "required": false,
202
+ "description": "Second button label (default: Our story)."
203
+ },
204
+ {
205
+ "name": "secondaryHref",
206
+ "kind": "url",
207
+ "required": false,
208
+ "description": "Second button link (default: /about)."
209
+ },
210
+ {
211
+ "name": "givingStyle",
212
+ "kind": "select",
213
+ "required": false,
214
+ "options": [
215
+ "form",
216
+ "button"
217
+ ],
218
+ "description": "How this hero offers giving: 'form' (default) places the donation widget beside the copy; 'button' renders copy only — the primary button becomes the giving path (defaulting to /donate). Templates MUST render both."
219
+ },
220
+ {
221
+ "name": "imageUrl",
222
+ "kind": "image",
223
+ "required": false,
224
+ "description": "Background photograph for the hero (uploaded through the platform pipeline). Superseded by `images` when that carries any entry — render imageUrl only when images is absent/empty. The template must TREAT the photo (scrim/tint from its own palette variables), never place it raw, and omit for its designed no-photo state."
225
+ },
226
+ {
227
+ "name": "photoFraming",
228
+ "kind": "select",
229
+ "required": false,
230
+ "options": [
231
+ "fill",
232
+ "whole"
233
+ ],
234
+ "description": "How hero photographs meet the hero. 'fill' (default): the hero keeps its designed shape and photos crop to fill it (use each photo's focus to pick the surviving slice). 'whole': the hero adapts toward the photo's own shape (height-clamped) so the FULL scene stays visible — the vista treatment for wide landscape photographs; templates overlay or stack their copy accordingly."
235
+ },
236
+ {
237
+ "name": "images",
238
+ "kind": "items",
239
+ "required": false,
240
+ "description": "Hero photographs (0–6, uploaded through the platform pipeline). One = a main image; several = rotation material — a template with supports.heroImagery renders at least the first and MAY place the hero_carousel island (or a CSS scroll strip) for the rest. Like every field: absent means the template's designed no-photo state, never a placeholder.",
241
+ "itemFields": [
242
+ {
243
+ "name": "imageUrl",
244
+ "kind": "image",
245
+ "required": true,
246
+ "description": "The photograph."
247
+ },
248
+ {
249
+ "name": "alt",
250
+ "kind": "text",
251
+ "required": false,
252
+ "description": "Accessible description; empty renders as decorative (alt=\"\")."
253
+ },
254
+ {
255
+ "name": "focus",
256
+ "kind": "select",
257
+ "required": false,
258
+ "options": [
259
+ "centre",
260
+ "top",
261
+ "bottom",
262
+ "left",
263
+ "right"
264
+ ],
265
+ "description": "Which part of the photograph survives the fill-crop (templates render photos cover-style, so an aspect mismatch crops — this picks the slice that stays). Maps to background-position/object-position; absent means centre."
266
+ }
267
+ ]
268
+ }
269
+ ],
270
+ "minimal": {},
271
+ "sample": {
272
+ "lead": "We turn everyday generosity into practical help for the families who need it most.",
273
+ "givingStyle": "button",
274
+ "images": [
275
+ {
276
+ "imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 90'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23355e4b'/%3E%3Cstop offset='1' stop-color='%23103528'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='160' height='90' fill='url(%23g)'/%3E%3Ccircle cx='118' cy='28' r='14' fill='%23baa769' opacity='.7'/%3E%3Ctext x='10' y='78' font-family='sans-serif' font-size='9' fill='%23ffffff' opacity='.75'%3EHero photo one%3C/text%3E%3C/svg%3E",
277
+ "alt": "Volunteers serving meals at dusk"
278
+ },
279
+ {
280
+ "imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 90'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23445a78'/%3E%3Cstop offset='1' stop-color='%23141d2c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='160' height='90' fill='url(%23g)'/%3E%3Ccircle cx='40' cy='30' r='12' fill='%23e8e2d4' opacity='.5'/%3E%3Ctext x='10' y='78' font-family='sans-serif' font-size='9' fill='%23ffffff' opacity='.75'%3EHero photo two%3C/text%3E%3C/svg%3E",
281
+ "alt": "The community hall on open day"
282
+ }
283
+ ]
284
+ }
311
285
  },
312
286
  {
313
- "name": "items",
314
- "kind": "items",
315
- "required": true,
316
- "description": "The impact cards.",
317
- "itemFields": [
318
- {
319
- "name": "imageUrl",
320
- "kind": "image",
321
- "required": false,
322
- "description": "Card image (uploaded through the platform pipeline). Preferred over icon."
323
- },
324
- {
325
- "name": "icon",
326
- "kind": "icon",
327
- "required": false,
328
- "description": "Legacy emoji/icon — superseded by imageUrl; render it only when imageUrl is absent."
329
- },
330
- {
331
- "name": "title",
332
- "kind": "text",
333
- "required": true,
334
- "description": "The card title."
287
+ "type": "impact",
288
+ "label": "Impact cards",
289
+ "pages": [
290
+ "home"
291
+ ],
292
+ "description": "What gifts make possible — optional intro plus icon/title/description cards. Claims must be truthful for the tenant.",
293
+ "fields": [
294
+ {
295
+ "name": "eyebrow",
296
+ "kind": "text",
297
+ "required": false,
298
+ "description": "Short kicker."
299
+ },
300
+ {
301
+ "name": "heading",
302
+ "kind": "text",
303
+ "required": false,
304
+ "description": "The section heading."
305
+ },
306
+ {
307
+ "name": "text",
308
+ "kind": "text",
309
+ "required": false,
310
+ "description": "An intro line."
311
+ },
312
+ {
313
+ "name": "items",
314
+ "kind": "items",
315
+ "required": true,
316
+ "description": "The impact cards.",
317
+ "itemFields": [
318
+ {
319
+ "name": "imageUrl",
320
+ "kind": "image",
321
+ "required": false,
322
+ "description": "Card image (uploaded through the platform pipeline). Preferred over icon."
323
+ },
324
+ {
325
+ "name": "icon",
326
+ "kind": "icon",
327
+ "required": false,
328
+ "description": "Legacy emoji/icon — superseded by imageUrl; render it only when imageUrl is absent."
329
+ },
330
+ {
331
+ "name": "title",
332
+ "kind": "text",
333
+ "required": true,
334
+ "description": "The card title."
335
+ },
336
+ {
337
+ "name": "description",
338
+ "kind": "text",
339
+ "required": true,
340
+ "description": "One sentence describing it."
341
+ }
342
+ ]
343
+ }
344
+ ],
345
+ "minimal": {
346
+ "items": [
347
+ {
348
+ "title": "Warm meals",
349
+ "description": "Hot meals served at a community kitchen."
350
+ }
351
+ ]
335
352
  },
336
- {
337
- "name": "description",
338
- "kind": "text",
339
- "required": true,
340
- "description": "One sentence describing it."
353
+ "sample": {
354
+ "eyebrow": "Where your gift goes",
355
+ "heading": "Real help, made of small acts",
356
+ "items": [
357
+ {
358
+ "imageUrl": "https://assets.example/impact-water.jpg",
359
+ "title": "£10 · Clean water",
360
+ "description": "Safe drinking water for a family for a whole month."
361
+ },
362
+ {
363
+ "icon": "🍲",
364
+ "title": "£25 · Warm meals",
365
+ "description": "Fifteen hot meals served at a local community kitchen."
366
+ },
367
+ {
368
+ "title": "£50 · School kit",
369
+ "description": "Books and supplies for two children for a full term."
370
+ }
371
+ ]
341
372
  }
342
- ]
343
- }
344
- ],
345
- "minimal": {
346
- "items": [
347
- {
348
- "title": "Warm meals",
349
- "description": "Hot meals served at a community kitchen."
350
- }
351
- ]
352
- },
353
- "sample": {
354
- "eyebrow": "Where your gift goes",
355
- "heading": "Real help, made of small acts",
356
- "items": [
357
- {
358
- "imageUrl": "https://assets.example/impact-water.jpg",
359
- "title": "£10 · Clean water",
360
- "description": "Safe drinking water for a family for a whole month."
361
- },
362
- {
363
- "icon": "🍲",
364
- "title": "£25 · Warm meals",
365
- "description": "Fifteen hot meals served at a local community kitchen."
366
- },
367
- {
368
- "title": "£50 · School kit",
369
- "description": "Books and supplies for two children for a full term."
370
- }
371
- ]
372
- }
373
- },
374
- {
375
- "type": "whatsOn",
376
- "label": "What's-on strip",
377
- "pages": [
378
- "home"
379
- ],
380
- "kind": "widget",
381
- "island": "whats_on_strip",
382
- "description": "Widget section — positions the what's-on strip of info events. No authorable content.",
383
- "fields": [],
384
- "minimal": {},
385
- "sample": {}
386
- },
387
- {
388
- "type": "events",
389
- "label": "Events carousel",
390
- "pages": [
391
- "home"
392
- ],
393
- "kind": "widget",
394
- "island": "events_carousel",
395
- "description": "Widget section — positions the ticketed-events carousel. No authorable content.",
396
- "fields": [],
397
- "minimal": {},
398
- "sample": {}
399
- },
400
- {
401
- "type": "articles",
402
- "label": "Latest articles",
403
- "pages": [
404
- "home"
405
- ],
406
- "kind": "widget",
407
- "island": "latest_articles",
408
- "description": "Widget section — positions the latest-articles strip. No authorable content.",
409
- "fields": [],
410
- "minimal": {},
411
- "sample": {}
412
- },
413
- {
414
- "type": "emergency",
415
- "label": "Emergency appeal strip",
416
- "pages": [
417
- "home"
418
- ],
419
- "description": "A high-urgency full-width strip — one headline, an optional line, one action. For live emergencies and time-critical appeals.",
420
- "fields": [
421
- {
422
- "name": "heading",
423
- "kind": "text",
424
- "required": true,
425
- "description": "The urgent headline."
426
- },
427
- {
428
- "name": "text",
429
- "kind": "text",
430
- "required": false,
431
- "description": "One supporting line."
432
373
  },
433
374
  {
434
- "name": "buttonLabel",
435
- "kind": "text",
436
- "required": false,
437
- "description": "Action label (default: Donate now)."
375
+ "type": "whatsOn",
376
+ "label": "What's on",
377
+ "pages": [
378
+ "home"
379
+ ],
380
+ "kind": "widget",
381
+ "island": "whats_on_strip",
382
+ "description": "Upcoming info events (special days — no sign-up). Render the `infoEvents` context your way, or place {% island 'whats_on_strip' %} for the ready-made strip.",
383
+ "fields": [],
384
+ "minimal": {},
385
+ "sample": {},
386
+ "context": [
387
+ "infoEvents"
388
+ ]
438
389
  },
439
390
  {
440
- "name": "buttonHref",
441
- "kind": "url",
442
- "required": false,
443
- "description": "Action link (default: /donate). Scheme-guarded on save."
444
- }
445
- ],
446
- "minimal": {
447
- "heading": "Emergency appeal"
448
- },
449
- "sample": {
450
- "heading": "Gaza emergency appeal",
451
- "text": "Families need food, water and shelter right now.",
452
- "buttonLabel": "Give emergency aid",
453
- "buttonHref": "/donate"
454
- }
455
- },
456
- {
457
- "type": "stats",
458
- "label": "Stats band",
459
- "pages": [
460
- "home"
461
- ],
462
- "description": "Big headline numbers — the at-a-glance impact band. Values are author-entered text (write them exactly as they should read).",
463
- "fields": [
464
- {
465
- "name": "eyebrow",
466
- "kind": "text",
467
- "required": false,
468
- "description": "Short kicker."
391
+ "type": "events",
392
+ "label": "Events",
393
+ "pages": [
394
+ "home"
395
+ ],
396
+ "kind": "widget",
397
+ "island": "events_carousel",
398
+ "description": "Upcoming ticketed events on the home page. Your template renders the `events` context (the curated listing model) however its voice demands — or places {% island 'events_carousel' %} for the ready-made rotating showcase. Listing data only: ticket sales happen on the platform-rendered detail view (detailHref).",
399
+ "fields": [],
400
+ "minimal": {},
401
+ "sample": {},
402
+ "context": [
403
+ "events"
404
+ ]
469
405
  },
470
406
  {
471
- "name": "heading",
472
- "kind": "text",
473
- "required": false,
474
- "description": "The band heading."
407
+ "type": "articles",
408
+ "label": "Latest articles",
409
+ "pages": [
410
+ "home"
411
+ ],
412
+ "kind": "widget",
413
+ "island": "latest_articles",
414
+ "description": "The newest published articles. Render the `latestArticles` context your way, or place {% island 'latest_articles' %} for the ready-made strip.",
415
+ "fields": [],
416
+ "minimal": {},
417
+ "sample": {},
418
+ "context": [
419
+ "latestArticles"
420
+ ]
475
421
  },
476
422
  {
477
- "name": "items",
478
- "kind": "items",
479
- "required": true,
480
- "description": "The stat entries.",
481
- "itemFields": [
482
- {
483
- "name": "value",
484
- "kind": "text",
485
- "required": true,
486
- "description": "The number, as it should read (e.g. 12,000 or 98%)."
423
+ "type": "emergency",
424
+ "label": "Emergency appeal strip",
425
+ "pages": [
426
+ "home"
427
+ ],
428
+ "description": "A high-urgency full-width strip — one headline, an optional line, one action. For live emergencies and time-critical appeals.",
429
+ "fields": [
430
+ {
431
+ "name": "heading",
432
+ "kind": "text",
433
+ "required": true,
434
+ "description": "The urgent headline."
435
+ },
436
+ {
437
+ "name": "text",
438
+ "kind": "text",
439
+ "required": false,
440
+ "description": "One supporting line."
441
+ },
442
+ {
443
+ "name": "buttonLabel",
444
+ "kind": "text",
445
+ "required": false,
446
+ "description": "Action label (default: Donate now)."
447
+ },
448
+ {
449
+ "name": "buttonHref",
450
+ "kind": "url",
451
+ "required": false,
452
+ "description": "Action link (default: /donate). Scheme-guarded on save."
453
+ }
454
+ ],
455
+ "minimal": {
456
+ "heading": "Emergency appeal"
487
457
  },
488
- {
489
- "name": "label",
490
- "kind": "text",
491
- "required": true,
492
- "description": "What it counts (e.g. meals served)."
458
+ "sample": {
459
+ "heading": "Gaza emergency appeal",
460
+ "text": "Families need food, water and shelter right now.",
461
+ "buttonLabel": "Give emergency aid",
462
+ "buttonHref": "/donate"
493
463
  }
494
- ]
495
- }
496
- ],
497
- "minimal": {
498
- "items": [
499
- {
500
- "value": "1,200",
501
- "label": "meals served"
502
- }
503
- ]
504
- },
505
- "sample": {
506
- "eyebrow": "Last year",
507
- "heading": "What your giving did",
508
- "items": [
509
- {
510
- "value": "12,400",
511
- "label": "hot meals served"
512
- },
513
- {
514
- "value": "3,100",
515
- "label": "families supported"
516
- },
517
- {
518
- "value": "98p",
519
- "label": "of every pound to the cause"
520
- }
521
- ]
522
- }
523
- },
524
- {
525
- "type": "appealGrid",
526
- "label": "Appeal cards",
527
- "pages": [
528
- "home"
529
- ],
530
- "description": "A grid of appeal/campaign cards — image, title, a line, and a per-card action.",
531
- "fields": [
532
- {
533
- "name": "eyebrow",
534
- "kind": "text",
535
- "required": false,
536
- "description": "Short kicker."
537
464
  },
538
465
  {
539
- "name": "heading",
540
- "kind": "text",
541
- "required": false,
542
- "description": "The section heading."
543
- },
544
- {
545
- "name": "items",
546
- "kind": "items",
547
- "required": true,
548
- "description": "The appeal cards.",
549
- "itemFields": [
550
- {
551
- "name": "imageUrl",
552
- "kind": "image",
553
- "required": false,
554
- "description": "Card photograph (uploaded through the platform pipeline)."
555
- },
556
- {
557
- "name": "title",
558
- "kind": "text",
559
- "required": true,
560
- "description": "The appeal's name."
561
- },
562
- {
563
- "name": "text",
564
- "kind": "text",
565
- "required": true,
566
- "description": "One sentence on what it funds."
567
- },
568
- {
569
- "name": "buttonLabel",
570
- "kind": "text",
571
- "required": false,
572
- "description": "Card action label (default: Donate)."
466
+ "type": "stats",
467
+ "label": "Stats band",
468
+ "pages": [
469
+ "home"
470
+ ],
471
+ "description": "Big headline numbers — the at-a-glance impact band. Values are author-entered text (write them exactly as they should read).",
472
+ "fields": [
473
+ {
474
+ "name": "eyebrow",
475
+ "kind": "text",
476
+ "required": false,
477
+ "description": "Short kicker."
478
+ },
479
+ {
480
+ "name": "heading",
481
+ "kind": "text",
482
+ "required": false,
483
+ "description": "The band heading."
484
+ },
485
+ {
486
+ "name": "items",
487
+ "kind": "items",
488
+ "required": true,
489
+ "description": "The stat entries.",
490
+ "itemFields": [
491
+ {
492
+ "name": "value",
493
+ "kind": "text",
494
+ "required": true,
495
+ "description": "The number, as it should read (e.g. 12,000 or 98%)."
496
+ },
497
+ {
498
+ "name": "label",
499
+ "kind": "text",
500
+ "required": true,
501
+ "description": "What it counts (e.g. meals served)."
502
+ }
503
+ ]
504
+ }
505
+ ],
506
+ "minimal": {
507
+ "items": [
508
+ {
509
+ "value": "1,200",
510
+ "label": "meals served"
511
+ }
512
+ ]
573
513
  },
574
- {
575
- "name": "buttonHref",
576
- "kind": "url",
577
- "required": false,
578
- "description": "Card action link (default: /donate). Scheme-guarded on save. The cause picker writes /donate/{slug} and records causeId beside it (editor bookkeeping; renderers use buttonHref only)."
514
+ "sample": {
515
+ "eyebrow": "Last year",
516
+ "heading": "What your giving did",
517
+ "items": [
518
+ {
519
+ "value": "12,400",
520
+ "label": "hot meals served"
521
+ },
522
+ {
523
+ "value": "3,100",
524
+ "label": "families supported"
525
+ },
526
+ {
527
+ "value": "98p",
528
+ "label": "of every pound to the cause"
529
+ }
530
+ ]
579
531
  }
580
- ]
581
- }
582
- ],
583
- "minimal": {
584
- "items": [
585
- {
586
- "title": "Winter appeal",
587
- "text": "Warm meals through the coldest months."
588
- }
589
- ]
590
- },
591
- "sample": {
592
- "eyebrow": "Our appeals",
593
- "heading": "Where help is needed most",
594
- "items": [
595
- {
596
- "title": "Winter appeal",
597
- "text": "Warm meals and heating grants through the coldest months.",
598
- "buttonLabel": "Support winter",
599
- "buttonHref": "/donate"
600
- },
601
- {
602
- "title": "Clean water",
603
- "text": "Wells and filtration for three villages.",
604
- "buttonLabel": "Give water",
605
- "buttonHref": "/donate"
606
- }
607
- ]
608
- }
609
- },
610
- {
611
- "type": "people",
612
- "label": "People / team",
613
- "pages": [
614
- "about"
615
- ],
616
- "description": "The key people behind the charity — trustees, staff, volunteers — as simple cards: name, position, an optional photograph (uploaded through the platform pipeline) and an optional one-line statement. Templates render a card grid that collapses to a single column on small screens; a card without a photo renders cleanly (initials or plain), never a placeholder face.",
617
- "fields": [
618
- {
619
- "name": "eyebrow",
620
- "kind": "text",
621
- "required": false,
622
- "description": "Short kicker, e.g. Who we are."
623
- },
624
- {
625
- "name": "heading",
626
- "kind": "text",
627
- "required": false,
628
- "description": "The section heading, e.g. Our team."
629
532
  },
630
533
  {
631
- "name": "text",
632
- "kind": "text",
633
- "required": false,
634
- "description": "An optional intro line."
534
+ "type": "appealGrid",
535
+ "label": "Appeal cards",
536
+ "pages": [
537
+ "home"
538
+ ],
539
+ "description": "A grid of appeal/campaign cards — image, title, a line, and a per-card action.",
540
+ "fields": [
541
+ {
542
+ "name": "eyebrow",
543
+ "kind": "text",
544
+ "required": false,
545
+ "description": "Short kicker."
546
+ },
547
+ {
548
+ "name": "heading",
549
+ "kind": "text",
550
+ "required": false,
551
+ "description": "The section heading."
552
+ },
553
+ {
554
+ "name": "items",
555
+ "kind": "items",
556
+ "required": true,
557
+ "description": "The appeal cards.",
558
+ "itemFields": [
559
+ {
560
+ "name": "imageUrl",
561
+ "kind": "image",
562
+ "required": false,
563
+ "description": "Card photograph (uploaded through the platform pipeline)."
564
+ },
565
+ {
566
+ "name": "title",
567
+ "kind": "text",
568
+ "required": true,
569
+ "description": "The appeal's name."
570
+ },
571
+ {
572
+ "name": "text",
573
+ "kind": "text",
574
+ "required": true,
575
+ "description": "One sentence on what it funds."
576
+ },
577
+ {
578
+ "name": "buttonLabel",
579
+ "kind": "text",
580
+ "required": false,
581
+ "description": "Card action label (default: Donate)."
582
+ },
583
+ {
584
+ "name": "buttonHref",
585
+ "kind": "url",
586
+ "required": false,
587
+ "description": "Card action link (default: /donate). Scheme-guarded on save. The cause picker writes /donate/{slug} and records causeId beside it (editor bookkeeping; renderers use buttonHref only)."
588
+ }
589
+ ]
590
+ }
591
+ ],
592
+ "minimal": {
593
+ "items": [
594
+ {
595
+ "title": "Winter appeal",
596
+ "text": "Warm meals through the coldest months."
597
+ }
598
+ ]
599
+ },
600
+ "sample": {
601
+ "eyebrow": "Our appeals",
602
+ "heading": "Where help is needed most",
603
+ "items": [
604
+ {
605
+ "title": "Winter appeal",
606
+ "text": "Warm meals and heating grants through the coldest months.",
607
+ "buttonLabel": "Support winter",
608
+ "buttonHref": "/donate"
609
+ },
610
+ {
611
+ "title": "Clean water",
612
+ "text": "Wells and filtration for three villages.",
613
+ "buttonLabel": "Give water",
614
+ "buttonHref": "/donate"
615
+ }
616
+ ]
617
+ }
635
618
  },
636
619
  {
637
- "name": "items",
638
- "kind": "items",
639
- "required": true,
640
- "description": "The people.",
641
- "itemFields": [
642
- {
643
- "name": "name",
644
- "kind": "text",
645
- "required": true,
646
- "description": "The person's name."
647
- },
648
- {
649
- "name": "role",
650
- "kind": "text",
651
- "required": true,
652
- "description": "Their position, e.g. Chair of Trustees."
653
- },
654
- {
655
- "name": "imageUrl",
656
- "kind": "image",
657
- "required": false,
658
- "description": "A photograph (platform pipeline). Absent renders cleanly — never a placeholder face."
620
+ "type": "people",
621
+ "label": "People / team",
622
+ "pages": [
623
+ "about"
624
+ ],
625
+ "description": "The key people behind the charity — trustees, staff, volunteers — as simple cards: name, position, an optional photograph (uploaded through the platform pipeline) and an optional one-line statement. Templates render a card grid that collapses to a single column on small screens; a card without a photo renders cleanly (initials or plain), never a placeholder face.",
626
+ "fields": [
627
+ {
628
+ "name": "eyebrow",
629
+ "kind": "text",
630
+ "required": false,
631
+ "description": "Short kicker, e.g. Who we are."
632
+ },
633
+ {
634
+ "name": "heading",
635
+ "kind": "text",
636
+ "required": false,
637
+ "description": "The section heading, e.g. Our team."
638
+ },
639
+ {
640
+ "name": "text",
641
+ "kind": "text",
642
+ "required": false,
643
+ "description": "An optional intro line."
644
+ },
645
+ {
646
+ "name": "items",
647
+ "kind": "items",
648
+ "required": true,
649
+ "description": "The people.",
650
+ "itemFields": [
651
+ {
652
+ "name": "name",
653
+ "kind": "text",
654
+ "required": true,
655
+ "description": "The person's name."
656
+ },
657
+ {
658
+ "name": "role",
659
+ "kind": "text",
660
+ "required": true,
661
+ "description": "Their position, e.g. Chair of Trustees."
662
+ },
663
+ {
664
+ "name": "imageUrl",
665
+ "kind": "image",
666
+ "required": false,
667
+ "description": "A photograph (platform pipeline). Absent renders cleanly — never a placeholder face."
668
+ },
669
+ {
670
+ "name": "statement",
671
+ "kind": "text",
672
+ "required": false,
673
+ "description": "One line in their voice, e.g. why they serve."
674
+ }
675
+ ]
676
+ }
677
+ ],
678
+ "minimal": {
679
+ "items": [
680
+ {
681
+ "name": "Amina Khan",
682
+ "role": "Chair of Trustees"
683
+ }
684
+ ]
659
685
  },
660
- {
661
- "name": "statement",
662
- "kind": "text",
663
- "required": false,
664
- "description": "One line in their voice, e.g. why they serve."
686
+ "sample": {
687
+ "eyebrow": "Who we are",
688
+ "heading": "The people behind the work",
689
+ "text": "A small team and a lot of volunteers, accountable to you.",
690
+ "items": [
691
+ {
692
+ "name": "Amina Khan",
693
+ "role": "Chair of Trustees",
694
+ "imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23355e4b'/%3E%3Ccircle cx='48' cy='38' r='16' fill='%23e8e2d4'/%3E%3Cpath d='M16 88c4-20 18-28 32-28s28 8 32 28z' fill='%23e8e2d4'/%3E%3C/svg%3E",
695
+ "statement": "Every pound is a trust — we treat it that way."
696
+ },
697
+ {
698
+ "name": "Daniel Osei",
699
+ "role": "Treasurer"
700
+ },
701
+ {
702
+ "name": "Sara Malik",
703
+ "role": "Volunteer Coordinator",
704
+ "statement": "Fifty pairs of hands, one purpose."
705
+ }
706
+ ]
665
707
  }
666
- ]
667
708
  }
668
- ],
669
- "minimal": {
670
- "items": [
671
- {
672
- "name": "Amina Khan",
673
- "role": "Chair of Trustees"
674
- }
675
- ]
676
- },
677
- "sample": {
678
- "eyebrow": "Who we are",
679
- "heading": "The people behind the work",
680
- "text": "A small team and a lot of volunteers, accountable to you.",
681
- "items": [
682
- {
683
- "name": "Amina Khan",
684
- "role": "Chair of Trustees",
685
- "imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23355e4b'/%3E%3Ccircle cx='48' cy='38' r='16' fill='%23e8e2d4'/%3E%3Cpath d='M16 88c4-20 18-28 32-28s28 8 32 28z' fill='%23e8e2d4'/%3E%3C/svg%3E",
686
- "statement": "Every pound is a trust — we treat it that way."
687
- },
688
- {
689
- "name": "Daniel Osei",
690
- "role": "Treasurer"
691
- },
692
- {
693
- "name": "Sara Malik",
694
- "role": "Volunteer Coordinator",
695
- "statement": "Fifty pairs of hands, one purpose."
696
- }
697
- ]
698
- }
699
- }
700
- ]
701
- }
709
+ ]
710
+ }