@port60/template-kit 0.10.0 → 0.11.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@port60/template-kit",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
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": {
@@ -80,8 +80,8 @@ platform accepts it; if it fails here, the upload will fail identically.
80
80
 
81
81
  Everything you read comes from ONE tree: \`site\`, \`site.brand\`, \`site.nav\`,
82
82
  \`site.socials\`, \`site.locale\` and the typed collections under \`site.content.*\`
83
- (services, events, articles, campaigns, causes, courses, volunteering, media, resources,
84
- locations, schedules, about). Four rules it never breaks, so neither should you:
83
+ (services, events, articles, campaigns, causes, courses, resources, locations,
84
+ schedules, about). Four rules it never breaks, so neither should you:
85
85
 
86
86
  - Every collection is BOUNDED (a documented cap plus a \`moreHref\`), link onward, never
87
87
  assume you have everything.
@@ -121,11 +121,11 @@ excludes it, and conformance proofs always run on the canonical fixtures.
121
121
  - Navigation can contain two levels below a top item. Render every supplied child and branch on
122
122
  optional \`group\`, \`description\`, \`imageUrl\` and \`megaMenu\` promo metadata. Never hardcode
123
123
  menu groups that are not in \`nav\`.
124
- - Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources, locations,
125
- volunteering opportunities and structured media. Derive or omit when a collection is empty and
126
- use the supplied URLs rather than constructing routes.
127
- - Submission and behaviour surfaces such as \`newsletter_signup\`, \`volunteer_signup\`, \`form\`,
128
- \`search\`, \`language_switch\` and \`next_prayer\` are platform islands. Place and style them;
124
+ - Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources and
125
+ locations. Derive or omit when a collection is empty and use the supplied URLs rather than
126
+ constructing routes.
127
+ - Submission and behaviour surfaces such as \`newsletter_signup\`, \`form\`, \`search\`,
128
+ \`language_switch\` and \`next_prayer\` are platform islands. Place and style them;
129
129
  never reproduce their API calls or consent behaviour.
130
130
  - \`manifest.imagery.hero\` (optional but recommended): declare the photo shape YOUR hero
131
131
  composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`), the charity's editor
@@ -111,34 +111,6 @@
111
111
  "href": { "type": "string", "since": "1.0" }
112
112
  }
113
113
  },
114
- "volunteering": {
115
- "since": "1.0",
116
- "cap": 12,
117
- "moreHref": "/volunteering",
118
- "description": "Open volunteering opportunities.",
119
- "item": {
120
- "id": { "type": "string", "since": "1.0" },
121
- "slug": { "type": "string", "since": "1.0" },
122
- "title": { "type": "string", "since": "1.0", "max": 200 },
123
- "summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
124
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
125
- "content": { "type": "object", "since": "1.0", "nullable": true }
126
- }
127
- },
128
- "media": {
129
- "since": "1.0",
130
- "cap": 12,
131
- "moreHref": "/media",
132
- "description": "Published media items. `content.mediaType` is an OPEN enum (VIDEO and AUDIO today).",
133
- "item": {
134
- "id": { "type": "string", "since": "1.0" },
135
- "slug": { "type": "string", "since": "1.0" },
136
- "title": { "type": "string", "since": "1.0", "max": 200 },
137
- "summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
138
- "imageUrl": { "type": "string", "since": "1.0", "nullable": true },
139
- "content": { "type": "object", "since": "1.0", "nullable": true }
140
- }
141
- },
142
114
  "resources": {
143
115
  "since": "1.0",
144
116
  "cap": 24,
@@ -47,7 +47,7 @@
47
47
  "section",
48
48
  "layout"
49
49
  ],
50
- "description": "The typed collections: services, events, articles, campaigns, causes, courses, volunteering, media, resources, locations, schedules, about. EVERY collection is bounded (cap + moreHref in the model registry), every enum is open, every optional field explicitly nullable, and the model only ever grows. The validator computes your template's content footprint from the paths you read and stamps the required minimum model version at publish; dynamic indexing into site.content is refused so the footprint stays decidable."
50
+ "description": "The typed collections: services, events, articles, campaigns, causes, courses, resources, locations, schedules, about. EVERY collection is bounded (cap + moreHref in the model registry), every enum is open, every optional field explicitly nullable, and the model only ever grows. The validator computes your template's content footprint from the paths you read and stamps the required minimum model version at publish; dynamic indexing into site.content is refused so the footprint stays decidable."
51
51
  },
