@port60/template-kit 0.16.0 → 0.18.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.
@@ -8,6 +8,7 @@
8
8
  "description": "The full donation flow for the tenant's quick-give cause: suggested amounts, custom amount, donor details, Gift Aid opt-in, payment. All payment/compliance behaviour is platform-owned.",
9
9
  "params": [],
10
10
  "stylingApi": [
11
+ ".action-card",
11
12
  ".donate-card",
12
13
  ".donate-head",
13
14
  ".donate-sub",
@@ -243,6 +244,55 @@
243
244
  ".impact-map-card-cta",
244
245
  ".impact-map-empty"
245
246
  ]
247
+ },
248
+ {
249
+ "name": "volunteer_signup",
250
+ "label": "Volunteer sign-up",
251
+ "status": "preview",
252
+ "description": "The volunteer sign-up flow: what to help with (general, then the events and campaigns that need people), when (only when the opportunity runs in shifts), a few details, then confirmation by a charity-branded email that also signs the volunteer in. Shares the donation widget's card vocabulary so a themed donate card themes this too; the vol-* classes cover what is specific to asking for help.",
253
+ "params": [],
254
+ "stylingApi": [
255
+ ".donate-card",
256
+ ".action-card",
257
+ ".vol-body",
258
+ ".volunteer-card",
259
+ ".vol-card",
260
+ ".vol-chosen",
261
+ ".vol-search",
262
+ ".vol-list",
263
+ ".vol-group",
264
+ ".vol-group-label",
265
+ ".vol-option",
266
+ ".vol-option-main",
267
+ ".vol-option-summary",
268
+ ".vol-option-meta",
269
+ ".vol-needed",
270
+ ".vol-shift-count",
271
+ ".vol-form",
272
+ ".vol-availability",
273
+ ".vol-chips",
274
+ ".vol-chip",
275
+ ".vol-confirm",
276
+ ".vol-optional",
277
+ ".vol-why",
278
+ ".vol-extra",
279
+ ".vol-summary",
280
+ ".vol-signed-in",
281
+ ".vol-summary-needed",
282
+ ".vol-field-error"
283
+ ]
284
+ },
285
+ {
286
+ "name": "primary_action_widget",
287
+ "label": "Primary action widget",
288
+ "status": "available",
289
+ "description": "The widget that leads the site. It IS the donation widget when the charity chose giving to lead, the volunteer sign-up when volunteering leads, and nothing when the charity chose no widget in the hero; site.focus tells you which ('donate', 'volunteer' or 'none'). Place {% island 'primary_action_widget' %} in the home hero's widget slot INSTEAD of donation_widget whenever your template declares supports.focus: the platform decides which widget renders, the template never does, and the charity's switch moves it without a template change. Both widgets wear .action-card for shared treatment; .donate-card and .volunteer-card carry kind-specific styling. A template that wants a different layout per kind may branch on section.primary.kind and place donation_widget or volunteer_signup itself, but then it owns the switch.",
290
+ "params": [],
291
+ "stylingApi": [
292
+ ".action-card",
293
+ ".donate-card",
294
+ ".volunteer-card"
295
+ ]
246
296
  }
247
297
  ]
248
298
  }
@@ -4,7 +4,12 @@
4
4
  "title": "Port60 template manifest (contract v1)",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["name", "version", "format", "supports"],
