@port60/template-kit 0.9.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.
Files changed (39) hide show
  1. package/README.md +10 -3
  2. package/bin/cli.mjs +26 -4
  3. package/package.json +1 -1
  4. package/src/commands/content.mjs +42 -0
  5. package/src/commands/create.mjs +6 -6
  6. package/src/commands/dev.mjs +97 -9
  7. package/src/commands/model.mjs +34 -0
  8. package/src/commands/packageCmd.mjs +3 -3
  9. package/src/commands/refresh.mjs +48 -0
  10. package/src/commands/upgrade.mjs +44 -0
  11. package/src/commands/validate.mjs +2 -3
  12. package/src/lib/agentsMd.mjs +52 -24
  13. package/src/vendor/contract/v1/behaviours.json +4 -4
  14. package/src/vendor/contract/v1/content-bounds.json +3 -3
  15. package/src/vendor/contract/v1/content-model.json +165 -0
  16. package/src/vendor/contract/v1/context.json +1497 -289
  17. package/src/vendor/contract/v1/dialect.json +3 -3
  18. package/src/vendor/contract/v1/fonts.json +1 -1
  19. package/src/vendor/contract/v1/imagery.json +4 -4
  20. package/src/vendor/contract/v1/islands.json +242 -235
  21. package/src/vendor/contract/v1/layout.json +4 -4
  22. package/src/vendor/contract/v1/manifest.schema.json +10 -12
  23. package/src/vendor/contract/v1/sections.json +189 -107
  24. package/src/vendor/contract/v1/tokens.json +1 -1
  25. package/src/vendor/contract/v1.lock.json +1 -1
  26. package/src/vendor/engine/content-footprint.mjs +79 -0
  27. package/src/vendor/validator/fixture-art.mjs +107 -0
  28. package/src/vendor/validator/model-reference.mjs +92 -0
  29. package/src/vendor/validator/platform-base.css +21 -0
  30. package/src/vendor/validator/preview.mjs +144 -59
  31. package/src/vendor/validator/site-context.mjs +97 -0
  32. package/src/vendor/validator/validate.mjs +70 -17
  33. package/starter/assets/theme.css +10 -10
  34. package/starter/layout.liquid +15 -16
  35. package/starter/manifest.json +2 -2
  36. package/starter/sections/campaigns.liquid +5 -5
  37. package/starter/sections/homeHero.liquid +11 -11
  38. package/starter/sections/people.liquid +1 -1
  39. package/starter/sections/values.liquid +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "format": "port60-liquid@1",
3
- "description": "The Port60 template dialect — a strict, whitelisted subset of Liquid. Anything not listed here is rejected at parse time by the engine and at publish time by the conformance validator.",
3
+ "description": "The Port60 template dialect, a strict, whitelisted subset of Liquid. Anything not listed here is rejected at parse time by the engine and at publish time by the conformance validator.",
4
4
  "outputEscape": "escape",
5
5
  "rawFilter": "raw",
6
6
  "tags": [
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "name": "island",
18
18
  "usage": "{% island 'donation_widget' %}",
19
- "description": "Places a platform functional island (a live, hydrated component) at this position. Templates place islands, never implement them — the island's markup, behaviour and data are platform-owned and upgraded platform-side. The island name must be declared in the manifest's supports.islands."
19
+ "description": "Places a platform functional island (a live, hydrated component) at this position. Templates place islands, never implement them, the island's markup, behaviour and data are platform-owned and upgraded platform-side. The island name must be declared in the manifest's supports.islands."
20
20
  },
21
21
  {
22
22
  "name": "content",
@@ -34,6 +34,6 @@
34
34
  ],
35
35
  "excluded": {
36
36
  "tags": ["include", "render", "layout", "tablerow", "cycle", "increment", "decrement"],
37
- "reason": "File inclusion (no file system exists inside an artifact yet — a snippets mechanism arrives with the Layout slot), table markup generation, and stateful counters are outside the v1 dialect."
37
+ "reason": "File inclusion (no file system exists inside an artifact yet, a snippets mechanism arrives with the Layout slot), table markup generation, and stateful counters are outside the v1 dialect."
38
38
  }
