@port60/template-kit 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@port60/template-kit",
3
- "version": "0.15.0",
4
- "description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready \u2014 every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
3
+ "version": "0.17.0",
4
+ "description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready: every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "p60-template-kit": "bin/cli.mjs"
@@ -12,7 +12,6 @@
12
12
  "starter"
13
13
  ],
14
14
  "scripts": {
15
- "sync-vendor": "node scripts/sync-vendor.mjs",
16
15
  "test": "node --test 'tests/*.test.mjs'"
17
16
  },
18
17
  "dependencies": {
@@ -23,5 +22,12 @@
23
22
  "repository": {
24
23
  "type": "git",
25
24
  "url": "git+https://github.com/Port60Labs/template-kit.git"
25
+ },
26
+ "homepage": "https://developers.port60.com",
27
+ "bugs": {
28
+ "url": "https://github.com/Port60Labs/template-kit/issues"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
26
32
  }
27
33
  }
@@ -17,9 +17,9 @@
17
17
  "name": "counter",
18
18
  "status": "available",
19
19
  "label": "Animated counter",
20
- "description": "Counts the element's number up from zero when first revealed. The static text IS the final formatted value (the no-JS render); data-p60-count carries the numeric target the engine animates to, grouped with the page locale. Reduced motion: the final number shows immediately.",
20
+ "description": "Counts the element's figure up from zero when first revealed. The static text IS the final value, typed exactly as it should read (\"£20\", \"95%\", \"1,200\", \"2.5k\"): the engine animates only the first run of digits and keeps the prefix, suffix, grouping and decimal places as typed, restoring the exact text on the last frame. A numeric attribute value (legacy) overrides the target; the text still sets the format. Reduced motion: the final figure shows immediately.",
21
21
  "attributes": [
22
- { "attr": "data-p60-count", "value": "the numeric target, digits only" },
22
+ { "attr": "data-p60-count", "value": "none: the element's own text is the target and the format; or a numeric target (legacy)" },
23
23
  { "attr": "data-p60-count-duration", "value": "optional ms, capped at 4000 (default 1200)" }
24
24
  ],
25
25
  "stateClasses": ["is-revealed"],
@@ -129,7 +129,7 @@
129
129
  "name": "nav",
130
130
  "status": "available",
131
131
  "label": "Navigation",
132
- "description": "Your menu, made robust. You design the menu (markup, every pixel, the breakpoint); the engine supplies the interaction CSS :hover cannot: data-p60-nav on each nav root (several allowed), data-p60-nav-item on each group holding a data-p60-nav-toggle control and a data-p60-nav-menu panel (nesting allowed). Hover-capable pointers get hover intent (a short delay to open, a grace period to close) so crossing the gap between a toggle and its panel never closes it; keyboard, touch and pen toggle on the control (Enter/click, ArrowDown opens and focuses the first link; a mouse click on an already-hovered parent link navigates); Escape closes and returns focus to the toggle; a click outside or focus moving away closes; one item stays open per level and aria-expanded is kept honest. The burger: data-p60-nav-burger on a button whose aria-controls (or the attribute value) names the stacked panel; the engine toggles is-open on both, closes on Escape (returning focus), on a real navigation inside the panel, on an outside click, and when the burger disappears at a wider viewport. State is is-open on the item, the panel and the burger; under .p60-js show a panel only on is-open and keep your :hover/:focus-within rules scoped to html:not(.p60-js) for the no-JS render.",
132
+ "description": "Your menu, made robust. You design the menu (markup, every pixel, the breakpoint); the engine supplies the interaction CSS :hover cannot: data-p60-nav on each nav root (several allowed), data-p60-nav-item on each group holding a data-p60-nav-toggle control and a data-p60-nav-menu panel (nesting allowed). Hover-capable pointers get hover intent (a short delay to open, a grace period to close) so crossing the gap between a toggle and its panel never closes it; keyboard, touch and pen toggle on the control (Enter/click, ArrowDown opens and focuses the first link; a mouse click on an already-hovered parent link navigates); Escape closes and returns focus to the toggle; a click outside or focus moving away closes; one item stays open per level and aria-expanded is kept honest. The burger: data-p60-nav-burger on a button whose aria-controls (or the attribute value) names the stacked panel; the engine toggles is-open on both, closes on Escape (returning focus), on a real navigation inside the panel, on an outside click, and when the burger disappears at a wider viewport. State is is-open on the item, the panel and the burger; under .p60-js show a panel only on is-open and keep your :hover/:focus-within rules scoped to html:not(.p60-js) for the no-JS render. Fit steps: when the root's row wraps or overflows, the engine steps is-fit-1, is-fit-2, is-fit-3 onto the root until it fits, re-measured on resize and once fonts settle, never on a stacked (column) nav; the platform's defaults shrink the member_menu pill at each step (the Port60 ID wordmark, then the text, leaving the person icon with its accessible label), and a template maps the same classes to anything else it wants to give up before the burger.",
133
133
  "attributes": [
134
134
  { "attr": "data-p60-nav", "value": "none; a nav root, several allowed" },
135
135
  { "attr": "data-p60-nav-item", "value": "none; a group holding one toggle and one menu, nestable" },
@@ -137,7 +137,7 @@
137
137
  { "attr": "data-p60-nav-menu", "value": "none; the item's panel" },
138
138
  { "attr": "data-p60-nav-burger", "value": "none, or the id of the panel when aria-controls is absent" }
139
139
  ],