52
52
  {
53
53
  "path": "section",
@@ -430,22 +430,6 @@
430
430
  "description": "A public location with id, name, address, latitude, longitude, primary and directionsHref. Provider references and private notes are excluded.",
431
431
  "deprecated": true
432
432
  },
433
- {
434
- "path": "opportunities",
435
- "availableIn": [
436
- "section:volunteering"
437
- ],
438
- "description": "Published volunteering opportunities in display order. Each item contains id, slug, title, summary, imageUrl and content with location, schedule, commitment, dates, expected hours and shift dates. Sign-up remains the volunteer_signup island.",
439
- "deprecated": true
440
- },
441
- {
442
- "path": "media",
443
- "availableIn": [
444
- "section:media"
445
- ],
446
- "description": "Published structured media entries. Each item contains id, slug, title, summary, imageUrl and content with mediaType, mediaUrl, speaker, publishedAt and duration.",
447
- "deprecated": true
448
- },
449
433
  {
450
434
  "path": "infoEvents",
451
435
  "availableIn": [
@@ -1738,92 +1722,6 @@
1738
1722
  }
1739
1723
  ]
1740
1724
  },
1741
- "volunteering": {
1742
- "opportunities": [
1743
- {
1744
- "id": "aaaaaaaa-1111-4111-8111-111111111111",
1745
- "slug": "garden-volunteer",
1746
- "title": "Garden Volunteer",
1747
- "summary": "Keep the Foundry Lane beds thriving: sowing, watering, and showing first-timers where the trowels live.",
1748
- "imageUrl": "p60fixture:volunteer/garden",
1749
- "content": {
1750
- "location": "Foundry Lane Garden",
1751
- "schedule": "Saturday mornings",
1752
- "commitment": "Twice a month",
1753
- "hoursExpected": "3 hours"
1754
- }
1755
- },
1756
- {
1757
- "id": "aaaaaaaa-1111-4111-8111-111111111112",
1758
- "slug": "kitchen-crew",
1759
- "title": "Wednesday Kitchen Crew",
1760
- "summary": "Help cook and serve the weekly community meal. Full food hygiene training provided.",
1761
- "imageUrl": "p60fixture:volunteer/kitchen",
1762
- "content": {
1763
- "location": "St Chad's Hall",
1764
- "schedule": "Wednesdays 9am to 2pm",
1765
- "commitment": "Weekly",
1766
- "hoursExpected": "5 hours"
1767
- }
1768
- },
1769
- {
1770
- "id": "aaaaaaaa-1111-4111-8111-111111111113",
1771
- "slug": "food-bank-driver",
1772
- "title": "Food Bank Driver",
1773
- "summary": "Collect supermarket surplus on the morning round. A full licence and a friendly wave required.",
1774
- "imageUrl": null,
1775
- "content": {
1776
- "location": "Foundry Lane depot",
1777
- "schedule": "Weekday mornings",
1778
- "commitment": "One morning a week",
1779
- "hoursExpected": "2 hours"
1780
- }
1781
- }
1782
- ]
1783
- },
1784
- "media": {
1785
- "media": [
1786
- {
1787
- "id": "bbbbbbbb-2222-4222-8222-222222222221",
1788
- "slug": "garden-opening-film",
1789
- "title": "The Garden Opening, on Film",
1790
- "summary": "Four minutes from the gates opening, shot by the youth club media group.",
1791
- "imageUrl": "p60fixture:media/gardenfilm",
1792
- "content": {
1793
- "mediaType": "VIDEO",
1794
- "mediaUrl": "/fixture/garden-opening",
1795
- "speaker": null,
1796
- "duration": "4 minutes"
1797
- }
1798
- },
1799
- {
1800
- "id": "bbbbbbbb-2222-4222-8222-222222222222",
1801
- "slug": "trustee-talk-2030",
1802
- "title": "Where the Trust Goes Next",
1803
- "summary": "Chair of trustees Priya Sharma on the three-year plan, recorded at the spring members' evening.",
1804
- "imageUrl": "p60fixture:media/talk",
1805
- "content": {
1806
- "mediaType": "VIDEO",
1807
- "mediaUrl": "/fixture/trustee-talk",
1808
- "speaker": "Priya Sharma",
1809
- "duration": "18 minutes"
1810
- }
1811
- },
1812
- {
1813
- "id": "bbbbbbbb-2222-4222-8222-222222222223",
1814
- "slug": "sound-of-the-kitchen",
1815
- "title": "Sound of the Kitchen",
1816
- "summary": "An audio portrait of Wednesday lunchtime, best enjoyed hungry.",
1817
- "imageUrl": null,
1818
- "content": {
1819
- "mediaType": "AUDIO",
1820
- "mediaUrl": "/fixture/kitchen-audio",
1821
- "speaker": null,
1822
- "duration": "9 minutes"
1823
- }
1824
- }
1825
- ]
1826
- },
1827
1725
  "impactMap": {
1828
1726
  "impactMap": {
1829
1727
  "slug": "our-work",
@@ -2357,88 +2255,6 @@
2357
2255
  "href": "/courses"
2358
2256
  }
2359
2257
  ],