7
+ "required": [
8
+ "name",
9
+ "version",
10
+ "format",
11
+ "supports"
12
+ ],
8
13
  "properties": {
9
14
  "name": {
10
15
  "type": "string",
@@ -18,11 +23,20 @@
18
23
  },
19
24
  "format": {
20
25
  "type": "string",
21
- "enum": ["port60-liquid@1"],
26
+ "enum": [
27
+ "port60-liquid@1"
28
+ ],
22
29
  "description": "The dialect/contract major this template is written against."
23
30
  },
24
- "label": { "type": "string", "maxLength": 60, "description": "Human-readable name shown in pickers." },
25
- "description": { "type": "string", "maxLength": 300 },
31
+ "label": {
32
+ "type": "string",
33
+ "maxLength": 60,
34
+ "description": "Human-readable name shown in pickers."
35
+ },
36
+ "description": {
37
+ "type": "string",
38
+ "maxLength": 300
39
+ },
26
40
  "requiresCapabilities": {
27
41
  "type": "array",
28
42
  "items": {
@@ -52,7 +66,15 @@
52
66
  "type": "array",
53
67
  "items": {
54
68
  "type": "string",
55
- "enum": ["general", "mosque", "church", "appeal-charity", "pta", "community", "training"]
69
+ "enum": [
70
+ "general",
71
+ "mosque",
72
+ "church",
73
+ "appeal-charity",
74
+ "pta",
75
+ "community",
76
+ "training"
77
+ ]
56
78
  },
57
79
  "uniqueItems": true,
58
80
  "default": [],
@@ -66,25 +88,38 @@
66
88
  "supports": {
67
89
  "type": "object",
68
90
  "additionalProperties": false,
69
- "required": ["pages", "sections"],
91
+ "required": [
92
+ "pages",
93
+ "sections"
94
+ ],
70
95
  "properties": {
71
96
  "pages": {
72
97
  "type": "array",
73
- "items": { "type": "string", "enum": ["home", "about"] },
98
+ "items": {
99
+ "type": "string",
100
+ "enum": [
101
+ "home",
102
+ "about"
103
+ ]
104
+ },
74
105
  "minItems": 1,
75
106
  "uniqueItems": true,
76
107
  "description": "Section based page bodies this template renders from sections/{type}.liquid. Contract v1 supports home and about. This is independent of layout chrome and route-specific pageTemplates."
77
108
  },
78
109
  "sections": {
79
110
  "type": "array",
80
- "items": { "type": "string" },
111
+ "items": {
112
+ "type": "string"
113
+ },
81
114
  "minItems": 1,
82
115
  "uniqueItems": true,
83
116
  "description": "Section types this template ships renderers for (sections/{type}.liquid). Unsupported types are omitted at render, never an error."
84
117
  },
85
118
  "islands": {
86
119
  "type": "array",
87
- "items": { "type": "string" },
120
+ "items": {
121
+ "type": "string"
122
+ },
88
123
  "uniqueItems": true,
89
124
  "default": [],
90
125
  "description": "Islands this template places. Every {% island %} name must be listed here AND exist in the platform island registry."
@@ -96,7 +131,15 @@
96
131
  },
97
132
  "pageTemplates": {
98
133
  "type": "array",
99
- "items": { "type": "string", "enum": ["events", "course", "articles", "article"] },
134
+ "items": {
135
+ "type": "string",
136
+ "enum": [
137
+ "events",
138
+ "course",
139
+ "articles",
140
+ "article"
141
+ ]
142
+ },
100
143
  "uniqueItems": true,
101
144
  "default": [],
102
145
  "description": "Route-specific data views rendered from pages/{page}.liquid with documented context. Events and articles own listings; course and article own detail presentation. Event transactions, course enrolment, article engagement, comments, identity and consent stay platform-owned islands. Requires supports.layout; without an entry the platform body renders inside the template chrome."
@@ -115,10 +158,34 @@
115
158
  "type": "array",
116
159
  "items": {
117
160
  "type": "string",
118
- "enum": ["reveal", "counter", "progress", "countdown", "accordion", "carousel", "stickyHeader", "stickyCta", "lightbox", "tabs", "nav"]
161
+ "enum": [
162
+ "reveal",
163
+ "counter",
164
+ "progress",
165
+ "countdown",
166
+ "accordion",
167
+ "carousel",
168
+ "stickyHeader",
169
+ "stickyCta",
170
+ "lightbox",
171
+ "tabs",
172
+ "nav"
173
+ ]
119
174
  },
120
175
  "uniqueItems": true,
121
176
  "description": "Engine-attached behaviours this template's markup opts into via data-p60-* attributes (see the behaviour catalogue). Templates never ship JavaScript; declaring here is what makes the engine wire the markup, and the validator proves declaration and usage agree in both directions."
177
+ },
178
+ "focus": {
179
+ "type": "array",
180
+ "uniqueItems": true,
181
+ "items": {
182
+ "type": "string",
183
+ "enum": [
184
+ "donate",
185
+ "volunteer"
186
+ ]
187
+ },
188
+ "description": "The site-focus kinds this template can LEAD with (docs/volunteering.md). Declaring 'volunteer' means the home hero places the primary_action_widget island (the widget that becomes the donation widget or the volunteer sign-up as the charity chooses) or branches on section.primary.kind, so the volunteer sign-up can take the widget slot; the picker marks templates that cannot lead with volunteering. Templates that predate the setting keep rendering the donation widget, since donate is the default."
122
189
  }
123
190
  }
124
191
  },
@@ -130,7 +197,10 @@
130
197
  "hero": {
131
198
  "type": "object",
132
199
  "additionalProperties": false,
133
- "required": ["idealAspect", "minWidth"],
200
+ "required": [
201
+ "idealAspect",
202
+ "minWidth"
203
+ ],
134
204
  "properties": {
135
205
  "idealAspect": {
136
206
  "type": "string",
@@ -166,7 +236,10 @@
166
236
  "items": {
167
237
  "type": "object",
168
238
  "additionalProperties": false,
169
- "required": ["name", "values"],
239
+ "required": [
240
+ "name",
241
+ "values"
242
+ ],
170
243
  "properties": {
171
244
  "name": {
172
245
  "type": "string",
@@ -176,14 +249,18 @@
176
249
  "values": {
177
250
  "type": "object",
178
251
  "minProperties": 1,
179
- "propertyNames": { "pattern": "^[a-zA-Z][a-zA-Z0-9]{0,39}$" },
180
- "additionalProperties": { "type": "string" },
181
- "description": "Knob key → value. Every key must be a declared settings knob; select values must be listed options; font values must be catalogue families."
252
+ "propertyNames": {
253
+ "pattern": "^[a-zA-Z][a-zA-Z0-9]{0,39}$"
254
+ },
255
+ "additionalProperties": {
256
+ "type": "string"
257
+ },
258
+ "description": "Knob key \u2192 value. Every key must be a declared settings knob; select values must be listed options; font values must be catalogue families."
182
259
  }
183
260
  }
184
261
  },
185
262
  "default": [],
186
- "description": "One-click LOOKS: author-named bundles of knob values (scheme + fonts + width…) applied together. Everything remains individually adjustable afterwards."
263
+ "description": "One-click LOOKS: author-named bundles of knob values (scheme + fonts + width\u2026) applied together. Everything remains individually adjustable afterwards."
187
264
  },
188
265
  "settings": {
189
266
  "type": "object",
@@ -194,21 +271,51 @@
194
271
  "items": {
195
272
  "type": "object",
196
273
  "additionalProperties": false,
197
- "required": ["key", "kind", "label"],
274
+ "required": [
275
+ "key",
276
+ "kind",
277
+ "label"
278
+ ],
198
279
  "properties": {
199
- "key": { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9]{0,39}$" },
200
- "kind": { "type": "string", "enum": ["color", "select", "toggle", "font"] },
201
- "label": { "type": "string", "maxLength": 60 },
280
+ "key": {
281
+ "type": "string",
282
+ "pattern": "^[a-zA-Z][a-zA-Z0-9]{0,39}$"
283
+ },
284
+ "kind": {
285
+ "type": "string",
286
+ "enum": [
287
+ "color",
288
+ "select",
289
+ "toggle",
290
+ "font"
291
+ ]
292
+ },
293
+ "label": {
294
+ "type": "string",
295
+ "maxLength": 60
296
+ },
202
297
  "default": {},
203
- "options": { "type": "array", "items": { "type": "string" } },
298
+ "options": {
299
+ "type": "array",
300
+ "items": {
301
+ "type": "string"
302
+ }
303
+ },
204
304
  "group": {
205
305
  "type": "string",
206
- "enum": ["typography", "looks"],
207
- "description": "Where the admin surfaces this knob: 'typography' → the Typography section beside the font slots; 'looks' → the Looks section with the one-click bundles (colour schemes and accent dials belong there, and ungrouped color knobs plus a knob keyed 'scheme' default there anyway); other ungrouped knobs render under Template options."
306
+ "enum": [
307
+ "typography",
308
+ "looks"
309
+ ],
310
+ "description": "Where the admin surfaces this knob: 'typography' \u2192 the Typography section beside the font slots; 'looks' \u2192 the Looks section with the one-click bundles (colour schemes and accent dials belong there, and ungrouped color knobs plus a knob keyed 'scheme' default there anyway); other ungrouped knobs render under Template options."
208
311
  },
209
312
  "weights": {
210
313
  "type": "array",
211
- "items": { "type": "integer", "minimum": 100, "maximum": 1000 },
314
+ "items": {
315
+ "type": "integer",
316
+ "minimum": 100,
317
+ "maximum": 1000
318
+ },
212
319
  "uniqueItems": true,
213
320
  "description": "FONT knobs only: the weights this template's typographic system uses for the slot. The host requests these for the tenant's chosen family (clamped to the family's real weights). The tenant picks the FAMILY; weights, sizes and tracking stay template-owned."
214
321
  }
@@ -237,7 +237,7 @@
237
237
  "name": "images",
238
238
  "kind": "items",
239
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.",
240
+ "description": "Hero photographs (0\u20136, 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
241
  "itemFields": [
242
242
  {
243
243
  "name": "imageUrl",
@@ -360,16 +360,16 @@
360
360
  "items": [
361
361
  {
362
362
  "imageUrl": "p60fixture:impact/parcel",
363
- "title": "£10 · A family food parcel",
363
+ "title": "\u00a310 \u00b7 A family food parcel",
364
364
  "description": "Three days of meals, toiletries and a bag of fresh produce from the garden."
365
365
  },
366
366
  {
367
- "icon": "🔥",
368
- "title": "£25 · A warm week",
367
+ "icon": "\ud83d\udd25",
368
+ "title": "\u00a325 \u00b7 A warm week",
369
369
  "description": "Heating top-ups for a household choosing between eating and heating."
370
370
  },
371
371
  {
372
- "title": "£50 · A month of youth club",
372
+ "title": "\u00a350 \u00b7 A month of youth club",
373
373
  "description": "Coaching, kit and hot food for a young person, four evenings a week."
374
374
  }
375
375
  ]
@@ -513,7 +513,7 @@
513
513
  "pages": [
514
514
  "home"
515
515
  ],
516
- "description": "Big headline numbers, the at-a-glance impact band. Values are author-entered text (write them exactly as they should read).",
516
+ "description": "Big headline numbers, the at-a-glance impact band. Values are author-entered text (write them exactly as they should read), or, per item, a live figure the platform counts (`source`).",
517
517
  "fields": [
518
518
  {
519
519
  "name": "eyebrow",
@@ -544,6 +544,18 @@
544
544
  "kind": "text",
545
545
  "required": true,
546
546
  "description": "What it counts (e.g. meals served)."
547
+ },
548
+ {
549
+ "name": "source",
550
+ "kind": "select",
551
+ "required": false,
552
+ "options": [
553
+ "volunteers.active",
554
+ "volunteers.hoursThisYear",
555
+ "volunteers.opportunitiesOpen",
556
+ "volunteers.needed"
557
+ ],
558
+ "description": "A figure the platform counts rather than the author types (volunteers who helped in the last twelve months, hours logged this year, opportunities open now, volunteers still needed). When set, `value` is replaced at render with the live count, formatted for the site's locale; templates render `value` exactly as before."
547
559
  }
548
560
  ]
549
561
  }
@@ -0,0 +1,49 @@
1
+ // The site focus for the PREVIEW: a JavaScript twin of charity-site's lib/focus.ts, kept in step by
2
+ // hand (the validator is plain JS and is vendored into the kit). Same rules: the leading action is
3
+ // the widget in the hero; the other action is the one button in the header and beside the hero; the
4
+ // leading action never appears twice. The preview assumes volunteering is OPEN, so the pairing shows.
5
+ export const FOCUS_CHOICES = ['donate', 'volunteer', 'none'];
6
+
7
+ const ACTION = {
8
+ donate: { kind: 'donate', label: 'Donate', href: '/donate' },
9
+ volunteer: { kind: 'volunteer', label: 'Volunteer', href: '/volunteer' }
10
+ };
11
+
12
+ /** `?focus=` from the dev server, or nothing: 'donate' is the platform default. */
13
+ export function normaliseFocus(value) {
14
+ return FOCUS_CHOICES.includes(value) ? value : 'donate';
15
+ }
16
+
17
+ /** The resolved surfaces for one preview render, the shape of site.actions on a live site. */
18
+ export function previewActions(focus) {
19
+ if (focus === 'volunteer') {
20
+ return { primary: ACTION.volunteer, secondary: ACTION.donate, widget: 'volunteer', header: ACTION.donate, hero: ACTION.donate };
21
+ }
22
+ if (focus === 'none') {
23
+ // Buttons only (a Custom setting with no widget): the hero button leads with giving.
24
+ return { primary: ACTION.donate, secondary: ACTION.volunteer, widget: null, header: ACTION.donate, hero: ACTION.donate };
25
+ }
26
+ return { primary: ACTION.donate, secondary: ACTION.volunteer, widget: 'donate', header: ACTION.volunteer, hero: ACTION.volunteer };
27
+ }
28
+
29
+ /** The home hero's resolved fields, as the platform computes them (lib/focus.ts withResolvedActions). */
30
+ export function withResolvedActions(content, actions) {
31
+ const style = content.actionStyle ?? content.givingStyle;
32
+ const buttonMode = style === 'button' || actions.widget === null;
33
+ return {
34
+ ...content,
35
+ actionStyle: buttonMode ? 'button' : 'widget',
36
+ primary: actions.primary,
37
+ secondary: actions.secondary,
38
+ action: buttonMode && actions.hero === null && style === 'button' ? actions.primary : actions.hero
39
+ };
40
+ }
41
+
42
+ /** The site tree with the focus applied: site.focus, site.actions, and the nav's one button. */
43
+ export function applyFocus(site, actions) {
44
+ const swapCta = (items) => Array.isArray(items)
45
+ ? items.map((item) => (item && item.cta ? (actions.header ? { ...item, label: actions.header.label, href: actions.header.href } : null) : item)).filter(Boolean)
46
+ : items;
47
+ const nav = site.nav ? { ...site.nav, items: swapCta(site.nav.items), derived: swapCta(site.nav.derived) } : site.nav;
48
+ return { ...site, focus: actions.widget ?? 'none', actions, nav };
49
+ }