140
- "stateClasses": ["is-open"],
140
+ "stateClasses": ["is-open", "is-fit-1", "is-fit-2", "is-fit-3"],
141
141
  "cssVars": []
142
142
  }
143
143
  ]
@@ -3,8 +3,15 @@
3
3
  "version": "1.0",
4
4
  "root": "site",
5
5
  "siblings": {
6
- "description": "site.brand, site.nav, site.socials and site.locale carry the SAME shapes documented in context.json for the flat era; the tree re-addresses them, it does not reshape them.",
7
- "keys": ["brand", "nav", "socials", "locale"]
6
+ "description": "site.brand, site.nav, site.socials and site.locale carry the SAME shapes documented in context.json for the flat era; the tree re-addresses them, it does not reshape them. site.focus ('donate' | 'volunteer') and site.actions ({ primary, secondary } as { kind, label, href }) are the site-focus switch (docs/volunteering.md): which action leads as the widget in the hero and which is the one button; both documented in context.json.",
7
+ "keys": [
8
+ "brand",
9
+ "nav",
10
+ "socials",
11
+ "locale",
12
+ "focus",
13
+ "actions"
14
+ ]
8
15
  },
9
16
  "collections": {
10
17
  "services": {
@@ -13,12 +20,34 @@
13
20
  "moreHref": "/services",
14
21
  "description": "The organisation's published content pages (the services CMS), newest curation order first.",
15
22
  "item": {
16
- "id": { "type": "string", "since": "1.0" },
17
- "title": { "type": "string", "since": "1.0", "max": 200 },
18
- "slug": { "type": "string", "since": "1.0" },
19
- "summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
20
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
21
- "href": { "type": "string", "since": "1.0" }
23
+ "id": {
24
+ "type": "string",
25
+ "since": "1.0"
26
+ },
27
+ "title": {
28
+ "type": "string",
29
+ "since": "1.0",
30
+ "max": 200
31
+ },
32
+ "slug": {
33
+ "type": "string",
34
+ "since": "1.0"
35
+ },
36
+ "summary": {
37
+ "type": "string",
38
+ "since": "1.0",
39
+ "nullable": true,
40
+ "max": 500
41
+ },
42
+ "imageUrl": {
43
+ "type": "string",
44
+ "since": "1.0",
45
+ "nullable": true
46
+ },
47
+ "href": {
48
+ "type": "string",
49
+ "since": "1.0"
50
+ }
22
51
  }
23
52
  },
24
53
  "events": {
@@ -27,20 +56,76 @@
27
56
  "moreHref": "/events",
28
57
  "description": "Published events, soonest first. `registrationMode` is an OPEN enum: INFO, RSVP and TICKETED exist today and new modes will arrive; branch on the ones you style and fall back for the rest.",
29
58
  "item": {
30
- "id": { "type": "string", "since": "1.0" },
31
- "name": { "type": "string", "since": "1.0", "max": 200 },
32
- "description": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
33
- "registrationMode": { "type": "string", "since": "1.0", "enumOpen": ["INFO", "RSVP", "TICKETED"] },
34
- "startsAt": { "type": "string", "since": "1.0" },
35
- "endsAt": { "type": "string", "since": "1.0", "nullable": true },
36
- "venueName": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
37
- "online": { "type": "boolean", "since": "1.0" },
38
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
39
- "onSale": { "type": "boolean", "since": "1.0" },
40
- "soldOut": { "type": "boolean", "since": "1.0" },
41
- "priceFrom": { "type": "number", "since": "1.0", "nullable": true },
42
- "currency": { "type": "string", "since": "1.0", "nullable": true },
43
- "detailHref": { "type": "string", "since": "1.0" }
59
+ "id": {
60
+ "type": "string",
61
+ "since": "1.0"
62
+ },
63
+ "name": {
64
+ "type": "string",
65
+ "since": "1.0",
66
+ "max": 200
67
+ },
68
+ "description": {
69
+ "type": "string",
70
+ "since": "1.0",
71
+ "nullable": true,
72
+ "max": 200
73
+ },
74
+ "registrationMode": {
75
+ "type": "string",
76
+ "since": "1.0",
77
+ "enumOpen": [
78
+ "INFO",
79
+ "RSVP",
80
+ "TICKETED"
81
+ ]
82
+ },
83
+ "startsAt": {
84
+ "type": "string",
85
+ "since": "1.0"
86
+ },
87
+ "endsAt": {
88
+ "type": "string",
89
+ "since": "1.0",
90
+ "nullable": true
91
+ },
92
+ "venueName": {
93
+ "type": "string",
94
+ "since": "1.0",
95
+ "nullable": true,
96
+ "max": 200
97
+ },
98
+ "online": {
99
+ "type": "boolean",
100
+ "since": "1.0"
101
+ },
102
+ "imageUrl": {
103
+ "type": "string",
104
+ "since": "1.0",
105
+ "nullable": true
106
+ },
107
+ "onSale": {
108
+ "type": "boolean",
109
+ "since": "1.0"
110
+ },
111
+ "soldOut": {
112
+ "type": "boolean",
113
+ "since": "1.0"
114
+ },
115
+ "priceFrom": {
116
+ "type": "number",
117
+ "since": "1.0",
118
+ "nullable": true
119
+ },
120
+ "currency": {
121
+ "type": "string",
122
+ "since": "1.0",
123
+ "nullable": true
124
+ },
125
+ "detailHref": {
126
+ "type": "string",
127
+ "since": "1.0"
128
+ }
44
129
  }
45
130
  },
46
131
  "articles": {
@@ -49,17 +134,59 @@
49
134
  "moreHref": "/articles/all",
50
135
  "description": "Published articles, newest first. Detail bodies stay editor-owned sanitised richtext on the article page BY DESIGN; the card model carries everything a listing needs.",
51
136
  "item": {
52
- "id": { "type": "string", "since": "1.0" },
53
- "title": { "type": "string", "since": "1.0", "max": 200 },
54
- "slug": { "type": "string", "since": "1.0" },
55
- "excerpt": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
56
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
57
- "authorName": { "type": "string", "since": "1.0", "nullable": true, "max": 120 },
58
- "authorRole": { "type": "string", "since": "1.0", "nullable": true, "max": 120 },
59
- "categories": { "type": "string[]", "since": "1.0" },
60
- "publishedAt": { "type": "string", "since": "1.0" },
61
- "readingMinutes": { "type": "number", "since": "1.0", "nullable": true },
62
- "href": { "type": "string", "since": "1.0" }
137
+ "id": {
138
+ "type": "string",
139
+ "since": "1.0"
140
+ },
141
+ "title": {
142
+ "type": "string",
143
+ "since": "1.0",
144
+ "max": 200
145
+ },
146
+ "slug": {
147
+ "type": "string",
148
+ "since": "1.0"
149
+ },
150
+ "excerpt": {
151
+ "type": "string",
152
+ "since": "1.0",
153
+ "nullable": true,
154
+ "max": 500
155
+ },
156
+ "imageUrl": {
157
+ "type": "string",
158
+ "since": "1.0",
159
+ "nullable": true
160
+ },
161
+ "authorName": {
162
+ "type": "string",
163
+ "since": "1.0",
164
+ "nullable": true,
165
+ "max": 120
166
+ },
167
+ "authorRole": {
168
+ "type": "string",
169
+ "since": "1.0",
170
+ "nullable": true,
171
+ "max": 120
172
+ },
173
+ "categories": {
174
+ "type": "string[]",
175
+ "since": "1.0"
176
+ },
177
+ "publishedAt": {
178
+ "type": "string",
179
+ "since": "1.0"
180
+ },
181
+ "readingMinutes": {
182
+ "type": "number",
183
+ "since": "1.0",
184
+ "nullable": true
185
+ },
186
+ "href": {
187
+ "type": "string",
188
+ "since": "1.0"
189
+ }
63
190
  }
64
191
  },
65
192
  "campaigns": {
@@ -68,11 +195,30 @@
68
195
  "moreHref": "/campaigns",
69
196
  "description": "Live campaigns, curation order.",
70
197
  "item": {
71
- "id": { "type": "string", "since": "1.0" },
72
- "title": { "type": "string", "since": "1.0", "max": 200 },
73
- "href": { "type": "string", "since": "1.0" },
74
- "summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
75
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true }
198
+ "id": {
199
+ "type": "string",
200
+ "since": "1.0"
201
+ },
202
+ "title": {
203
+ "type": "string",
204
+ "since": "1.0",
205
+ "max": 200
206
+ },
207
+ "href": {
208
+ "type": "string",
209
+ "since": "1.0"
210
+ },
211
+ "summary": {
212
+ "type": "string",
213
+ "since": "1.0",
214
+ "nullable": true,
215
+ "max": 500
216
+ },
217
+ "imageUrl": {
218
+ "type": "string",
219
+ "since": "1.0",
220
+ "nullable": true
221
+ }
76
222
  }
77
223
  },