2360
- "volunteering": [
2361
- {
2362
- "id": "aaaaaaaa-1111-4111-8111-111111111111",
2363
- "slug": "garden-volunteer",
2364
- "title": "Garden Volunteer",
2365
- "summary": "Keep the Foundry Lane beds thriving: sowing, watering, and showing first-timers where the trowels live.",
2366
- "imageUrl": "p60fixture:volunteer/garden",
2367
- "content": {
2368
- "location": "Foundry Lane Garden",
2369
- "schedule": "Saturday mornings",
2370
- "commitment": "Twice a month",
2371
- "hoursExpected": "3 hours"
2372
- }
2373
- },
2374
- {
2375
- "id": "aaaaaaaa-1111-4111-8111-111111111112",
2376
- "slug": "kitchen-crew",
2377
- "title": "Wednesday Kitchen Crew",
2378
- "summary": "Help cook and serve the weekly community meal. Full food hygiene training provided.",
2379
- "imageUrl": "p60fixture:volunteer/kitchen",
2380
- "content": {
2381
- "location": "St Chad's Hall",
2382
- "schedule": "Wednesdays 9am to 2pm",
2383
- "commitment": "Weekly",
2384
- "hoursExpected": "5 hours"
2385
- }
2386
- },
2387
- {
2388
- "id": "aaaaaaaa-1111-4111-8111-111111111113",
2389
- "slug": "food-bank-driver",
2390
- "title": "Food Bank Driver",
2391
- "summary": "Collect supermarket surplus on the morning round. A full licence and a friendly wave required.",
2392
- "imageUrl": null,
2393
- "content": {
2394
- "location": "Foundry Lane depot",
2395
- "schedule": "Weekday mornings",
2396
- "commitment": "One morning a week",
2397
- "hoursExpected": "2 hours"
2398
- }
2399
- }
2400
- ],
2401
- "media": [
2402
- {
2403
- "id": "bbbbbbbb-2222-4222-8222-222222222221",
2404
- "slug": "garden-opening-film",
2405
- "title": "The Garden Opening, on Film",
2406
- "summary": "Four minutes from the gates opening, shot by the youth club media group.",
2407
- "imageUrl": "p60fixture:media/gardenfilm",
2408
- "content": {
2409
- "mediaType": "VIDEO",
2410
- "mediaUrl": "/fixture/garden-opening",
2411
- "speaker": null,
2412
- "duration": "4 minutes"
2413
- }
2414
- },
2415
- {
2416
- "id": "bbbbbbbb-2222-4222-8222-222222222222",
2417
- "slug": "trustee-talk-2030",
2418
- "title": "Where the Trust Goes Next",
2419
- "summary": "Chair of trustees Priya Sharma on the three-year plan, recorded at the spring members' evening.",
2420
- "imageUrl": "p60fixture:media/talk",
2421
- "content": {
2422
- "mediaType": "VIDEO",
2423
- "mediaUrl": "/fixture/trustee-talk",
2424
- "speaker": "Priya Sharma",
2425
- "duration": "18 minutes"
2426
- }
2427
- },
2428
- {
2429
- "id": "bbbbbbbb-2222-4222-8222-222222222223",
2430
- "slug": "sound-of-the-kitchen",
2431
- "title": "Sound of the Kitchen",
2432
- "summary": "An audio portrait of Wednesday lunchtime, best enjoyed hungry.",
2433
- "imageUrl": null,
2434
- "content": {
2435
- "mediaType": "AUDIO",
2436
- "mediaUrl": "/fixture/kitchen-audio",
2437
- "speaker": null,
2438
- "duration": "9 minutes"
2439
- }
2440
- }
2441
- ],
2442
2258
  "resources": [
2443
2259
  {
2444
2260
  "id": "88888888-8888-8888-8888-888888888881",
@@ -203,20 +203,6 @@
203
203
  ".site-search-empty"
204
204
  ]
205
205
  },
