@port60/template-kit 0.10.0 → 0.12.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 +1 -1
- package/src/lib/agentsMd.mjs +7 -7
- package/src/vendor/contract/v1/content-model.json +0 -28
- package/src/vendor/contract/v1/context.json +1 -185
- package/src/vendor/contract/v1/islands.json +0 -14
- package/src/vendor/contract/v1/manifest.schema.json +1 -3
- package/src/vendor/contract/v1/sections.json +6 -74
- package/src/vendor/contract/v1/tokens.json +3 -0
- package/src/vendor/validator/fixture-art.mjs +0 -2
- package/src/vendor/validator/platform-base.css +135 -9
- package/src/vendor/validator/preview.mjs +1 -9
- package/src/vendor/validator/validate.mjs +2 -6
- package/starter/manifest.json +6 -4
- package/starter/sections/impactMap.liquid +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@port60/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|
package/src/lib/agentsMd.mjs
CHANGED
|
@@ -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,
|
|
84
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
- Submission and behaviour surfaces such as \`newsletter_signup\`, \`
|
|
128
|
-
\`
|
|
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,
|
|
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",
|
|
@@ -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",
|
|
@@ -966,6 +892,12 @@
|
|
|
966
892
|
"kind": "text",
|
|
967
893
|
"required": false,
|
|
968
894
|
"description": "Optional introduction."
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
"name": "mapSlug",
|
|
898
|
+
"kind": "text",
|
|
899
|
+
"required": false,
|
|
900
|
+
"description": "WHICH map renders here, by its address (the Pages editor's placement picker writes it; the Maps area is where maps exist). Absent = the tenant's first published map."
|
|
969
901
|
}
|
|
970
902
|
],
|
|
971
903
|
"minimal": {},
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
{ "name": "--accent", "description": "Action accent (the donate CTA family)." },
|
|
13
13
|
{ "name": "--accent-strong", "description": "Darker accent." },
|
|
14
14
|
{ "name": "--gold", "description": "Highlight tint (marks, ratings)." },
|
|
15
|
+
{ "name": "--marker", "description": "Impact-map pin colour (the live-map markers are a zoom-scaling circle layer that take this colour). Defaults to --accent." },
|
|
16
|
+
{ "name": "--map-mode", "description": "Impact-map basemap colour mode read by the map island: auto (derive the map's paper/ink from the theme), light, dark, or muted. Default auto." },
|
|
17
|
+
{ "name": "--map-projection", "description": "Impact-map projection: flat (default) or globe — the planet as a rotatable sphere with atmosphere." },
|
|
15
18
|
{ "name": "--line", "description": "Hairline borders." },
|
|
16
19
|
{ "name": "--line-strong", "description": "Emphasised borders (selected, focus)." },
|
|
17
20
|
{ "name": "--danger", "description": "Errors and destructive affordances." },
|
|
@@ -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
|
|
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
--accent: #ef6044;
|
|
21
21
|
--accent-strong: #d94d33;
|
|
22
22
|
--gold: #f3b13b;
|
|
23
|
+
/* Impact map. --marker = the map pin colour (a deliberate brand accent by default; the live-map
|
|
24
|
+
markers are a zoom-scaling MapLibre circle layer that takes this colour). --map-mode = the
|
|
25
|
+
basemap colour mode read by the map island: `auto` derives the map's paper/ink from the theme
|
|
26
|
+
neutrals below, `light`/`dark`/`muted` force a base. Template-owned — set in theme.css or exposed
|
|
27
|
+
as Appearance knobs; the tenant never edits map colour directly. */
|
|
28
|
+
--marker: var(--accent);
|
|
29
|
+
--map-mode: auto;
|
|
30
|
+
/* flat (default) or globe — MapLibre's own globe projection, a rotatable sphere. */
|
|
31
|
+
--map-projection: flat;
|
|
23
32
|
--line: rgba(31, 42, 51, 0.1);
|
|
24
33
|
--line-strong: rgba(18, 135, 122, 0.28);
|
|
25
34
|
--danger: #c0392b;
|
|
@@ -953,6 +962,22 @@ textarea.p60-form-control {
|
|
|
953
962
|
gap: 1rem;
|
|
954
963
|
}
|
|
955
964
|
|
|
965
|
+
/* MOBILE BREAKOUT (owner, 2026-08-17): on phones the nesting stack squashes the card badly —
|
|
966
|
+
measured on Sanctuary: 390px viewport → 346 container → 294 aside → 249px card → ~195px of
|
|
967
|
+
actual controls. A donate form is a working control surface; it gets the full screen width
|
|
968
|
+
(edge to edge, side borders/radius dropped, its own padding kept). The negative-margin trick
|
|
969
|
+
escapes ANY wrapper depth; layouts here are symmetric, so the sheet lands exactly 0..100vw. */
|
|
970
|
+
@media (max-width: 640px) {
|
|
971
|
+
.donate-card {
|
|
972
|
+
width: 100vw;
|
|
973
|
+
margin-left: calc(50% - 50vw);
|
|
974
|
+
border-radius: 0;
|
|
975
|
+
border-left: 0;
|
|
976
|
+
border-right: 0;
|
|
977
|
+
padding: 1.4rem 1.1rem;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
956
981
|
/* Optional cause image banner at the top of the widget (set per-cause in tenant-admin). */
|
|
957
982
|
.donate-cause-img {
|
|
958
983
|
width: 100%;
|
|
@@ -1966,6 +1991,30 @@ textarea.p60-form-control {
|
|
|
1966
1991
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
1967
1992
|
gap: 1.5rem;
|
|
1968
1993
|
}
|
|
1994
|
+
/* MOBILE: the stacked grid becomes an edge-to-edge SWIPE ROW with a peek (owner, 2026-08-17) —
|
|
1995
|
+
the scroller bleeds to the screen edges, each card keeps its padding, the first card aligns to
|
|
1996
|
+
the text gutter, and the cut-off next card IS the "swipe me" affordance (the reference maps'
|
|
1997
|
+
card idiom). Snap keeps cards settling cleanly; the scrollbar is hidden, the peek replaces it. */
|
|
1998
|
+
@media (max-width: 640px) {
|
|
1999
|
+
.article-grid {
|
|
2000
|
+
display: flex;
|
|
2001
|
+
width: 100vw;
|
|
2002
|
+
margin-left: calc(50% - 50vw);
|
|
2003
|
+
overflow-x: auto;
|
|
2004
|
+
-webkit-overflow-scrolling: touch;
|
|
2005
|
+
scroll-snap-type: x proximity;
|
|
2006
|
+
gap: 0.9rem;
|
|
2007
|
+
padding: 0.2rem 22px 0.6rem;
|
|
2008
|
+
scroll-padding-left: 22px;
|
|
2009
|
+
scrollbar-width: none;
|
|
2010
|
+
}
|
|
2011
|
+
.article-grid::-webkit-scrollbar { display: none; }
|
|
2012
|
+
.article-grid > .article-card {
|
|
2013
|
+
flex: 0 0 78%;
|
|
2014
|
+
max-width: 330px;
|
|
2015
|
+
scroll-snap-align: start;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1969
2018
|
.article-card {
|
|
1970
2019
|
display: flex;
|
|
1971
2020
|
flex-direction: column;
|
|
@@ -2355,6 +2404,26 @@ textarea.p60-form-control {
|
|
|
2355
2404
|
align-items: center;
|
|
2356
2405
|
gap: 0.5rem;
|
|
2357
2406
|
}
|
|
2407
|
+
/* MOBILE: the rotator is MEDIA — it bleeds edge to edge like the hero and the map, with the
|
|
2408
|
+
arrows overlaid on the image instead of eating width beside it. */
|
|
2409
|
+
@media (max-width: 640px) {
|
|
2410
|
+
.events-carousel {
|
|
2411
|
+
position: relative;
|
|
2412
|
+
width: 100vw;
|
|
2413
|
+
margin-left: calc(50% - 50vw);
|
|
2414
|
+
gap: 0;
|
|
2415
|
+
}
|
|
2416
|
+
.events-carousel .carousel-slide { border-radius: 0; }
|
|
2417
|
+
.events-carousel > .carousel-arrow {
|
|
2418
|
+
position: absolute;
|
|
2419
|
+
top: 50%;
|
|
2420
|
+
transform: translateY(-50%);
|
|
2421
|
+
z-index: 2;
|
|
2422
|
+
opacity: 0.9;
|
|
2423
|
+
}
|
|
2424
|
+
.events-carousel > .carousel-arrow:first-of-type { left: 10px; }
|
|
2425
|
+
.events-carousel > .carousel-arrow:last-of-type { right: 10px; }
|
|
2426
|
+
}
|
|
2358
2427
|
.carousel-slide {
|
|
2359
2428
|
position: relative;
|
|
2360
2429
|
flex: 1;
|
|
@@ -4228,23 +4297,80 @@ textarea.p60-form-control {
|
|
|
4228
4297
|
.people-grid { grid-template-columns: 1fr; }
|
|
4229
4298
|
}
|
|
4230
4299
|
|
|
4231
|
-
/* ── Impact map island (WP4) —
|
|
4232
|
-
|
|
4300
|
+
/* ── Impact map island (WP4) — the OVERLAY borrows the theme: pins wear --marker (a brand accent by
|
|
4301
|
+
default), the card is a themed --surface. The BASEMAP colour is derived by the map island from the
|
|
4302
|
+
theme + --map-mode (see lib/mapTheme.ts). Brand on the overlay, neutrals on the map — the recipe
|
|
4303
|
+
that stays legible on any theme. Templates restyle any of this via the tokens or these classes. */
|
|
4233
4304
|
.impact-map { margin: 24px 0; }
|
|
4234
4305
|
.impact-map-head h2 { margin: 0 0 6px; }
|
|
4235
4306
|
.impact-map-intro { margin: 0 0 14px; opacity: 0.85; }
|
|
4236
|
-
.impact-map-
|
|
4237
|
-
|
|
4307
|
+
.impact-map-stats { margin: 0 0 14px; color: var(--muted); font-weight: 600; letter-spacing: 0.01em; }
|
|
4308
|
+
/* The stable stage, shaped like THE WORLD BAND ITSELF: the island's planet view is all 360° of
|
|
4309
|
+
longitude by 84°N–58°S (whole Greenland in, only the polar void and Antarctica trimmed), which in
|
|
4310
|
+
Web Mercator is a 1.497:1 box — so a frame with that exact ratio holds the full planet edge to
|
|
4311
|
+
edge, full width AND full height, at any screen size. A ratio wider than the band (a max-height
|
|
4312
|
+
clamp once made it so on large screens) turns the fit height-constrained and the world renders
|
|
4313
|
+
NARROWER than the frame — dead bars at the sides; a narrower ratio (min-height on phones) just
|
|
4314
|
+
adds benign vertical margin. Relative + clipped so MapLibre always has a real box and can never
|
|
4315
|
+
escape it. Keep this ratio in step with the island's planet bounds. */
|
|
4316
|
+
.impact-map-stage { position: relative; width: 100%; aspect-ratio: 1.497; min-height: 300px;
|
|
4317
|
+
border-radius: 14px; overflow: hidden; }
|
|
4318
|
+
/* width:100% is LOAD-BEARING: with height pinned by min-height and width left auto, CSS derives
|
|
4319
|
+
width FROM the aspect-ratio (300 x 1.497 = 449px) — on phones the stage overflowed the page
|
|
4320
|
+
sideways, shoving every other section into a lopsided "left with a right gutter" layout. Width
|
|
4321
|
+
pinned, the ratio yields height and min-height only ever makes the stage TALLER. */
|
|
4322
|
+
/* MOBILE BREAKOUT: a map is a canvas — every pixel is information, nothing needs a margin — so on
|
|
4323
|
+
phones it bleeds edge to edge (media bleeds, prose gutters: the hero already sets the rhythm).
|
|
4324
|
+
Cooperative gestures keep page-scrolling safe over the full-width canvas. */
|
|
4325
|
+
@media (max-width: 640px) {
|
|
4326
|
+
.impact-map-stage {
|
|
4327
|
+
width: 100vw;
|
|
4328
|
+
margin-left: calc(50% - 50vw);
|
|
4329
|
+
border-radius: 0;
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
.impact-map-canvas { position: absolute; inset: 0;
|
|
4333
|
+
background: linear-gradient(160deg, rgba(0, 0, 0, 0.28), rgba(0, 0, 0, 0.55)), var(--muted); }
|
|
4238
4334
|
.impact-map-pin { position: absolute; width: 16px; height: 16px; border-radius: 50%;
|
|
4239
|
-
transform: translate(-50%, -50%); border: 2px solid
|
|
4335
|
+
transform: translate(-50%, -50%); border: 2px solid var(--surface); background: var(--marker);
|
|
4240
4336
|
cursor: pointer; padding: 0; transition: transform 120ms ease; }
|
|
4241
4337
|
.impact-map-pin:hover, .impact-map-pin:focus-visible { transform: translate(-50%, -50%) scale(1.35); }
|
|
4242
|
-
.impact-map-pin--active { box-shadow: 0 0 0 5px
|
|
4243
|
-
.impact-map-card { margin-top: 14px;
|
|
4338
|
+
.impact-map-pin--active { box-shadow: 0 0 0 5px var(--primary-soft); }
|
|
4339
|
+
.impact-map-card { margin-top: 14px; background: var(--surface); color: var(--ink);
|
|
4340
|
+
border: 1px solid var(--line); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm);
|
|
4244
4341
|
padding: 16px 18px; max-width: 34rem; }
|
|
4245
4342
|
.impact-map-card-kind { display: inline-block; font-size: 11px; font-weight: 700;
|
|
4246
|
-
letter-spacing: 0.06em; text-transform: uppercase;
|
|
4343
|
+
letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }
|
|
4247
4344
|
.impact-map-card-title { margin: 0 0 6px; }
|
|
4248
4345
|
.impact-map-card-summary { margin: 0 0 10px; opacity: 0.9; }
|
|
4249
|
-
.impact-map-card-cta { font-weight: 600; }
|
|
4346
|
+
.impact-map-card-cta { font-weight: 600; color: var(--primary); }
|
|
4250
4347
|
.impact-map-empty { opacity: 0.7; }
|
|
4348
|
+
/* The live tile layer (slice 2): fills the stage from first render so MapLibre measures a real box;
|
|
4349
|
+
the pin canvas overlays it until (unless) the map is ready, then unmounts to reveal the map.
|
|
4350
|
+
Sized with width/height (NOT inset) because MapLibre forces position:relative on this element,
|
|
4351
|
+
which would defeat absolute insets and collapse the box to zero height. */
|
|
4352
|
+
.impact-map-gl { width: 100%; height: 100%; }
|
|
4353
|
+
/* Live-map markers are a MapLibre circle layer (WebGL) — they scale with zoom and take their colour
|
|
4354
|
+
from --marker (circle-color) — so there is no DOM marker element to style here. The pin canvas
|
|
4355
|
+
fallback above keeps its own .impact-map-pin dots for the no-tiles case. */
|
|
4356
|
+
/* The INLINE marker card (a MapLibre popup anchored at the clicked marker — where "See more"
|
|
4357
|
+
lives): the shell takes the theme's surface; the tip is recoloured per anchor side to match. */
|
|
4358
|
+
.impact-map-popup .maplibregl-popup-content { background: var(--surface); color: var(--ink);
|
|
4359
|
+
border: 1px solid var(--line); border-radius: var(--radius-sm); box-shadow: var(--shadow);
|
|
4360
|
+
padding: 14px 16px; }
|
|
4361
|
+
.impact-map-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
|
|
4362
|
+
.impact-map-popup.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,
|
|
4363
|
+
.impact-map-popup.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip { border-top-color: var(--surface); }
|
|
4364
|
+
.impact-map-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip,
|
|
4365
|
+
.impact-map-popup.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,
|
|
4366
|
+
.impact-map-popup.maplibregl-popup-anchor-top-right .maplibregl-popup-tip { border-bottom-color: var(--surface); }
|
|
4367
|
+
.impact-map-popup.maplibregl-popup-anchor-left .maplibregl-popup-tip { border-right-color: var(--surface); }
|
|
4368
|
+
.impact-map-popup.maplibregl-popup-anchor-right .maplibregl-popup-tip { border-left-color: var(--surface); }
|
|
4369
|
+
.impact-map-popup-card .impact-map-card-title { margin: 4px 0 6px; font-size: 1.02rem; }
|
|
4370
|
+
.impact-map-popup-card .impact-map-card-summary { margin: 0 0 8px; }
|
|
4371
|
+
/* The point's image (when it has one): bleeds to the popup's top edge; plain rounded in the
|
|
4372
|
+
fallback card. Absent image = the card simply starts at the kind line. */
|
|
4373
|
+
.impact-map-card-image { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
|
|
4374
|
+
border-radius: 10px; margin: 0 0 10px; }
|
|
4375
|
+
.impact-map-popup-card .impact-map-card-image { width: calc(100% + 32px); margin: -14px -16px 10px;
|
|
4376
|
+
border-radius: calc(var(--radius-sm) - 1px) calc(var(--radius-sm) - 1px) 0 0; }
|
|
@@ -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.
|
package/starter/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"format": "port60-liquid@1",
|
|
5
5
|
"label": "Starter",
|
|
6
6
|
"description": "The reference template for the Port60 dialect, the developer docs' worked example and the base to copy when building your own. 1.1.0 adds the layout slot: the template owns the header, navigation and footer chrome.",
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
"values",
|
|
16
16
|
"cta",
|
|
17
17
|
"people",
|
|
18
|
-
"campaigns"
|
|
18
|
+
"campaigns",
|
|
19
|
+
"impactMap"
|
|
19
20
|
],
|
|
20
21
|
"islands": [
|
|
21
22
|
"donation_widget",
|
|
22
23
|
"member_menu",
|
|
23
|
-
"hero_carousel"
|
|
24
|
+
"hero_carousel",
|
|
25
|
+
"map"
|
|
24
26
|
],
|
|
25
27
|
"layout": true,
|
|
26
28
|
"heroImagery": true,
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
}
|
|
47
49
|
]
|
|
48
50
|
},
|
|
49
|
-
"changelog": "
|
|
51
|
+
"changelog": "Adds the impact map section: pin your located work on a themed map of where you work.",
|
|
50
52
|
"imagery": {
|
|
51
53
|
"hero": {
|
|
52
54
|
"idealAspect": "16:9",
|