78
224
  "causes": {
@@ -81,16 +227,54 @@
81
227
  "moreHref": "/donate",
82
228
  "description": "Causes currently accepting donations, quick-give first. `paymentOptions` mirrors the public cause contract.",
83
229
  "item": {
84
- "id": { "type": "string", "since": "1.0" },
85
- "title": { "type": "string", "since": "1.0", "max": 200 },
86
- "slug": { "type": "string", "since": "1.0" },
87
- "description": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
88
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
89
- "targetAmount": { "type": "number", "since": "1.0", "nullable": true },
90
- "raisedAmount": { "type": "number", "since": "1.0", "nullable": true },
91
- "progressPercentage": { "type": "number", "since": "1.0", "nullable": true },
92
- "href": { "type": "string", "since": "1.0" },
93
- "paymentOptions": { "type": "object", "since": "1.0", "nullable": true }
230
+ "id": {
231
+ "type": "string",
232
+ "since": "1.0"
233
+ },
234
+ "title": {
235
+ "type": "string",
236
+ "since": "1.0",
237
+ "max": 200
238
+ },
239
+ "slug": {
240
+ "type": "string",
241
+ "since": "1.0"
242
+ },
243
+ "description": {
244
+ "type": "string",
245
+ "since": "1.0",
246
+ "nullable": true,
247
+ "max": 500
248
+ },
249
+ "imageUrl": {
250
+ "type": "string",
251
+ "since": "1.0",
252
+ "nullable": true
253
+ },
254
+ "targetAmount": {
255
+ "type": "number",
256
+ "since": "1.0",
257
+ "nullable": true
258
+ },
259
+ "raisedAmount": {
260
+ "type": "number",
261
+ "since": "1.0",
262
+ "nullable": true
263
+ },
264
+ "progressPercentage": {
265
+ "type": "number",
266
+ "since": "1.0",
267
+ "nullable": true
268
+ },
269
+ "href": {
270
+ "type": "string",
271
+ "since": "1.0"
272
+ },
273
+ "paymentOptions": {
274
+ "type": "object",
275
+ "since": "1.0",
276
+ "nullable": true
277
+ }
94
278
  }
95
279
  },