39
39
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "The curated font catalogue behind `font` settings knobs. The tenant picks a FAMILY; the template's typographic system (weights, sizes, tracking) is author-owned — the host requests the template's declared weights for the chosen family, clamped to the family's real weights below (nearest wins). Curated, not the raw Google catalogue: every family here is open-licence (OFL/Apache), quality-checked, and tagged so non-technical admins choose by vibe. Served from Bunny Fonts (a privacy-first, EU-hosted drop-in mirror of the Google Fonts catalogue — same families, same css2 API). Additive-only within a contract major; never remove a family (published sites reference them).",
2
+ "description": "The curated font catalogue behind `font` settings knobs. The tenant picks a FAMILY; the template's typographic system (weights, sizes, tracking) is author-owned, the host requests the template's declared weights for the chosen family, clamped to the family's real weights below (nearest wins). Curated, not the raw Google catalogue: every family here is open-licence (OFL/Apache), quality-checked, and tagged so non-technical admins choose by vibe. Served from Bunny Fonts (a privacy-first, EU-hosted drop-in mirror of the Google Fonts catalogue, same families, same css2 API). Additive-only within a contract major; never remove a family (published sites reference them).",
3
3
  "provider": {
4
4
  "cssBase": "https://fonts.bunny.net/css2",
5
5
  "preconnect": "https://fonts.bunny.net"
@@ -1,23 +1,23 @@
1
1
  {
2
- "description": "Platform imagery envelopes — ADVICE, never enforcement. Entity images (events, courses, articles) render on SEVERAL surfaces at once (cards, carousel slides, detail banners), including platform-owned components, so their guidance is platform-level: the envelope every surface can live with. Template-composed slots (the home hero) are different — their ideal is the TEMPLATE's own design call, declared per-template in manifest.imagery and surfaced beside the upload. Editors measure the actual upload against the relevant guidance and advise; the charity always decides.",
2
+ "description": "Platform imagery envelopes, ADVICE, never enforcement. Entity images (events, courses, articles) render on SEVERAL surfaces at once (cards, carousel slides, detail banners), including platform-owned components, so their guidance is platform-level: the envelope every surface can live with. Template-composed slots (the home hero) are different, their ideal is the TEMPLATE's own design call, declared per-template in manifest.imagery and surfaced beside the upload. Editors measure the actual upload against the relevant guidance and advise; the charity always decides.",
3
3
  "entityImages": {
4
4
  "eventPromo": {
5
5
  "idealAspect": "3:2",
6
6
  "aspectRange": ["4:3", "16:9"],
7
7
  "minWidth": 1600,
8
- "note": "One photo, several surfaces: the events card, the home carousel slide, and the event page banner — each crops differently, so keep the subject centred and avoid text baked into the image."
8
+ "note": "One photo, several surfaces: the events card, the home carousel slide, and the event page banner, each crops differently, so keep the subject centred and avoid text baked into the image."
9
9
  },
10
10
  "courseImage": {
11
11
  "idealAspect": "3:2",
12
12
  "aspectRange": ["4:3", "16:9"],
13
13
  "minWidth": 1600,
14
- "note": "Renders as the course card and the course page opener — subject centred, no baked-in text."
14
+ "note": "Renders as the course card and the course page opener, subject centred, no baked-in text."
15
15
  },
16
16
  "articleCover": {
17
17
  "idealAspect": "16:9",
18
18
  "aspectRange": ["3:2", "2:1"],
19
19
  "minWidth": 1600,
20
- "note": "The article card thumbnail and the article page cover — wide editorial shots sit best."
20
+ "note": "The article card thumbnail and the article page cover, wide editorial shots sit best."
21
21
  }
22
22
  }
23
23
  }
@@ -1,237 +1,244 @@
1
1
  {
2
- "description": "The platform's functional islands — the app-block table. A template PLACES an island with {% island 'name' %}; the platform renders, hydrates, upgrades and patches it. The stylingApi lists the STABLE class names a template may target with CSS; anything not listed is internal and may change without notice.",
3
- "islands": [
4
- {
5
- "name": "donation_widget",
6
- "label": "Donation widget",
7
- "status": "available",
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
- "params": [],
10
- "stylingApi": [
11
- ".donate-card",
12
- ".donate-head",
13
- ".donate-sub",
14
- ".freq-tabs",
15
- ".freq-tab",
16
- ".amount-grid",
17
- ".amount-option",
18
- ".amount-list",
19
- ".amount-row",
20
- ".giftaid-box",
21
- ".donate-submit",
22
- ".donate-note",
23
- ".donate-success"
24
- ]
25
- },
26
- {
27
- "name": "member_menu",
28
- "label": "Port60 ID sign-in / account menu",
29
- "status": "available",
30
- "description": "The branded Port60 ID entry point: the sign-in button (popup flow) for guests, the account pill (Members area / Sign out) for signed-in members. Renders NOTHING when the tenant doesn't allow member sign-ups. The identity brand and popup behaviour are platform-owned and identical across every template — place it in your header's nav; never rebuild it.",
31
- "params": [],
32
- "stylingApi": [
33
- ".nav-p60-signin",
34
- ".nav-account",
35
- ".nav-account-toggle",
36
- ".nav-account-menu",
37
- ".p60-mark"
38
- ]
39
- },
40
- {
41
- "name": "events_carousel",
42
- "label": "Events carousel",
43
- "status": "available",
44
- "description": "The rotating showcase of published ticketed events. Renders nothing when the tenant has no matching published content.",
45
- "params": [],
46
- "stylingApi": [
47
- ".events-carousel",
48
- ".carousel-slide",
49
- ".carousel-caption",
50
- ".carousel-dots",
51
- ".carousel-dot"
52
- ]
53
- },
54
- {
55
- "name": "whats_on_strip",
56
- "label": "What's-on strip",
57
- "status": "available",
58
- "description": "The slim banner of upcoming info events. Renders nothing when the tenant has no matching published content.",
59
- "params": [],
60
- "stylingApi": [
61
- ".info-events-bar",
62
- ".info-events-inner",
63
- ".info-event-chip"
64
- ]
65
- },
66
- {
67
- "name": "latest_articles",
68
- "label": "Latest articles",
69
- "status": "available",
70
- "description": "A strip of the most recent published articles. Renders nothing when the tenant has no matching published content.",
71
- "params": [],
72
- "stylingApi": [
73
- ".article-grid",
74
- ".article-card",
75
- ".article-card-body",
76
- ".article-strip-cta"
77
- ]
78
- },
79
- {
80
- "name": "course_enrol",
81
- "label": "Course enrolment widget",
82
- "status": "available",
83
- "description": "The member-gated course enrolment journey (schedule options, household picker, payment, seat holds) for the course the page addresses. Only meaningful on the course page template (pages/course.liquid) — place it where your layout wants the purchase column (left rail, right rail, or below the content); position in your source is position on the page. Everything inside is platform-owned; it renders a standard sold-out note when no places remain (branch on course.soldOut if you want your own tag elsewhere).",
84
- "params": [],
85
- "stylingApi": [
86
- ".course-enrol",
87
- ".course-variant-list",
88
- ".course-subject-list",
89
- ".course-summary",
90
- ".course-pay-panel",
91
- ".course-soldout-note"
92
- ]
93
- },
94
- {
95
- "name": "next_prayer",
96
- "label": "Next worship time",
97
- "status": "available",
98
- "description": "A live countdown to worship.next for the tenant's schedule. Renders nothing when no next time is available. The platform owns the clock; the template owns presentation through the stable classes.",
99
- "params": [],
100
- "stylingApi": [
101
- ".next-prayer",
102
- ".next-prayer-label",
103
- ".next-prayer-name",
104
- ".next-prayer-time",
105
- ".next-prayer-countdown"
106
- ]
107
- },
108
- {
109
- "name": "article_engagement",
110
- "label": "Article engagement",
111
- "status": "available",
112
- "description": "The platform-owned article views, likes and sharing bar for the article page being rendered.",
113
- "params": [],
114
- "stylingApi": [
115
- ".article-engage",
116
- ".article-views",
117
- ".article-share-group",
118
- ".article-share-btn",
119
- ".article-share-word",
120
- ".article-share-copied"
121
- ]
122
- },
123
- {
124
- "name": "article_comments",
125
- "label": "Article comments",
126
- "status": "available",
127
- "description": "The identity-aware article conversation for the article page being rendered. Renders the closed state when comments are disabled.",
128
- "params": [],
129
- "stylingApi": [
130
- ".article-comments",
131
- ".article-comment-list",
132
- ".article-comment",
133
- ".article-comment-head",
134
- ".article-comment-form",
135
- ".article-comments-gate",
136
- ".article-comments-note"
137
- ]
138
- },
139
- {
140
- "name": "hero_carousel",
141
- "label": "Hero image carousel",
142
- "status": "available",
143
- "description": "Rotation for the home hero's photographs (homeHero.images, 2+): auto-advance every ~6s, pauses on hover/focus, honours prefers-reduced-motion (no auto-advance), swipe on touch, dot navigation. The platform owns the motion; the TEMPLATE owns the look — including the tonal treatment: every slide ships an empty .hero-slide-scrim layer over its image, and the template colours it from its OWN palette variables (a colour wash, a blend, a gradient) so any uploaded photo sits inside the template's tones. Renders nothing with fewer than 2 images — single-image and no-image states are the template's own renderer, not this island's.",
144
- "params": [],
145
- "stylingApi": [
146
- ".hero-carousel",
147
- ".hero-slides",
148
- ".hero-slide",
149
- ".hero-slide--active",
150
- ".hero-slide-img",
151
- ".hero-slide-scrim",
152
- ".hero-dots",
153
- ".hero-dot",
154
- ".hero-dot--active"
155
- ]
156
- },
157
- {
158
- "name": "newsletter_signup",
159
- "label": "Newsletter sign-up",
160
- "status": "available",
161
- "description": "Explicit email opt-in over the platform's versioned consent and one-click unsubscribe rail. Templates own placement and presentation; wording, evidence and abuse checks remain platform-owned.",
162
- "params": [],
163
- "stylingApi": [
164
- ".newsletter-signup",
165
- ".newsletter-label",
166
- ".newsletter-fields",
167
- ".newsletter-email",
168
- ".newsletter-submit",
169
- ".newsletter-consent",
170
- ".newsletter-message"
171
- ]
172
- },
173
- {
174
- "name": "language_switch",
175
- "label": "Language switch",
176
- "status": "available",
177
- "description": "Switches between the platform's supported public languages while preserving the current route. The host supplies locale and text direction to Liquid and the document element.",
178
- "params": [],
179
- "stylingApi": [
180
- ".language-switch",
181
- ".language-switch-label",
182
- ".language-switch-select"
183
- ]
184
- },
185
- {
186
- "name": "search",
187
- "label": "Site search",
188
- "status": "available",
189
- "description": "Searches published articles, programmes, events and public resources. It reads public projections only and cannot search member, payment, private media or admin data.",
190
- "params": [],
191
- "stylingApi": [
192
- ".site-search",
193
- ".site-search-form",
194
- ".site-search-label",
195
- ".site-search-fields",
196
- ".site-search-input",
197
- ".site-search-submit",
198
- ".site-search-results",
199
- ".site-search-result",
200
- ".site-search-kind",
201
- ".site-search-link",
202
- ".site-search-summary",
203
- ".site-search-empty"
204
- ]
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
- {
221
- "name": "form",
222
- "label": "Public form",
223
- "status": "available",
224
- "description": "Renders the tenant's first published platform form and records its submission with explicit privacy acceptance. Templates cannot handle or redirect submissions themselves.",
225
- "params": [],
226
- "stylingApi": [
227
- ".public-form",
228
- ".public-form-head",
229
- ".public-form-field",
230
- ".public-form-check",
231
- ".public-form-privacy",
232
- ".public-form-submit",
233
- ".public-form-message"
234
- ]
235
- }
236
- ]
2
+ "description": "The platform's functional islands, the app-block table. A template PLACES an island with {% island 'name' %}; the platform renders, hydrates, upgrades and patches it. The stylingApi lists the STABLE class names a template may target with CSS; anything not listed is internal and may change without notice.",
3
+ "islands": [
4
+ {
5
+ "name": "donation_widget",
6
+ "label": "Donation widget",
7
+ "status": "available",
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
+ "params": [],
10
+ "stylingApi": [
11
+ ".donate-card",
12
+ ".donate-head",
13
+ ".donate-sub",
14
+ ".freq-tabs",
15
+ ".freq-tab",
16
+ ".amount-grid",
17
+ ".amount-option",
18
+ ".amount-list",
19
+ ".amount-row",
20
+ ".giftaid-box",
21
+ ".donate-submit",
22
+ ".donate-note",
23
+ ".donate-success"
24
+ ]
25
+ },
26
+ {
27
+ "name": "member_menu",
28
+ "label": "Port60 ID sign-in / account menu",
29
+ "status": "available",
30
+ "description": "The branded Port60 ID entry point: the sign-in button (popup flow) for guests, the account pill (Members area / Sign out) for signed-in members. Renders NOTHING when the tenant doesn't allow member sign-ups. The identity brand and popup behaviour are platform-owned and identical across every template, place it in your header's nav; never rebuild it.",
31
+ "params": [],
32
+ "stylingApi": [
33
+ ".nav-p60-signin",
34
+ ".nav-account",
35
+ ".nav-account-toggle",
36
+ ".nav-account-menu",
37
+ ".p60-mark"
38
+ ]
39
+ },
40
+ {
41
+ "name": "events_carousel",
42
+ "label": "Events carousel",
43
+ "status": "available",
44
+ "description": "The rotating showcase of published ticketed events. Renders nothing when the tenant has no matching published content.",
45
+ "params": [],
46
+ "stylingApi": [
47
+ ".events-carousel",
48
+ ".carousel-slide",
49
+ ".carousel-caption",
50
+ ".carousel-dots",
51
+ ".carousel-dot"
52
+ ]
53
+ },
54
+ {
55
+ "name": "whats_on_strip",
56
+ "label": "What's-on strip",
57
+ "status": "available",
58
+ "description": "The slim banner of upcoming info events. Renders nothing when the tenant has no matching published content.",
59
+ "params": [],
60
+ "stylingApi": [
61
+ ".info-events-bar",
62
+ ".info-events-inner",
63
+ ".info-event-chip"
64
+ ]
65
+ },
66
+ {
67
+ "name": "latest_articles",
68
+ "label": "Latest articles",
69
+ "status": "available",
70
+ "description": "A strip of the most recent published articles. Renders nothing when the tenant has no matching published content.",
71
+ "params": [],
72
+ "stylingApi": [
73
+ ".article-grid",
74
+ ".article-card",
75
+ ".article-card-body",
76
+ ".article-strip-cta"
77
+ ]
78
+ },
79
+ {
80
+ "name": "course_enrol",
81
+ "label": "Course enrolment widget",
82
+ "status": "available",
83
+ "description": "The member-gated course enrolment journey (schedule options, household picker, payment, seat holds) for the course the page addresses. Only meaningful on the course page template (pages/course.liquid), place it where your layout wants the purchase column (left rail, right rail, or below the content); position in your source is position on the page. Everything inside is platform-owned; it renders a standard sold-out note when no places remain (branch on course.soldOut if you want your own tag elsewhere).",
84
+ "params": [],
85
+ "stylingApi": [
86
+ ".course-enrol",
87
+ ".course-variant-list",
88
+ ".course-subject-list",
89
+ ".course-summary",
90
+ ".course-pay-panel",
91
+ ".course-soldout-note"
92
+ ]
93
+ },
94
+ {
95
+ "name": "next_prayer",
96
+ "label": "Next worship time",
97
+ "status": "available",
98
+ "description": "A live countdown to worship.next for the tenant's schedule. Renders nothing when no next time is available. The platform owns the clock; the template owns presentation through the stable classes.",
99
+ "params": [],
100
+ "stylingApi": [
101
+ ".next-prayer",
102
+ ".next-prayer-label",
103
+ ".next-prayer-name",
104
+ ".next-prayer-time",
105
+ ".next-prayer-countdown"
106
+ ]
107
+ },
108
+ {
109
+ "name": "article_engagement",
110
+ "label": "Article engagement",
111
+ "status": "available",
112
+ "description": "The platform-owned article views, likes and sharing bar for the article page being rendered.",
113
+ "params": [],
114
+ "stylingApi": [
115
+ ".article-engage",
116
+ ".article-views",
117
+ ".article-share-group",
118
+ ".article-share-btn",
119
+ ".article-share-word",
120
+ ".article-share-copied"
121
+ ]
122
+ },
123
+ {
124
+ "name": "article_comments",
125
+ "label": "Article comments",
126
+ "status": "available",
127
+ "description": "The identity-aware article conversation for the article page being rendered. Renders the closed state when comments are disabled.",
128
+ "params": [],
129
+ "stylingApi": [
130
+ ".article-comments",
131
+ ".article-comment-list",
132
+ ".article-comment",
133
+ ".article-comment-head",
134
+ ".article-comment-form",
135
+ ".article-comments-gate",
136
+ ".article-comments-note"
137
+ ]
138
+ },
139
+ {
140
+ "name": "hero_carousel",
141
+ "label": "Hero image carousel",
142
+ "status": "available",
143
+ "description": "Rotation for the home hero's photographs (homeHero.images, 2+): auto-advance every ~6s, pauses on hover/focus, honours prefers-reduced-motion (no auto-advance), swipe on touch, dot navigation. The platform owns the motion; the TEMPLATE owns the look, including the tonal treatment: every slide ships an empty .hero-slide-scrim layer over its image, and the template colours it from its OWN palette variables (a colour wash, a blend, a gradient) so any uploaded photo sits inside the template's tones. Renders nothing with fewer than 2 images, single-image and no-image states are the template's own renderer, not this island's.",
144
+ "params": [],
145
+ "stylingApi": [
146
+ ".hero-carousel",
147
+ ".hero-slides",
148
+ ".hero-slide",
149
+ ".hero-slide--active",
150
+ ".hero-slide-img",
151
+ ".hero-slide-scrim",
152
+ ".hero-dots",
153
+ ".hero-dot",
154
+ ".hero-dot--active"
155
+ ]
156
+ },
157
+ {
158
+ "name": "newsletter_signup",
159
+ "label": "Newsletter sign-up",
160
+ "status": "available",
161
+ "description": "Explicit email opt-in over the platform's versioned consent and one-click unsubscribe rail. Templates own placement and presentation; wording, evidence and abuse checks remain platform-owned.",
162
+ "params": [],
163
+ "stylingApi": [
164
+ ".newsletter-signup",
165
+ ".newsletter-label",
166
+ ".newsletter-fields",
167
+ ".newsletter-email",
168
+ ".newsletter-submit",
169
+ ".newsletter-consent",
170
+ ".newsletter-message"
171
+ ]
172
+ },
173
+ {
174
+ "name": "language_switch",
175
+ "label": "Language switch",
176
+ "status": "available",
177
+ "description": "Switches between the platform's supported public languages while preserving the current route. The host supplies locale and text direction to Liquid and the document element.",
178
+ "params": [],
179
+ "stylingApi": [
180
+ ".language-switch",
181
+ ".language-switch-label",
182
+ ".language-switch-select"
183
+ ]
184
+ },
185
+ {
186
+ "name": "search",
187
+ "label": "Site search",
188
+ "status": "available",
189
+ "description": "Searches published articles, programmes, events and public resources. It reads public projections only and cannot search member, payment, private media or admin data.",
190
+ "params": [],
191
+ "stylingApi": [
192
+ ".site-search",
193
+ ".site-search-form",
194
+ ".site-search-label",
195
+ ".site-search-fields",
196
+ ".site-search-input",
197
+ ".site-search-submit",
198
+ ".site-search-results",
199
+ ".site-search-result",
200
+ ".site-search-kind",
201
+ ".site-search-link",
202
+ ".site-search-summary",
203
+ ".site-search-empty"
204
+ ]
205
+ },
206
+ {
207
+ "name": "form",
208
+ "label": "Public form",
209
+ "status": "available",
210
+ "description": "Renders the tenant's first published platform form and records its submission with explicit privacy acceptance. Templates cannot handle or redirect submissions themselves.",
211
+ "params": [],
212
+ "stylingApi": [
213
+ ".public-form",
214
+ ".public-form-head",
215
+ ".public-form-field",
216
+ ".public-form-check",
217
+ ".public-form-privacy",
218
+ ".public-form-submit",
219
+ ".public-form-message"
220
+ ]
221
+ },
222
+ {
223
+ "name": "map",
224
+ "label": "Impact map",
225
+ "status": "available",
226
+ "description": "The tenant's published impact map: a themed, privacy-clean map (platform-served tiles, never a third-party tile CDN) whose points open cards derived from the content they link. The island derives its colours from the template's tokens; v1 renders the tenant's first published map.",
227
+ "params": [],
228
+ "stylingApi": [
229
+ ".impact-map",
230
+ ".impact-map-head",
231
+ ".impact-map-intro",
232
+ ".impact-map-canvas",
233
+ ".impact-map-pin",
234
+ ".impact-map-pin--active",
235
+ ".impact-map-card",
236
+ ".impact-map-card-kind",
237
+ ".impact-map-card-title",
238
+ ".impact-map-card-summary",
239
+ ".impact-map-card-cta",
240
+ ".impact-map-empty"
241
+ ]
242
+ }
243
+ ]
237
244
  }
@@ -1,16 +1,16 @@
1
1
  {
2
- "description": "How platform pages are placed inside a template. Every platform page (donate, services, events, campaigns, the members area) renders through a fixed set of LAYOUT INTENTS — the content SEAM. A template STYLES these intents to place content its way; it must never build a parallel content container, or platform pages drift from the template's own sections. Enforced at publish by the conformance validator.",
2
+ "description": "How platform pages are placed inside a template. Every platform page (donate, services, events, campaigns, the members area) renders through a fixed set of LAYOUT INTENTS, the content SEAM. A template STYLES these intents to place content its way; it must never build a parallel content container, or platform pages drift from the template's own sections. Enforced at publish by the conformance validator.",
3
3
  "widthToken": "--container",
4
4
  "intents": [
5
5
  {
6
6
  "class": "container",
7
7
  "kind": "contained",
8
- "note": "The contained content column — text, forms, cards. Style it (and override --container) to set your content width and gutter. Use it in your own layout and sections too, so everything shares one column."
8
+ "note": "The contained content column, text, forms, cards. Style it (and override --container) to set your content width and gutter. Use it in your own layout and sections too, so everything shares one column."
9
9
  },
10
10
  {
11
11
  "class": "full",
12
12
  "kind": "full-bleed",
13
- "note": "Edge-to-edge regardless of the container it sits in — a hero image, a colour band. Restyle it freely; the default breaks out to the viewport width."
13
+ "note": "Edge-to-edge regardless of the container it sits in, a hero image, a colour band. Restyle it freely; the default breaks out to the viewport width."
14
14
  }
15
15
  ],
16
16
  "rules": [
@@ -19,7 +19,7 @@
19
19
  "kind": "css-width-off-token",
20
20
  "token": "--container",
21
21
  "allow": ["container", "full"],
22
- "message": "builds its own content container from var(--container). Platform pages render inside .container, so a parallel container drifts from your sections — style .container instead (override --container for width and add your gutter there), and use .full for edge-to-edge."
22
+ "message": "builds its own content container from var(--container). Platform pages render inside .container, so a parallel container drifts from your sections, style .container instead (override --container for width and add your gutter there), and use .full for edge-to-edge."
23
23
  }
24
24
  ]
25
25
  }