@port60/template-kit 1.0.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 +43 -1
- package/package.json +1 -1
- package/src/commands/create.mjs +1 -1
- package/src/lib/agentsMd.mjs +58 -1
- package/src/vendor/contract/v2/content-model.json +1 -1
- package/src/vendor/contract/v2/dialect.json +3 -1
- 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 +334 -4
- package/src/vendor/engine/dialect.mjs +4 -0
- package/src/vendor/engine/locale.mjs +74 -0
- 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 +106 -11
- 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
|
@@ -92,7 +92,9 @@
|
|
|
92
92
|
"upcase",
|
|
93
93
|
"url_decode",
|
|
94
94
|
"url_encode",
|
|
95
|
-
"where"
|
|
95
|
+
"where",
|
|
96
|
+
"t",
|
|
97
|
+
"local_date"
|
|
96
98
|
],
|
|
97
99
|
"excluded": {
|
|
98
100
|
"tags": [
|
|
@@ -200,6 +202,18 @@
|
|
|
200
202
|
"pages",
|
|
201
203
|
"sections"
|
|
202
204
|
],
|
|
205
|
+
"dependentSchemas": {
|
|
206
|
+
"heroImageLimit": {
|
|
207
|
+
"required": [
|
|
208
|
+
"heroImagery"
|
|
209
|
+
],
|
|
210
|
+
"properties": {
|
|
211
|
+
"heroImagery": {
|
|
212
|
+
"const": true
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
203
217
|
"properties": {
|
|
204
218
|
"pages": {
|
|
205
219
|
"type": "array",
|
|
@@ -260,6 +274,53 @@
|
|
|
260
274
|
"default": false,
|
|
261
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`."
|
|
262
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
|
+
},
|
|
263
324
|
"navigationHighlights": {
|
|
264
325
|
"type": "boolean",
|
|
265
326
|
"default": false,
|
|
@@ -271,11 +332,55 @@
|
|
|
271
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.",
|
|
272
333
|
"$comment": "Follows the navigationHighlights pattern: declared honesty, proven by the validator."
|
|
273
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
|
+
},
|
|
274
373
|
"heroImagery": {
|
|
275
374
|
"type": "boolean",
|
|
276
375
|
"default": false,
|
|
277
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."
|
|
278
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
|
+
},
|
|
279
384
|
"behaviors": {
|
|
280
385
|
"type": "array",
|
|
281
386
|
"items": {
|
|
@@ -549,6 +654,13 @@
|
|
|
549
654
|
"footer"
|
|
550
655
|
],
|
|
551
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
|
+
},
|
|
552
664
|
"header": {
|
|
553
665
|
"type": "array",
|
|
554
666
|
"items": {
|
|
@@ -2596,7 +2708,7 @@
|
|
|
2596
2708
|
"since": "2.0",
|
|
2597
2709
|
"cap": 4,
|
|
2598
2710
|
"moreHref": null,
|
|
2599
|
-
"description": "TYPE-DERIVED recurring schedules. `type` is an OPEN enum; 'worship' is the first (
|
|
2711
|
+
"description": "TYPE-DERIVED recurring schedules. `type` is an OPEN enum; 'worship' is the first (times, labels, today, next, jumah). Mosque daily times and next may carry a stable `key`: fajr, zuhr, asr, maghrib or isha. Use key for icons, never replace the supplied name. Missing/unknown keys remain readable without an icon. Friday rows are `jumah`, not `jumuah`. Other institutions arrive as new types; render the types you know, ignore the rest.",
|
|
2600
2712
|
"item": {
|
|
2601
2713
|
"type": {
|
|
2602
2714
|
"type": "string",
|
|
@@ -2625,6 +2737,17 @@
|
|
|
2625
2737
|
],
|
|
2626
2738
|
"description": "The page opener: a headline with an optional kicker and sanitised rich-text intro.",
|
|
2627
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
|
+
},
|
|
2628
2751
|
{
|
|
2629
2752
|
"name": "eyebrow",
|
|
2630
2753
|
"kind": "text",
|
|
@@ -2661,6 +2784,17 @@
|
|
|
2661
2784
|
],
|
|
2662
2785
|
"description": "A set of principles shown as cards or a numbered list, per the template.",
|
|
2663
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
|
+
},
|
|
2664
2798
|
{
|
|
2665
2799
|
"name": "eyebrow",
|
|
2666
2800
|
"kind": "text",
|
|
@@ -2736,6 +2870,17 @@
|
|
|
2736
2870
|
],
|
|
2737
2871
|
"description": "A closing band: heading, optional line, one button.",
|
|
2738
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
|
+
},
|
|
2739
2884
|
{
|
|
2740
2885
|
"name": "heading",
|
|
2741
2886
|
"kind": "text",
|
|
@@ -2781,6 +2926,17 @@
|
|
|
2781
2926
|
],
|
|
2782
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.",
|
|
2783
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
|
+
},
|
|
2784
2940
|
{
|
|
2785
2941
|
"name": "eyebrow",
|
|
2786
2942
|
"kind": "text",
|
|
@@ -2843,7 +2999,7 @@
|
|
|
2843
2999
|
"name": "images",
|
|
2844
3000
|
"kind": "items",
|
|
2845
3001
|
"required": false,
|
|
2846
|
-
"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.",
|
|
2847
3003
|
"itemFields": [
|
|
2848
3004
|
{
|
|
2849
3005
|
"name": "imageUrl",
|
|
@@ -2911,6 +3067,17 @@
|
|
|
2911
3067
|
],
|
|
2912
3068
|
"description": "What gifts make possible, optional intro plus icon/title/description cards. Claims must be truthful for the tenant.",
|
|
2913
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
|
+
},
|
|
2914
3081
|
{
|
|
2915
3082
|
"name": "eyebrow",
|
|
2916
3083
|
"kind": "text",
|
|
@@ -3000,6 +3167,23 @@
|
|
|
3000
3167
|
"kind": "widget",
|
|
3001
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.",
|
|
3002
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
|
+
},
|
|
3003
3187
|
{
|
|
3004
3188
|
"name": "eyebrow",
|
|
3005
3189
|
"kind": "text",
|
|
@@ -3038,6 +3222,23 @@
|
|
|
3038
3222
|
"kind": "widget",
|
|
3039
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.",
|
|
3040
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
|
+
},
|
|
3041
3242
|
{
|
|
3042
3243
|
"name": "eyebrow",
|
|
3043
3244
|
"kind": "text",
|
|
@@ -3079,6 +3280,23 @@
|
|
|
3079
3280
|
"campaignsLabel"
|
|
3080
3281
|
],
|
|
3081
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
|
+
},
|
|
3082
3300
|
{
|
|
3083
3301
|
"name": "eyebrow",
|
|
3084
3302
|
"kind": "text",
|
|
@@ -3116,6 +3334,17 @@
|
|
|
3116
3334
|
"causes"
|
|
3117
3335
|
],
|
|
3118
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
|
+
},
|
|
3119
3348
|
{
|
|
3120
3349
|
"name": "heading",
|
|
3121
3350
|
"kind": "text",
|
|
@@ -3159,6 +3388,17 @@
|
|
|
3159
3388
|
],
|
|
3160
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`).",
|
|
3161
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
|
+
},
|
|
3162
3402
|
{
|
|
3163
3403
|
"name": "eyebrow",
|
|
3164
3404
|
"kind": "text",
|
|
@@ -3242,6 +3482,23 @@
|
|
|
3242
3482
|
"causes"
|
|
3243
3483
|
],
|
|
3244
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
|
+
},
|
|
3245
3502
|
{
|
|
3246
3503
|
"name": "eyebrow",
|
|
3247
3504
|
"kind": "text",
|
|
@@ -3331,6 +3588,23 @@
|
|
|
3331
3588
|
"services"
|
|
3332
3589
|
],
|
|
3333
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
|
+
},
|
|
3334
3608
|
{
|
|
3335
3609
|
"name": "eyebrow",
|
|
3336
3610
|
"kind": "text",
|
|
@@ -3368,6 +3642,23 @@
|
|
|
3368
3642
|
"resources"
|
|
3369
3643
|
],
|
|
3370
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
|
+
},
|
|
3371
3662
|
{
|
|
3372
3663
|
"name": "eyebrow",
|
|
3373
3664
|
"kind": "text",
|
|
@@ -3402,6 +3693,17 @@
|
|
|
3402
3693
|
],
|
|
3403
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.",
|
|
3404
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
|
+
},
|
|
3405
3707
|
{
|
|
3406
3708
|
"name": "eyebrow",
|
|
3407
3709
|
"kind": "text",
|
|
@@ -3494,6 +3796,17 @@
|
|
|
3494
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).",
|
|
3495
3797
|
"context": [],
|
|
3496
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
|
+
},
|
|
3497
3810
|
{
|
|
3498
3811
|
"name": "eyebrow",
|
|
3499
3812
|
"kind": "text",
|
|
@@ -3533,6 +3846,23 @@
|
|
|
3533
3846
|
"home"
|
|
3534
3847
|
],
|
|
3535
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
|
+
},
|
|
3536
3866
|
{
|
|
3537
3867
|
"name": "eyebrow",
|
|
3538
3868
|
"kind": "text",
|
|
@@ -5272,7 +5602,7 @@
|
|
|
5272
5602
|
"name": "language_switch",
|
|
5273
5603
|
"label": "Language switch",
|
|
5274
5604
|
"status": "available",
|
|
5275
|
-
"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.",
|
|
5276
5606
|
"params": [],
|
|
5277
5607
|
"stylingApi": [
|
|
5278
5608
|
".language-switch",
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// JSON); this module does no file loading.
|
|
6
6
|
|
|
7
7
|
/** Marker protocol for island placement: the island tag emits these; the renderer splits on them. */
|
|
8
|
+
import { templateMessage, templateDate } from './locale.mjs';
|
|
9
|
+
|
|
8
10
|
export const ISLAND_MARK_START = '\u0000P60_ISLAND:';
|
|
9
11
|
export const ISLAND_MARK_END = '\u0000';
|
|
10
12
|
|
|
@@ -31,6 +33,8 @@ export function configureDialect(liquid, dialect, islandNames) {
|
|
|
31
33
|
}
|
|
32
34
|
// Filters LiquidJS ships beyond our whitelist (e.g. its non-standard json/inspect debug filters).
|
|
33
35
|
const allowed = new Set(dialect.filters);
|
|
36
|
+
if (allowed.has('t')) liquid.registerFilter('t', templateMessage);
|
|
37
|
+
if (allowed.has('local_date')) liquid.registerFilter('local_date', templateDate);
|
|
34
38
|
for (const name of ['json', 'inspect', 'to_integer', 'normalize_whitespace', 'find', 'find_exp', 'group_by', 'group_by_exp', 'where_exp', 'sum']) {
|
|
35
39
|
if (!allowed.has(name)) {
|
|
36
40
|
liquid.registerFilter(name, () => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Template interface copy only. Never translate tenant-authored content or source record labels.
|
|
2
|
+
// Plain JS so the host and kit use the exact same implementation, without network or tenant state.
|
|
3
|
+
const arabic = Object.freeze({
|
|
4
|
+
'Explore': 'تصفّح', 'Menu': 'القائمة', 'Open menu': 'فتح القائمة', 'Close menu': 'إغلاق القائمة',
|
|
5
|
+
'Skip to content': 'انتقل إلى المحتوى', 'Skip to main content': 'انتقل إلى المحتوى الرئيسي',
|
|
6
|
+
'Search the website': 'ابحث في الموقع', 'Search this site': 'ابحث في الموقع',
|
|
7
|
+
'Main': 'التنقل الرئيسي', 'Footer': 'تذييل الصفحة', 'Social links': 'حسابات التواصل',
|
|
8
|
+
'Cookies': 'ملفات تعريف الارتباط', 'Cookie settings': 'إعدادات ملفات تعريف الارتباط',
|
|
9
|
+
'Previous image': 'الصورة السابقة', 'Next image': 'الصورة التالية',
|
|
10
|
+
'Previous campaign': 'الحملة السابقة', 'Next campaign': 'الحملة التالية',
|
|
11
|
+
'At the mosque': 'في المسجد', 'Prayer times': 'مواقيت الصلاة',
|
|
12
|
+
'Prayer and Jumuah times': 'مواقيت الصلاة والجمعة', 'Prayer schedule': 'مواقيت الصلاة',
|
|
13
|
+
'Prayer timetable': 'جدول الصلاة', 'Daily prayer timetable': 'جدول الصلوات اليومية',
|
|
14
|
+
'Daily prayer times': 'مواقيت الصلوات اليومية', 'Daily prayers': 'الصلوات اليومية',
|
|
15
|
+
'Prayer': 'الصلاة', 'Jumuah': 'الجمعة', 'Friday prayer': 'صلاة الجمعة',
|
|
16
|
+
'Friday at the mosque': 'الجمعة في المسجد', 'Prayer information': 'معلومات الصلاة',
|
|
17
|
+
'Full timetable': 'الجدول الكامل', 'Next:': 'الصلاة القادمة:',
|
|
18
|
+
'Begins': 'بداية الوقت', 'Congregation': 'الجماعة', 'Jama’at': 'الجماعة',
|
|
19
|
+
'Read article': 'اقرأ الخبر', 'Read more': 'اقرأ المزيد', 'Find out more': 'اعرف المزيد',
|
|
20
|
+
'Event details': 'تفاصيل الفعالية', 'View event': 'عرض الفعالية',
|
|
21
|
+
'View course': 'عرض الدورة', 'Explore course': 'اكتشف الدورة',
|
|
22
|
+
'View campaign': 'عرض الحملة', 'Explore campaign': 'اكتشف الحملة',
|
|
23
|
+
'Support this campaign': 'ادعم هذه الحملة', 'Online': 'عبر الإنترنت',
|
|
24
|
+
'Tickets not on sale': 'التذاكر غير متاحة للبيع', 'Sold out': 'اكتمل العدد',
|
|
25
|
+
'Free': 'مجاناً', 'From': 'من', 'Registration': 'التسجيل', 'Tickets': 'تذاكر',
|
|
26
|
+
'Get tickets': 'احجز تذكرتك', 'Book a place': 'احجز مكانك',
|
|
27
|
+
'Download': 'تنزيل', 'Read document': 'اقرأ المستند', 'View document': 'عرض المستند',
|
|
28
|
+
'All articles': 'جميع الأخبار', 'All events': 'جميع الفعاليات', 'All courses': 'جميع الدورات',
|
|
29
|
+
'All campaigns': 'جميع الحملات', 'All services': 'جميع الخدمات',
|
|
30
|
+
'Article': 'خبر', 'Course': 'دورة', 'Campaign': 'حملة', 'Service': 'خدمة',
|
|
31
|
+
'Article unavailable': 'الخبر غير متاح', 'Course unavailable': 'الدورة غير متاحة',
|
|
32
|
+
'This article is not available to view.': 'هذا الخبر غير متاح للعرض.',
|
|
33
|
+
'This course is not available to view.': 'هذه الدورة غير متاحة للعرض.',
|
|
34
|
+
'Browse': 'تصفّح', 'Browse courses': 'تصفّح الدورات', 'Back to': 'العودة إلى',
|
|
35
|
+
'Availability': 'التوفّر', 'Course details': 'تفاصيل الدورة', 'Course options': 'خيارات الدورة',
|
|
36
|
+
'Course enrolment': 'التسجيل في الدورة', 'Open article photograph': 'فتح صورة الخبر',
|
|
37
|
+
'Children': 'الأطفال', 'Adults': 'البالغون', 'Families': 'العائلات', 'Everyone': 'الجميع',
|
|
38
|
+
'Starts': 'البداية', 'Ends': 'النهاية', 'Monthly': 'شهرياً', 'One payment': 'دفعة واحدة',
|
|
39
|
+
'No places currently available': 'لا توجد أماكن متاحة حالياً', 'Not supplied': 'غير محدد',
|
|
40
|
+
'Payment': 'الدفع', 'Price': 'السعر', 'Term fee': 'رسوم الفصل', 'Updated': 'آخر تحديث',
|
|
41
|
+
'Where': 'المكان', 'Who it is for': 'الفئة المستهدفة', 'Open document': 'فتح المستند',
|
|
42
|
+
'Word document': 'مستند Word', 'Support this appeal': 'ادعم هذا النداء',
|
|
43
|
+
'View available sessions and enrolment details.': 'اطّلع على الجلسات المتاحة وتفاصيل التسجيل.',
|
|
44
|
+
'min read': 'دقائق للقراءة', '% of target reached': '% من الهدف',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const templateMessageKeys = Object.freeze(Object.keys(arabic));
|
|
48
|
+
const language = value => typeof value === 'string' && value.length <= 35
|
|
49
|
+
? value.toLowerCase().split('-')[0] : 'en';
|
|
50
|
+
|
|
51
|
+
/** Unknown copy/languages retain the supplied English UI fallback. Output remains autoescaped. */
|
|
52
|
+
export function templateMessage(message, locale) {
|
|
53
|
+
if (typeof message !== 'string') return '';
|
|
54
|
+
return language(locale) === 'ar' && Object.hasOwn(arabic, message) ? arabic[message] : message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Exactly six formatter instances at most. No per-tenant or arbitrary-pattern cache growth.
|
|
58
|
+
const formatters = new Map();
|
|
59
|
+
export function templateDate(value, locale, style = 'date') {
|
|
60
|
+
// A timestamp needs an explicit offset. Date.parse on a zone-less timestamp would depend
|
|
61
|
+
// on the kit author's machine rather than the host's UTC rendering contract.
|
|
62
|
+
if (typeof value !== 'string' || value.length > 64 || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2}))?$/.test(value)) return '';
|
|
63
|
+
const timestamp = Date.parse(value);
|
|
64
|
+
if (!Number.isFinite(timestamp) || !['date', 'datetime'].includes(style)) return '';
|
|
65
|
+
const day = value.slice(0, 10);
|
|
66
|
+
if (new Date(Date.parse(`${day}T00:00:00Z`)).toISOString().slice(0, 10) !== day) return '';
|
|
67
|
+
const lang = ['ar', 'cy'].includes(language(locale)) ? language(locale) : 'en-GB';
|
|
68
|
+
const key = `${lang}:${style}`;
|
|
69
|
+
if (!formatters.has(key)) formatters.set(key, new Intl.DateTimeFormat(lang, {
|
|
70
|
+
calendar: 'gregory', timeZone: 'UTC', day: '2-digit', month: 'long', year: 'numeric',
|
|
71
|
+
...(style === 'datetime' ? { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' } : {}),
|
|
72
|
+
}));
|
|
73
|
+
return formatters.get(key).format(timestamp);
|
|
74
|
+
}
|