96
280
  "courses": {
@@ -99,16 +283,59 @@
99
283
  "moreHref": "/courses",
100
284
  "description": "Enrollable courses. The long-form About body is sanitised richtext on the course page, never in cards.",
101
285
  "item": {
102
- "id": { "type": "string", "since": "1.0" },
103
- "title": { "type": "string", "since": "1.0", "max": 200 },
104
- "description": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
105
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
106
- "venue": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
107
- "audience": { "type": "string", "since": "1.0", "enumOpen": ["ADULT", "CHILD", "FAMILY"] },
108
- "priceFrom": { "type": "number", "since": "1.0", "nullable": true },
109
- "currency": { "type": "string", "since": "1.0", "nullable": true },
110
- "soldOut": { "type": "boolean", "since": "1.0" },
111
- "href": { "type": "string", "since": "1.0" }
286
+ "id": {
287
+ "type": "string",
288
+ "since": "1.0"
289
+ },
290
+ "title": {
291
+ "type": "string",
292
+ "since": "1.0",
293
+ "max": 200
294
+ },
295
+ "description": {
296
+ "type": "string",
297
+ "since": "1.0",
298
+ "nullable": true,
299
+ "max": 500
300
+ },
301
+ "imageUrl": {
302
+ "type": "string",
303
+ "since": "1.0",
304
+ "nullable": true
305
+ },
306
+ "venue": {
307
+ "type": "string",
308
+ "since": "1.0",
309
+ "nullable": true,
310
+ "max": 200
311
+ },
312
+ "audience": {
313
+ "type": "string",
314
+ "since": "1.0",
315
+ "enumOpen": [
316
+ "ADULT",
317
+ "CHILD",
318
+ "FAMILY"
319
+ ]
320
+ },
321
+ "priceFrom": {
322
+ "type": "number",
323
+ "since": "1.0",
324
+ "nullable": true
325
+ },
326
+ "currency": {
327
+ "type": "string",
328
+ "since": "1.0",
329
+ "nullable": true
330
+ },
331
+ "soldOut": {
332
+ "type": "boolean",
333
+ "since": "1.0"
334
+ },
335
+ "href": {
336
+ "type": "string",
337
+ "since": "1.0"
338
+ }
112
339
  }
113
340
  },