206
- {
207
- "name": "volunteer_signup",
208
- "label": "Volunteer expression of interest",
209
- "status": "available",
210
- "description": "Lists published opportunities and captures a privacy-recorded expression of interest. Templates own placement and styling; validation, abuse checks and submission storage remain platform-owned.",
211
- "params": [],
212
- "stylingApi": [
213
- ".volunteer-signup",
214
- ".volunteer-field",
215
- ".volunteer-privacy",
216
- ".volunteer-submit",
217
- ".volunteer-message"
218
- ]
219
- },
220
206
  {
221
207
  "name": "form",
222
208
  "label": "Public form",
@@ -36,14 +36,12 @@
36
36
  "events",
37
37
  "articles",
38
38
  "services",
39
- "volunteering",
40
39
  "forms",
41
40
  "resources",
42
41
  "locations",
43
42
  "newsletter",
44
43
  "i18n",
45
- "search",
46
- "media"
44
+ "search"
47
45
  ]
48
46
  },
49
47
  "uniqueItems": true,
@@ -775,80 +775,6 @@
775
775
  "text": "Two front doors in the middle of Alderford, both step-free."
776
776
  }
777
777
  },
778
- {
779
- "type": "volunteering",
780
- "label": "Volunteering opportunities",
781
- "pages": [
782
- "home"
783
- ],
784
- "description": "Published opportunities from the volunteering module. Templates render the cards and place volunteer_signup for the platform-owned, privacy-recorded expression-of-interest flow.",
785
- "context": [
786
- "opportunities"
787
- ],
788
- "fields": [
789
- {
790
- "name": "eyebrow",
791
- "kind": "text",
792
- "required": false,
793
- "description": "Short kicker."
794
- },
795
- {
796
- "name": "heading",
797
- "kind": "text",
798
- "required": false,
799
- "description": "The section heading."
800
- },
801
- {
802
- "name": "text",
803
- "kind": "text",
804
- "required": false,
805
- "description": "Optional introduction."
806
- }
807
- ],
808
- "minimal": {},
809
- "sample": {
810
- "eyebrow": "Get involved",
811
- "heading": "Volunteer with us",
812
- "text": "Two hours or twenty, there is a role that fits the time you have."
813
- }
814
- },
815
- {
816
- "type": "media",
817
- "label": "Audio, video and livestreams",
818
- "pages": [
819
- "home"
820
- ],
821
- "description": "Structured public audio, video and livestream entries with titles, speakers, dates and safe media links.",
822
- "context": [
823
- "media"
824
- ],
825
- "fields": [
826
- {
827
- "name": "eyebrow",
828
- "kind": "text",
829
- "required": false,
830
- "description": "Short kicker."
831
- },
832
- {
833
- "name": "heading",
834
- "kind": "text",
835
- "required": false,
836
- "description": "The section heading."
837
- },
838
- {
839
- "name": "text",
840
- "kind": "text",
841
- "required": false,
842
- "description": "Optional introduction."
843
- }
844
- ],
845
- "minimal": {},
846
- "sample": {
847
- "eyebrow": "Media",
848
- "heading": "Watch and listen",
849
- "text": "Films, talks and the occasional kitchen soundscape from around the Trust."
850
- }
851
- },
852
778
  {
853
779
  "type": "people",
854
780
  "label": "People / team",
@@ -18,8 +18,6 @@ const PALETTES = {
18
18
  service: [['#2f4d3e', '#132a1f', '#9fc490'], ['#31495c', '#14222e', '#8fb8c9']],
19
19
  people: [['#4d4238', '#292019', '#d3a97c'], ['#3d4550', '#1f242b', '#b0a3c4']],
20
20
  course: [['#2d4a55', '#112530', '#83c1b0'], ['#4a3c2d', '#251d11', '#c1a583']],
21
- media: [['#26243a', '#100e1e', '#8f7fd9'], ['#3a2430', '#1e0f18', '#d97fae']],
22
- volunteer: [['#3e4a2d', '#1e2511', '#b7c183'], ['#2d3f4a', '#111e25', '#83a9c1']],
23
21
  impact: [['#44503c', '#232b1e', '#c2cf9f'], ['#503c46', '#2b1e25', '#cf9fb4']]
24
22
  };
25
23
 
@@ -147,12 +147,6 @@ function islandSkeleton(name, ctx = {}, fx = STUDIO_FX) {
147
147
  return `<label class="language-switch" data-p60-preview-island="language_switch">${previewNote(name)}<span class="language-switch-label">Language</span><select class="language-switch-select" disabled><option>English</option><option>Cymraeg</option><option>العربية</option></select></label>`;
148
148
  case 'search':
149
149
  return `<div class="site-search" data-p60-preview-island="search">${previewNote(name)}<form class="site-search-form"><label class="site-search-label">Search this site</label><div class="site-search-fields"><input class="site-search-input" type="search" disabled><button class="site-search-submit" type="button" disabled>Search</button></div></form><ul class="site-search-results"><li class="site-search-result"><span class="site-search-kind">Article</span><a class="site-search-link" href="#">The Community Garden Opens Its Gates</a><p class="site-search-summary">Two years of digging and Saturday mornings in the rain: the Foundry Lane garden is open.</p></li></ul></div>`;
150
- case 'volunteer_signup': {
151
- const opportunities = fx.sections?.volunteering?.opportunities ?? [];
152
- const options = (opportunities.length ? opportunities : [{ title: 'Garden Volunteer' }])
153
- .map((o) => `<option>${escapeHtml(o.title)}</option>`).join('');
154
- return `<form class="volunteer-signup" data-p60-preview-island="volunteer_signup">${previewNote(name)}<label class="volunteer-field"><span>Opportunity</span><select disabled>${options}</select></label><label class="volunteer-field"><span>Name</span><input disabled></label><label class="volunteer-field"><span>Email address</span><input disabled></label><label class="volunteer-privacy"><input type="checkbox" disabled><span>I agree that the organisation may respond.</span></label><button class="volunteer-submit" type="button" disabled>Register my interest</button></form>`;
155
- }
156
150
  case 'map': {
157
151
  // The impact-map skeleton: the fixture's points projected onto a token-themed canvas —
158
152
  // the same fallback rendering production uses until the platform tile layer is configured.
@@ -448,9 +442,7 @@ export async function renderStudioPreview(files, options = {}) {
448
442
  emergency: { causes: c.causes ?? [] },
449
443
  programmes: { services: c.services ?? [] },
450
444
  resources: { resources: c.resources ?? [] },
451
- locations: { locations: c.locations ?? [] },
452
- volunteering: { opportunities: c.volunteering ?? [] },
453
- media: { media: c.media ?? [] }
445
+ locations: { locations: c.locations ?? [] }
454
446
  }
455
447
  };
456
448
  }
@@ -94,14 +94,12 @@ export async function validateArtifact(files) {
94
94
  events: () => sections.has('events') || pages.has('events') || islands.has('events_carousel'),
95
95
  articles: () => sections.has('articles') || hasAny(pages, ['articles', 'article']) || islands.has('latest_articles'),
96
96
  services: () => sections.has('programmes'),
97
- volunteering: () => sections.has('volunteering') || islands.has('volunteer_signup'),
98
97
  forms: () => islands.has('form'),
99
98
  resources: () => sections.has('resources'),
100
99
  locations: () => sections.has('locations'),
101
100
  newsletter: () => islands.has('newsletter_signup'),
102
101
  i18n: () => islands.has('language_switch'),
103
- search: () => islands.has('search'),
104
- media: () => sections.has('media')
102
+ search: () => islands.has('search')
105
103
  };
106
104
  for (const capability of manifest?.requiresCapabilities ?? []) {
107
105
  if (!capabilitySurface[capability]?.()) {
@@ -236,9 +234,7 @@ export async function validateArtifact(files) {
236
234
  articles: { island: 'latest_articles', dataKey: 'latestArticles', sentinel: sentinelOf('articles', 'latestArticles', 'title') },
237
235
  campaigns: { island: null, dataKey: 'campaigns', sentinel: sentinelOf('campaigns', 'campaigns', 'title') },
238
236
  resources: { island: null, dataKey: 'resources', sentinel: sentinelOf('resources', 'resources', 'title') },
239
- locations: { island: null, dataKey: 'locations', sentinel: sentinelOf('locations', 'locations', 'name') },
240
- volunteering: { island: 'volunteer_signup', dataKey: 'opportunities', sentinel: sentinelOf('volunteering', 'opportunities', 'title') },
241
- media: { island: null, dataKey: 'media', sentinel: sentinelOf('media', 'media', 'title') }
237
+ locations: { island: null, dataKey: 'locations', sentinel: sentinelOf('locations', 'locations', 'name') }
242
238
  };
243
239
 
244
240
  // 2–4. Sections: catalogue membership, parse, fixture renders.