@port60/template-kit 1.1.0 → 1.2.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/README.md +27 -1
- package/package.json +1 -1
- package/src/lib/agentsMd.mjs +48 -0
- package/src/vendor/contract/v2/islands.json +1 -1
- package/src/vendor/contract/v2/manifest.schema.json +49 -0
- package/src/vendor/contract/v2/sections.json +219 -1
- package/src/vendor/contract/v2/site.schema.json +4 -0
- package/src/vendor/contract/v2.lock.json +330 -2
- package/src/vendor/engine/presentation-capabilities.mjs +45 -0
- package/src/vendor/engine/section-heading-alignment.mjs +28 -0
- package/src/vendor/validator/collection-link-visibility.mjs +47 -0
- package/src/vendor/validator/heading-alignment.mjs +148 -0
- package/src/vendor/validator/navigation-modes.mjs +60 -0
- package/src/vendor/validator/platform-base.css +96 -0
- package/src/vendor/validator/presentation-proof.mjs +53 -0
- package/src/vendor/validator/preview-v2.mjs +9 -2
- package/src/vendor/validator/site-context-v2.mjs +17 -2
- package/src/vendor/validator/validate-v2.mjs +127 -3
- package/starter/manifest.json +1 -0
|
@@ -202,6 +202,18 @@
|
|
|
202
202
|
"pages",
|
|
203
203
|
"sections"
|
|
204
204
|
],
|
|
205
|
+
"dependentSchemas": {
|
|
206
|
+
"heroImageLimit": {
|
|
207
|
+
"required": [
|
|
208
|
+
"heroImagery"
|
|
209
|
+
],
|
|
210
|
+
"properties": {
|
|
211
|
+
"heroImagery": {
|
|
212
|
+
"const": true
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
205
217
|
"properties": {
|
|
206
218
|
"pages": {
|
|
207
219
|
"type": "array",
|
|
@@ -262,6 +274,53 @@
|
|
|
262
274
|
"default": false,
|
|
263
275
|
"description": "True when the layout renders the `worship` context (the prayer/service-times rail). Declared honesty: the choosers badge worship-enabled tenants toward looks that show their times, and warn on looks that don't. Requires supports.layout; the layout must actually branch on `worship`."
|
|
264
276
|
},
|
|
277
|
+
"navigationModes": {
|
|
278
|
+
"type": "object",
|
|
279
|
+
"additionalProperties": false,
|
|
280
|
+
"required": [
|
|
281
|
+
"options",
|
|
282
|
+
"default"
|
|
283
|
+
],
|
|
284
|
+
"properties": {
|
|
285
|
+
"options": {
|
|
286
|
+
"type": "array",
|
|
287
|
+
"minItems": 2,
|
|
288
|
+
"maxItems": 2,
|
|
289
|
+
"uniqueItems": true,
|
|
290
|
+
"items": {
|
|
291
|
+
"enum": [
|
|
292
|
+
"simple",
|
|
293
|
+
"mega"
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"default": {
|
|
298
|
+
"enum": [
|
|
299
|
+
"simple",
|
|
300
|
+
"mega"
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"description": "Explicit header presentation switch for this exact layout. Both simple and mega must be implemented without changing saved navigation nodes, destinations or metadata. Requires supports.layout. The host resolves an accepted saved menus.headerMode or this default into site.nav.headerMode. Mark the navigation root data-p60-navigation-mode with that resolved mode. Missing support preserves the historical layout and leaves saved preferences unused, never inferred from navigationHighlights."
|
|
305
|
+
},
|
|
306
|
+
"sectionCollectionLinkVisibility": {
|
|
307
|
+
"type": "array",
|
|
308
|
+
"minItems": 1,
|
|
309
|
+
"maxItems": 7,
|
|
310
|
+
"uniqueItems": true,
|
|
311
|
+
"items": {
|
|
312
|
+
"enum": [
|
|
313
|
+
"events",
|
|
314
|
+
"articles",
|
|
315
|
+
"courses",
|
|
316
|
+
"campaigns",
|
|
317
|
+
"services",
|
|
318
|
+
"documents",
|
|
319
|
+
"appealGrid"
|
|
320
|
+
]
|
|
321
|
+
},
|
|
322
|
+
"description": "Supported collection section types whose existing onward link may be hidden. Every entry must also be in supports.sections and render its supplied collection destination on a permanent data-p60-collection-link anchor. Only section.showCollectionLink == false adds hidden; absent or true retains the template default link. Keep its shared label and destination unchanged, and never hide the heading, records, cards or individual record links. No new URLs, text override or positioning control."
|
|
323
|
+
},
|
|
265
324
|
"navigationHighlights": {
|
|
266
325
|
"type": "boolean",
|
|
267
326
|
"default": false,
|
|
@@ -273,11 +332,55 @@
|
|
|
273
332
|
"description": "True when the sections mark which node shows which field with data-p60-field, so the editor can put the caret on the page instead of in a side panel. The address is inside that section's own content: data-p60-field=\"title\", or data-p60-field=\"items.{{ forloop.index0 }}.label\" for an entry in a list. Purely additive: the attribute is markup, the contract is untouched, and a template that marks nothing keeps the section-level editor. The validator reads the marks from the source and warns on any that does not name a field of that section. Missing or false leaves this exact template version editing in the panel.",
|
|
274
333
|
"$comment": "Follows the navigationHighlights pattern: declared honesty, proven by the validator."
|
|
275
334
|
},
|
|
335
|
+
"sectionHeadingAlignment": {
|
|
336
|
+
"type": "object",
|
|
337
|
+
"maxProperties": 16,
|
|
338
|
+
"propertyNames": {
|
|
339
|
+
"enum": [
|
|
340
|
+
"hero",
|
|
341
|
+
"values",
|
|
342
|
+
"cta",
|
|
343
|
+
"homeHero",
|
|
344
|
+
"impact",
|
|
345
|
+
"events",
|
|
346
|
+
"articles",
|
|
347
|
+
"campaigns",
|
|
348
|
+
"emergency",
|
|
349
|
+
"stats",
|
|
350
|
+
"appealGrid",
|
|
351
|
+
"services",
|
|
352
|
+
"documents",
|
|
353
|
+
"people",
|
|
354
|
+
"impactMap",
|
|
355
|
+
"courses"
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
"additionalProperties": {
|
|
359
|
+
"type": "array",
|
|
360
|
+
"minItems": 1,
|
|
361
|
+
"maxItems": 3,
|
|
362
|
+
"uniqueItems": true,
|
|
363
|
+
"items": {
|
|
364
|
+
"enum": [
|
|
365
|
+
"start",
|
|
366
|
+
"center",
|
|
367
|
+
"end"
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
"description": "Explicit opt-in per supported section type to desktop heading alignment choices. Missing entries disable the editor control. The host projects section.headingAlignment only for declared options. Mark heading/eyebrow elements using data-p60-heading-align with that value. The shared base applies text-align at widths of at least 1024 CSS pixels; absent/empty hooks retain the template default at every width. The validator proves each declared option renders on real headings and cannot change unrelated content. No layout positioning is implied."
|
|
372
|
+
},
|
|
276
373
|
"heroImagery": {
|
|
277
374
|
"type": "boolean",
|
|
278
375
|
"default": false,
|
|
279
376
|
"description": "True when the homeHero renderer displays the tenant's hero photographs (homeHero.images, legacy imageUrl), always under a TREATMENT built from the template's own palette variables (scrim/tint/blend), never raw. Declared honesty, proven behaviourally at validation: the choosers badge photo-led tenants toward templates that show their images and warn on ones that won't. Requires the homeHero section among supports.sections."
|
|
280
377
|
},
|
|
378
|
+
"heroImageLimit": {
|
|
379
|
+
"type": "integer",
|
|
380
|
+
"minimum": 1,
|
|
381
|
+
"maximum": 6,
|
|
382
|
+
"description": "Maximum homeHero photographs this exact template version can present. Requires supports.heroImagery: true. Missing means one; no hero imagery means zero. Values above one require rendering every image in a fixture of the declared size or placing the hero_carousel island. Generic carousel behavior or island declarations alone do not opt in. This gates editor additions, never truncates saved photographs or changes an older artifact's rendering."
|
|
383
|
+
},
|
|
281
384
|
"behaviors": {
|
|
282
385
|
"type": "array",
|
|
283
386
|
"items": {
|
|
@@ -551,6 +654,13 @@
|
|
|
551
654
|
"footer"
|
|
552
655
|
],
|
|
553
656
|
"properties": {
|
|
657
|
+
"headerMode": {
|
|
658
|
+
"enum": [
|
|
659
|
+
"simple",
|
|
660
|
+
"mega"
|
|
661
|
+
],
|
|
662
|
+
"description": "Resolved header presentation only when the exact template declares navigationModes. The host uses the accepted saved preference or the declared default. Omitted for unsupported templates; never inferred from menu shape or highlights."
|
|
663
|
+
},
|
|
554
664
|
"header": {
|
|
555
665
|
"type": "array",
|
|
556
666
|
"items": {
|
|
@@ -2627,6 +2737,17 @@
|
|
|
2627
2737
|
],
|
|
2628
2738
|
"description": "The page opener: a headline with an optional kicker and sanitised rich-text intro.",
|
|
2629
2739
|
"fields": [
|
|
2740
|
+
{
|
|
2741
|
+
"name": "headingAlignment",
|
|
2742
|
+
"kind": "select",
|
|
2743
|
+
"required": false,
|
|
2744
|
+
"options": [
|
|
2745
|
+
"start",
|
|
2746
|
+
"center",
|
|
2747
|
+
"end"
|
|
2748
|
+
],
|
|
2749
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
2750
|
+
},
|
|
2630
2751
|
{
|
|
2631
2752
|
"name": "eyebrow",
|
|
2632
2753
|
"kind": "text",
|
|
@@ -2663,6 +2784,17 @@
|
|
|
2663
2784
|
],
|
|
2664
2785
|
"description": "A set of principles shown as cards or a numbered list, per the template.",
|
|
2665
2786
|
"fields": [
|
|
2787
|
+
{
|
|
2788
|
+
"name": "headingAlignment",
|
|
2789
|
+
"kind": "select",
|
|
2790
|
+
"required": false,
|
|
2791
|
+
"options": [
|
|
2792
|
+
"start",
|
|
2793
|
+
"center",
|
|
2794
|
+
"end"
|
|
2795
|
+
],
|
|
2796
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
2797
|
+
},
|
|
2666
2798
|
{
|
|
2667
2799
|
"name": "eyebrow",
|
|
2668
2800
|
"kind": "text",
|
|
@@ -2738,6 +2870,17 @@
|
|
|
2738
2870
|
],
|
|
2739
2871
|
"description": "A closing band: heading, optional line, one button.",
|
|
2740
2872
|
"fields": [
|
|
2873
|
+
{
|
|
2874
|
+
"name": "headingAlignment",
|
|
2875
|
+
"kind": "select",
|
|
2876
|
+
"required": false,
|
|
2877
|
+
"options": [
|
|
2878
|
+
"start",
|
|
2879
|
+
"center",
|
|
2880
|
+
"end"
|
|
2881
|
+
],
|
|
2882
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
2883
|
+
},
|
|
2741
2884
|
{
|
|
2742
2885
|
"name": "heading",
|
|
2743
2886
|
"kind": "text",
|
|
@@ -2783,6 +2926,17 @@
|
|
|
2783
2926
|
],
|
|
2784
2927
|
"description": "The home opener. EVERY field optional: eyebrow/title derive from branding when absent, the lead OMITS when absent (never a fabricated placeholder), buttons fall back to platform defaults.",
|
|
2785
2928
|
"fields": [
|
|
2929
|
+
{
|
|
2930
|
+
"name": "headingAlignment",
|
|
2931
|
+
"kind": "select",
|
|
2932
|
+
"required": false,
|
|
2933
|
+
"options": [
|
|
2934
|
+
"start",
|
|
2935
|
+
"center",
|
|
2936
|
+
"end"
|
|
2937
|
+
],
|
|
2938
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
2939
|
+
},
|
|
2786
2940
|
{
|
|
2787
2941
|
"name": "eyebrow",
|
|
2788
2942
|
"kind": "text",
|
|
@@ -2845,7 +2999,7 @@
|
|
|
2845
2999
|
"name": "images",
|
|
2846
3000
|
"kind": "items",
|
|
2847
3001
|
"required": false,
|
|
2848
|
-
"description": "Hero photographs (0-6, uploaded through the platform pipeline). One = a main image; several = rotation material
|
|
3002
|
+
"description": "Hero photographs (0-6, uploaded through the platform pipeline). One = a main image; several = rotation material. A template with supports.heroImagery renders at least the first and may place the hero_carousel island (or a CSS scroll strip) for the rest. Declare supports.heroImageLimit above one to offer multiple photographs in the editor; missing means one, and absent heroImagery means zero. Saved photographs survive template switches and are never truncated by this capability. Like every field: absent means the template's designed no-photo state, never a placeholder.",
|
|
2849
3003
|
"itemFields": [
|
|
2850
3004
|
{
|
|
2851
3005
|
"name": "imageUrl",
|
|
@@ -2913,6 +3067,17 @@
|
|
|
2913
3067
|
],
|
|
2914
3068
|
"description": "What gifts make possible, optional intro plus icon/title/description cards. Claims must be truthful for the tenant.",
|
|
2915
3069
|
"fields": [
|
|
3070
|
+
{
|
|
3071
|
+
"name": "headingAlignment",
|
|
3072
|
+
"kind": "select",
|
|
3073
|
+
"required": false,
|
|
3074
|
+
"options": [
|
|
3075
|
+
"start",
|
|
3076
|
+
"center",
|
|
3077
|
+
"end"
|
|
3078
|
+
],
|
|
3079
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3080
|
+
},
|
|
2916
3081
|
{
|
|
2917
3082
|
"name": "eyebrow",
|
|
2918
3083
|
"kind": "text",
|
|
@@ -3002,6 +3167,23 @@
|
|
|
3002
3167
|
"kind": "widget",
|
|
3003
3168
|
"description": "Published events across registration modes. Render the supplied bounded site.content.events.items with this placement's authored introduction. Optional motion uses declared behaviours; ticket sales and RSVP stay on the platform-owned detail route supplied by each item.",
|
|
3004
3169
|
"fields": [
|
|
3170
|
+
{
|
|
3171
|
+
"name": "showCollectionLink",
|
|
3172
|
+
"kind": "boolean",
|
|
3173
|
+
"required": false,
|
|
3174
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3175
|
+
},
|
|
3176
|
+
{
|
|
3177
|
+
"name": "headingAlignment",
|
|
3178
|
+
"kind": "select",
|
|
3179
|
+
"required": false,
|
|
3180
|
+
"options": [
|
|
3181
|
+
"start",
|
|
3182
|
+
"center",
|
|
3183
|
+
"end"
|
|
3184
|
+
],
|
|
3185
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3186
|
+
},
|
|
3005
3187
|
{
|
|
3006
3188
|
"name": "eyebrow",
|
|
3007
3189
|
"kind": "text",
|
|
@@ -3040,6 +3222,23 @@
|
|
|
3040
3222
|
"kind": "widget",
|
|
3041
3223
|
"description": "Published articles from the supplied bounded site.content.articles.items. Render the records with this placement's authored introduction and link through the supplied item URLs. Article engagement and comments remain platform islands on the detail page.",
|
|
3042
3224
|
"fields": [
|
|
3225
|
+
{
|
|
3226
|
+
"name": "showCollectionLink",
|
|
3227
|
+
"kind": "boolean",
|
|
3228
|
+
"required": false,
|
|
3229
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
"name": "headingAlignment",
|
|
3233
|
+
"kind": "select",
|
|
3234
|
+
"required": false,
|
|
3235
|
+
"options": [
|
|
3236
|
+
"start",
|
|
3237
|
+
"center",
|
|
3238
|
+
"end"
|
|
3239
|
+
],
|
|
3240
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3241
|
+
},
|
|
3043
3242
|
{
|
|
3044
3243
|
"name": "eyebrow",
|
|
3045
3244
|
"kind": "text",
|
|
@@ -3081,6 +3280,23 @@
|
|
|
3081
3280
|
"campaignsLabel"
|
|
3082
3281
|
],
|
|
3083
3282
|
"fields": [
|
|
3283
|
+
{
|
|
3284
|
+
"name": "showCollectionLink",
|
|
3285
|
+
"kind": "boolean",
|
|
3286
|
+
"required": false,
|
|
3287
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
"name": "headingAlignment",
|
|
3291
|
+
"kind": "select",
|
|
3292
|
+
"required": false,
|
|
3293
|
+
"options": [
|
|
3294
|
+
"start",
|
|
3295
|
+
"center",
|
|
3296
|
+
"end"
|
|
3297
|
+
],
|
|
3298
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3299
|
+
},
|
|
3084
3300
|
{
|
|
3085
3301
|
"name": "eyebrow",
|
|
3086
3302
|
"kind": "text",
|
|
@@ -3118,6 +3334,17 @@
|
|
|
3118
3334
|
"causes"
|
|
3119
3335
|
],
|
|
3120
3336
|
"fields": [
|
|
3337
|
+
{
|
|
3338
|
+
"name": "headingAlignment",
|
|
3339
|
+
"kind": "select",
|
|
3340
|
+
"required": false,
|
|
3341
|
+
"options": [
|
|
3342
|
+
"start",
|
|
3343
|
+
"center",
|
|
3344
|
+
"end"
|
|
3345
|
+
],
|
|
3346
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3347
|
+
},
|
|
3121
3348
|
{
|
|
3122
3349
|
"name": "heading",
|
|
3123
3350
|
"kind": "text",
|
|
@@ -3161,6 +3388,17 @@
|
|
|
3161
3388
|
],
|
|
3162
3389
|
"description": "Big headline numbers, the at-a-glance impact band. Values are author-entered text (write them exactly as they should read), or, per item, a live figure the platform counts (`source`).",
|
|
3163
3390
|
"fields": [
|
|
3391
|
+
{
|
|
3392
|
+
"name": "headingAlignment",
|
|
3393
|
+
"kind": "select",
|
|
3394
|
+
"required": false,
|
|
3395
|
+
"options": [
|
|
3396
|
+
"start",
|
|
3397
|
+
"center",
|
|
3398
|
+
"end"
|
|
3399
|
+
],
|
|
3400
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3401
|
+
},
|
|
3164
3402
|
{
|
|
3165
3403
|
"name": "eyebrow",
|
|
3166
3404
|
"kind": "text",
|
|
@@ -3244,6 +3482,23 @@
|
|
|
3244
3482
|
"causes"
|
|
3245
3483
|
],
|
|
3246
3484
|
"fields": [
|
|
3485
|
+
{
|
|
3486
|
+
"name": "showCollectionLink",
|
|
3487
|
+
"kind": "boolean",
|
|
3488
|
+
"required": false,
|
|
3489
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3490
|
+
},
|
|
3491
|
+
{
|
|
3492
|
+
"name": "headingAlignment",
|
|
3493
|
+
"kind": "select",
|
|
3494
|
+
"required": false,
|
|
3495
|
+
"options": [
|
|
3496
|
+
"start",
|
|
3497
|
+
"center",
|
|
3498
|
+
"end"
|
|
3499
|
+
],
|
|
3500
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3501
|
+
},
|
|
3247
3502
|
{
|
|
3248
3503
|
"name": "eyebrow",
|
|
3249
3504
|
"kind": "text",
|
|
@@ -3333,6 +3588,23 @@
|
|
|
3333
3588
|
"services"
|
|
3334
3589
|
],
|
|
3335
3590
|
"fields": [
|
|
3591
|
+
{
|
|
3592
|
+
"name": "showCollectionLink",
|
|
3593
|
+
"kind": "boolean",
|
|
3594
|
+
"required": false,
|
|
3595
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3596
|
+
},
|
|
3597
|
+
{
|
|
3598
|
+
"name": "headingAlignment",
|
|
3599
|
+
"kind": "select",
|
|
3600
|
+
"required": false,
|
|
3601
|
+
"options": [
|
|
3602
|
+
"start",
|
|
3603
|
+
"center",
|
|
3604
|
+
"end"
|
|
3605
|
+
],
|
|
3606
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3607
|
+
},
|
|
3336
3608
|
{
|
|
3337
3609
|
"name": "eyebrow",
|
|
3338
3610
|
"kind": "text",
|
|
@@ -3370,6 +3642,23 @@
|
|
|
3370
3642
|
"resources"
|
|
3371
3643
|
],
|
|
3372
3644
|
"fields": [
|
|
3645
|
+
{
|
|
3646
|
+
"name": "showCollectionLink",
|
|
3647
|
+
"kind": "boolean",
|
|
3648
|
+
"required": false,
|
|
3649
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3650
|
+
},
|
|
3651
|
+
{
|
|
3652
|
+
"name": "headingAlignment",
|
|
3653
|
+
"kind": "select",
|
|
3654
|
+
"required": false,
|
|
3655
|
+
"options": [
|
|
3656
|
+
"start",
|
|
3657
|
+
"center",
|
|
3658
|
+
"end"
|
|
3659
|
+
],
|
|
3660
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3661
|
+
},
|
|
3373
3662
|
{
|
|
3374
3663
|
"name": "eyebrow",
|
|
3375
3664
|
"kind": "text",
|
|
@@ -3404,6 +3693,17 @@
|
|
|
3404
3693
|
],
|
|
3405
3694
|
"description": "The key people behind the charity, trustees, staff, volunteers, as simple cards: name, position, an optional photograph (uploaded through the platform pipeline) and an optional one-line statement. Templates render a card grid that collapses to a single column on small screens; a card without a photo renders cleanly (initials or plain), never a placeholder face.",
|
|
3406
3695
|
"fields": [
|
|
3696
|
+
{
|
|
3697
|
+
"name": "headingAlignment",
|
|
3698
|
+
"kind": "select",
|
|
3699
|
+
"required": false,
|
|
3700
|
+
"options": [
|
|
3701
|
+
"start",
|
|
3702
|
+
"center",
|
|
3703
|
+
"end"
|
|
3704
|
+
],
|
|
3705
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3706
|
+
},
|
|
3407
3707
|
{
|
|
3408
3708
|
"name": "eyebrow",
|
|
3409
3709
|
"kind": "text",
|
|
@@ -3496,6 +3796,17 @@
|
|
|
3496
3796
|
"description": "The organisation's impact map: a themed map whose points link existing content (campaigns, causes, articles, events, places), each opening a card derived from the linked thing. Place the `map` island, it brings the map, the points and the cards; your template brings placement and tokens (the island theming contract).",
|
|
3497
3797
|
"context": [],
|
|
3498
3798
|
"fields": [
|
|
3799
|
+
{
|
|
3800
|
+
"name": "headingAlignment",
|
|
3801
|
+
"kind": "select",
|
|
3802
|
+
"required": false,
|
|
3803
|
+
"options": [
|
|
3804
|
+
"start",
|
|
3805
|
+
"center",
|
|
3806
|
+
"end"
|
|
3807
|
+
],
|
|
3808
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3809
|
+
},
|
|
3499
3810
|
{
|
|
3500
3811
|
"name": "eyebrow",
|
|
3501
3812
|
"kind": "text",
|
|
@@ -3535,6 +3846,23 @@
|
|
|
3535
3846
|
"home"
|
|
3536
3847
|
],
|
|
3537
3848
|
"fields": [
|
|
3849
|
+
{
|
|
3850
|
+
"name": "showCollectionLink",
|
|
3851
|
+
"kind": "boolean",
|
|
3852
|
+
"required": false,
|
|
3853
|
+
"description": "Only false hides this placement's existing onward collection link. Missing or true keeps the template default. Requires this exact template to declare supports.sectionCollectionLinkVisibility for this section. Keep a stable data-p60-collection-link anchor and add hidden only for false. The platform-supplied URL and shared label stay unchanged; headings, records and individual card links are unaffected."
|
|
3854
|
+
},
|
|
3855
|
+
{
|
|
3856
|
+
"name": "headingAlignment",
|
|
3857
|
+
"kind": "select",
|
|
3858
|
+
"required": false,
|
|
3859
|
+
"options": [
|
|
3860
|
+
"start",
|
|
3861
|
+
"center",
|
|
3862
|
+
"end"
|
|
3863
|
+
],
|
|
3864
|
+
"description": "Desktop alignment of the heading and optional eyebrow. The template may position their heading-only group within its existing content container and gutters while preserving readable width. Only applied when this exact template declares supports.sectionHeadingAlignment for this section and option. Absent means template default. Start/end follow the site direction. Below 1024 CSS pixels the template retains its own layout. Introductions, body copy, buttons, cards, images and islands keep their placement and alignment; full-bleed imagery does not widen the heading bounds."
|
|
3865
|
+
},
|
|
3538
3866
|
{
|
|
3539
3867
|
"name": "eyebrow",
|
|
3540
3868
|
"kind": "text",
|
|
@@ -5274,7 +5602,7 @@
|
|
|
5274
5602
|
"name": "language_switch",
|
|
5275
5603
|
"label": "Language switch",
|
|
5276
5604
|
"status": "available",
|
|
5277
|
-
"description": "
|
|
5605
|
+
"description": "Temporarily inactive: currently renders nothing. Localisation supports a site's language and text direction; translated tenant content and an explicit per-site language choice need a separate opt-in design. The host supplies locale and direction independently. Do not add a replacement visitor selector.",
|
|
5278
5606
|
"params": [],
|
|
5279
5607
|
"stylingApi": [
|
|
5280
5608
|
".language-switch",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Exact-artifact opt-ins. Preferences survive template switches in the stored document;
|
|
2
|
+
// only supported values reach the render context or the editor's controls.
|
|
3
|
+
export const NAVIGATION_MODES = Object.freeze(['simple', 'mega']);
|
|
4
|
+
export const COLLECTION_LINK_SECTIONS = Object.freeze([
|
|
5
|
+
'events', 'articles', 'courses', 'campaigns', 'services', 'documents', 'appealGrid'
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
/** Editor addition limit, not a mutation or truncation of previously saved photographs. */
|
|
9
|
+
export function heroImageLimitCapability(manifest) {
|
|
10
|
+
if (manifest?.supports?.heroImagery !== true) return 0;
|
|
11
|
+
const limit = manifest.supports.heroImageLimit;
|
|
12
|
+
return manifest.format === 'port60-liquid@2' && Number.isInteger(limit) && limit >= 1 && limit <= 6 ? limit : 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function navigationModeCapabilities(manifest) {
|
|
16
|
+
if (manifest?.format !== 'port60-liquid@2' || manifest?.supports?.layout !== true) return null;
|
|
17
|
+
const declared = manifest.supports.navigationModes;
|
|
18
|
+
if (!declared || typeof declared !== 'object' || Array.isArray(declared)) return null;
|
|
19
|
+
const { options, default: fallback } = declared;
|
|
20
|
+
if (!Array.isArray(options) || options.length !== 2 || new Set(options).size !== 2
|
|
21
|
+
|| options.some(option => !NAVIGATION_MODES.includes(option)) || !options.includes(fallback)) return null;
|
|
22
|
+
return { options: [...options], default: fallback };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvedNavigationMode(manifest, preference) {
|
|
26
|
+
const capability = navigationModeCapabilities(manifest);
|
|
27
|
+
return capability ? (capability.options.includes(preference) ? preference : capability.default) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function collectionLinkVisibilityCapabilities(manifest) {
|
|
31
|
+
if (manifest?.format !== 'port60-liquid@2') return [];
|
|
32
|
+
const sections = manifest?.supports?.sections;
|
|
33
|
+
const declared = manifest?.supports?.sectionCollectionLinkVisibility;
|
|
34
|
+
if (!Array.isArray(sections) || !Array.isArray(declared) || !declared.length
|
|
35
|
+
|| new Set(declared).size !== declared.length
|
|
36
|
+
|| declared.some(type => !COLLECTION_LINK_SECTIONS.includes(type) || !sections.includes(type))) return [];
|
|
37
|
+
return [...declared];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function withCollectionLinkVisibility(content, type, manifest) {
|
|
41
|
+
const projected = { ...content };
|
|
42
|
+
if (!collectionLinkVisibilityCapabilities(manifest).includes(type)
|
|
43
|
+
|| typeof projected.showCollectionLink !== 'boolean') delete projected.showCollectionLink;
|
|
44
|
+
return projected;
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Presentation choices are opt-in on the exact immutable artifact. Never mutate saved content:
|
|
2
|
+
// switching to an unsupported template leaves the original choice available for a later switch.
|
|
3
|
+
export const HEADING_ALIGNMENTS = Object.freeze(['start', 'center', 'end']);
|
|
4
|
+
|
|
5
|
+
export function headingAlignmentCapabilities(manifest) {
|
|
6
|
+
if (manifest?.format !== 'port60-liquid@2') return {};
|
|
7
|
+
const declared = manifest?.supports?.sectionHeadingAlignment;
|
|
8
|
+
if (!declared || typeof declared !== 'object' || Array.isArray(declared)) return {};
|
|
9
|
+
const sections = manifest?.supports?.sections;
|
|
10
|
+
if (!Array.isArray(sections)) return {};
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [type, options] of Object.entries(declared)) {
|
|
13
|
+
if (!sections.includes(type) || !/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(type)) continue;
|
|
14
|
+
if (!Array.isArray(options) || !options.length || options.length > 3
|
|
15
|
+
|| options.some(option => !HEADING_ALIGNMENTS.includes(option))
|
|
16
|
+
|| new Set(options).size !== options.length) continue;
|
|
17
|
+
Object.defineProperty(result, type, { value: [...options], enumerable: true });
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function withHeadingAlignment(content, type, manifest) {
|
|
23
|
+
const projected = { ...content };
|
|
24
|
+
const capabilities = headingAlignmentCapabilities(manifest);
|
|
25
|
+
const options = Object.hasOwn(capabilities, type) ? capabilities[type] : undefined;
|
|
26
|
+
if (!options?.includes(projected.headingAlignment)) delete projected.headingAlignment;
|
|
27
|
+
return projected;
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parsePresentationMarkup, presentationText, presentationVisible, presentationSignature } from './presentation-proof.mjs';
|
|
2
|
+
|
|
3
|
+
export const COLLECTION_LINK_TYPES = ['events', 'articles', 'courses', 'campaigns', 'services', 'documents', 'appealGrid'];
|
|
4
|
+
const ATTRIBUTE = 'data-p60-collection-link';
|
|
5
|
+
|
|
6
|
+
/** render receives raw saved values and a destination override; the caller applies host projection. */
|
|
7
|
+
export async function proveCollectionLinkVisibility(render, entry, declared) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const href = `/p60-collection-link-${entry.type}`;
|
|
10
|
+
const content = { ...structuredClone(entry.sample ?? {}) };
|
|
11
|
+
// Authored appeal cards intentionally replace the connected causes collection.
|
|
12
|
+
if (entry.type === 'appealGrid') content.items = [];
|
|
13
|
+
delete content.showCollectionLink;
|
|
14
|
+
let baseline;
|
|
15
|
+
for (const requested of [undefined, true, false, 'false', null, 0]) {
|
|
16
|
+
const label = requested === undefined ? 'default' : JSON.stringify(requested);
|
|
17
|
+
try {
|
|
18
|
+
const html = await render({ ...content, ...(requested === undefined ? {} : { showCollectionLink: requested }) }, href);
|
|
19
|
+
const { nodes, root } = parsePresentationMarkup(html);
|
|
20
|
+
const hooks = nodes.filter(node => Object.hasOwn(node.attrs, ATTRIBUTE));
|
|
21
|
+
const expectedHidden = declared && requested === false;
|
|
22
|
+
if (declared && !hooks.length) errors.push(`${label}: must retain a permanent data-p60-collection-link anchor`);
|
|
23
|
+
for (const hook of hooks) {
|
|
24
|
+
if (hook.tag !== 'a' || hook.attrs.href !== href || !presentationText(hook)) {
|
|
25
|
+
errors.push(`${label}: collection link hook must mark a nonempty onward anchor using the supplied collection href, not a section, card or record link`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (('hidden' in hook.attrs) !== expectedHidden) errors.push(`${label}: only boolean false may add hidden to the onward anchor`);
|
|
29
|
+
const attrs = hook.attrs;
|
|
30
|
+
hook.attrs = { ...attrs };
|
|
31
|
+
delete hook.attrs.hidden;
|
|
32
|
+
if (!presentationVisible(hook)) errors.push(`${label}: the onward anchor must otherwise be visible, not hidden by its ancestor or another attribute`);
|
|
33
|
+
hook.attrs = attrs;
|
|
34
|
+
}
|
|
35
|
+
const signature = presentationSignature(root, node => Object.hasOwn(node.attrs, ATTRIBUTE) ? ['hidden'] : []);
|
|
36
|
+
if (baseline === undefined) baseline = signature;
|
|
37
|
+
else if (signature !== baseline) errors.push(`${label}: visibility may change only the onward anchor hidden attribute, never copy, URLs, headings, records or other markup`);
|
|
38
|
+
} catch (error) { errors.push(`${label}: failed rendering visibility fixture, ${error.message}`); }
|
|
39
|
+
}
|
|
40
|
+
if (declared) {
|
|
41
|
+
try {
|
|
42
|
+
const { nodes } = parsePresentationMarkup(await render(content, null));
|
|
43
|
+
if (nodes.some(node => Object.hasOwn(node.attrs, ATTRIBUTE))) errors.push('missing destination: omit the onward anchor rather than inventing a URL');
|
|
44
|
+
} catch (error) { errors.push(`missing destination: failed rendering fixture, ${error.message}`); }
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(errors)].map(message => `section '${entry.type}' showCollectionLink: ${message}`);
|
|
47
|
+
}
|