114
341
  "resources": {
@@ -117,13 +344,39 @@
117
344
  "moreHref": "/resources",
118
345
  "description": "Published documents, grouped by folder.",
119
346
  "item": {
120
- "id": { "type": "string", "since": "1.0" },
121
- "title": { "type": "string", "since": "1.0", "max": 200 },
122
- "folder": { "type": "string", "since": "1.0", "nullable": true },
123
- "url": { "type": "string", "since": "1.0" },
124
- "contentType": { "type": "string", "since": "1.0", "nullable": true },
125
- "bytes": { "type": "number", "since": "1.0", "nullable": true },
126
- "publishedAt": { "type": "string", "since": "1.0", "nullable": true }
347
+ "id": {
348
+ "type": "string",
349
+ "since": "1.0"
350
+ },
351
+ "title": {
352
+ "type": "string",
353
+ "since": "1.0",
354
+ "max": 200
355
+ },
356
+ "folder": {
357
+ "type": "string",
358
+ "since": "1.0",
359
+ "nullable": true
360
+ },
361
+ "url": {
362
+ "type": "string",
363
+ "since": "1.0"
364
+ },
365
+ "contentType": {
366
+ "type": "string",
367
+ "since": "1.0",
368
+ "nullable": true
369
+ },
370
+ "bytes": {
371
+ "type": "number",
372
+ "since": "1.0",
373
+ "nullable": true
374
+ },
375
+ "publishedAt": {
376
+ "type": "string",
377
+ "since": "1.0",
378
+ "nullable": true
379
+ }
127
380
  }
128
381
  },
129
382
  "locations": {
@@ -132,13 +385,40 @@
132
385
  "moreHref": "/locations",
133
386
  "description": "The organisation's physical places, primary first.",
134
387
  "item": {
135
- "id": { "type": "string", "since": "1.0" },
136
- "name": { "type": "string", "since": "1.0", "max": 200 },
137
- "address": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
138
- "latitude": { "type": "number", "since": "1.0", "nullable": true },
139
- "longitude": { "type": "number", "since": "1.0", "nullable": true },
140
- "primary": { "type": "boolean", "since": "1.0" },
141
- "directionsHref": { "type": "string", "since": "1.0", "nullable": true }
388
+ "id": {
389
+ "type": "string",
390
+ "since": "1.0"
391
+ },
392
+ "name": {
393
+ "type": "string",
394
+ "since": "1.0",
395
+ "max": 200
396
+ },
397
+ "address": {
398
+ "type": "string",
399
+ "since": "1.0",
400
+ "nullable": true,
401
+ "max": 500
402
+ },
403
+ "latitude": {
404
+ "type": "number",
405
+ "since": "1.0",
406
+ "nullable": true
407
+ },
408
+ "longitude": {
409
+ "type": "number",
410
+ "since": "1.0",
411
+ "nullable": true
412
+ },
413
+ "primary": {
414
+ "type": "boolean",
415
+ "since": "1.0"
416
+ },
417
+ "directionsHref": {
418
+ "type": "string",
419
+ "since": "1.0",
420
+ "nullable": true
421
+ }
142
422
  }
143
423
  },
144
424
  "schedules": {
@@ -147,8 +427,17 @@
147
427
  "moreHref": null,
148
428
  "description": "TYPE-DERIVED recurring schedules. `type` is an OPEN enum; 'worship' is the first (its shape is the layout worship contract: times, labels, today, next). Other institutions arrive as new types; render the types you know, ignore the rest.",
149
429
  "item": {
150
- "type": { "type": "string", "since": "1.0", "enumOpen": ["worship"] },
151
- "content": { "type": "object", "since": "1.0" }
430
+ "type": {
431
+ "type": "string",
432
+ "since": "1.0",
433
+ "enumOpen": [
434
+ "worship"
435
+ ]
436
+ },
437
+ "content": {
438
+ "type": "object",
439
+ "since": "1.0"
440
+ }
152
441
  }
153
442
  },
154
443
  "about": {
@@ -157,8 +446,15 @@
157
446
  "moreHref": null,
158
447
  "description": "The admin-authored page composition: the ordered sections of the home/about surface. `type` is the section catalogue's OPEN enum; `content` is that section type's fields. This is what `section.*` re-addresses; a mega-page template can walk it directly.",
159
448
  "item": {
160
- "type": { "type": "string", "since": "1.0", "enumOpen": [] },
161
- "content": { "type": "object", "since": "1.0" }
449
+ "type": {
450
+ "type": "string",
451
+ "since": "1.0",
452
+ "enumOpen": []
453
+ },
454
+ "content": {
455
+ "type": "object",
456
+ "since": "1.0"
457
+ }
162
458
  }
163
459
  }
164
460
  }