@port60/template-kit 0.8.0 → 0.10.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 +10 -3
- package/bin/cli.mjs +26 -4
- package/package.json +1 -1
- package/src/commands/content.mjs +42 -0
- package/src/commands/create.mjs +10 -7
- package/src/commands/dev.mjs +134 -8
- package/src/commands/model.mjs +34 -0
- package/src/commands/packageCmd.mjs +3 -3
- package/src/commands/refresh.mjs +48 -0
- package/src/commands/upgrade.mjs +44 -0
- package/src/commands/validate.mjs +2 -3
- package/src/lib/agentsMd.mjs +78 -21
- package/src/vendor/contract/v1/behaviours.json +129 -0
- package/src/vendor/contract/v1/content-bounds.json +3 -3
- package/src/vendor/contract/v1/content-model.json +193 -0
- package/src/vendor/contract/v1/context.json +1985 -141
- package/src/vendor/contract/v1/dialect.json +3 -3
- package/src/vendor/contract/v1/fonts.json +1 -1
- package/src/vendor/contract/v1/imagery.json +4 -4
- package/src/vendor/contract/v1/islands.json +256 -110
- package/src/vendor/contract/v1/layout.json +25 -0
- package/src/vendor/contract/v1/manifest.schema.json +60 -14
- package/src/vendor/contract/v1/sections.json +330 -61
- package/src/vendor/contract/v1/tokens.json +1 -1
- package/src/vendor/contract/v1.lock.json +1 -1
- package/src/vendor/engine/content-footprint.mjs +79 -0
- package/src/vendor/validator/behaviors-runtime.js +2 -0
- package/src/vendor/validator/fixture-art.mjs +109 -0
- package/src/vendor/validator/model-reference.mjs +92 -0
- package/src/vendor/validator/platform-base.css +4250 -0
- package/src/vendor/validator/preview.mjs +497 -23
- package/src/vendor/validator/site-context.mjs +97 -0
- package/src/vendor/validator/validate.mjs +214 -14
- package/starter/assets/theme.css +54 -8
- package/starter/layout.liquid +15 -16
- package/starter/manifest.json +10 -5
- package/starter/sections/campaigns.liquid +23 -0
- package/starter/sections/homeHero.liquid +26 -11
- package/starter/sections/people.liquid +1 -1
- package/starter/sections/values.liquid +6 -1
package/src/lib/agentsMd.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// The scaffold's AI-agent briefing (T3, user requirement: the kit must let people put an AI
|
|
2
|
-
// engine on template work and be productive immediately). Written for ANY coding agent
|
|
3
|
-
// AGENTS.md is the cross-tool convention and CLAUDE.md carries the identical content for tools
|
|
2
|
+
// engine on template work and be productive immediately). Written for ANY coding agent, // AGENTS.md is the cross-tool convention and CLAUDE.md carries the identical content for tools
|
|
4
3
|
// that read that name. The briefing is the contract's rules + the iteration loop, so an agent's
|
|
5
4
|
// first move is always the same: read this, edit, validate --json, repeat until clean.
|
|
6
5
|
|
|
7
6
|
export function agentsMd(name) {
|
|
8
7
|
return `# Working on the "${name}" Port60 template
|
|
9
8
|
|
|
10
|
-
You are working on a **Port60 site template
|
|
9
|
+
You are working on a **Port60 site template**, a small, versioned artifact of Liquid renderers
|
|
11
10
|
and CSS that a charity's site is rendered through. It contains **no application code**: no
|
|
12
11
|
JavaScript, no API calls, no payment logic. Templates decide how a site *looks*; the platform
|
|
13
12
|
owns what it *does*.
|
|
@@ -22,25 +21,25 @@ npm run package # validate + produce the uploadable <name>-<version>.z
|
|
|
22
21
|
\`\`\`
|
|
23
22
|
|
|
24
23
|
**After every meaningful edit, run \`npm run validate:json\` and fix every error before moving
|
|
25
|
-
on.** The validator is the exact code the platform runs at upload
|
|
24
|
+
on.** The validator is the exact code the platform runs at upload, if it passes here, the
|
|
26
25
|
platform accepts it; if it fails here, the upload will fail identically.
|
|
27
26
|
|
|
28
27
|
## The file layout (nothing else is accepted)
|
|
29
28
|
|
|
30
|
-
- \`manifest.json
|
|
29
|
+
- \`manifest.json\`, identity + what you support. \`name\` and \`version\` are immutable
|
|
31
30
|
identity; bump \`version\` (semver) for every published change.
|
|
32
|
-
- \`layout.liquid
|
|
31
|
+
- \`layout.liquid\`, the page chrome (header/nav/footer). Must contain **exactly one**
|
|
33
32
|
\`{% content %}\` slot. Only needed when \`supports.layout\` is true.
|
|
34
|
-
- \`sections/<type>.liquid
|
|
33
|
+
- \`sections/<type>.liquid\`, one renderer per section type you declare in
|
|
35
34
|
\`supports.sections\`. Charities compose pages from a **closed catalogue** of section types
|
|
36
|
-
(see \`npm run validate\` output or the docs)
|
|
37
|
-
- \`pages/<page>.liquid
|
|
38
|
-
- \`assets/theme.css
|
|
39
|
-
allowed. **No images, no JS
|
|
35
|
+
(see \`npm run validate\` output or the docs), you cannot invent new types.
|
|
36
|
+
- \`pages/<page>.liquid\`, optional full-page templates for \`supports.pageTemplates\`.
|
|
37
|
+
- \`assets/theme.css\`, required, your entire look. More \`.css\` files under \`assets/\` are
|
|
38
|
+
allowed. **No images, no JS**, they are refused at upload.
|
|
40
39
|
|
|
41
40
|
## The rules the validator enforces (do not fight them)
|
|
42
41
|
|
|
43
|
-
1. **Escape-by-default.** Every output is HTML-escaped unless you use \`| raw
|
|
42
|
+
1. **Escape-by-default.** Every output is HTML-escaped unless you use \`| raw\`, and the only
|
|
44
43
|
values you may pass through raw are the contract's sanitised richtext fields.
|
|
45
44
|
2. **The dialect is a whitelist.** \`{% include %}\`, \`{% render %}\`, \`{% layout %}\` and
|
|
46
45
|
several other tags are excluded and fail at parse. Unknown filters throw.
|
|
@@ -53,30 +52,88 @@ platform accepts it; if it fails here, the upload will fail identically.
|
|
|
53
52
|
hide the rail when \`worship\` is null, and rendering it undeclared is equally an error. The
|
|
54
53
|
same honesty applies across the contract.
|
|
55
54
|
5. **Render budgets are real.** Runaway loops are killed (~1s per render). Keep renderers simple.
|
|
56
|
-
6. **Context is a whitelist.** Sections see \`{section, brand}
|
|
57
|
-
\`{brand, nav, socials, worship}\`; page
|
|
58
|
-
exists
|
|
55
|
+
6. **Context is a whitelist.** Sections see \`{section, brand}\` plus only the collection named by
|
|
56
|
+
that section in the contract; layouts see \`{brand, nav, socials, worship, locale}\`; page
|
|
57
|
+
templates see their documented fixture + \`brand\`. Nothing else exists, do not invent
|
|
58
|
+
variables.
|
|
59
|
+
7. **Capabilities are matching metadata, never entitlements.** Every value in
|
|
60
|
+
\`requiresCapabilities\` must have a declared section, page template or island that presents it.
|
|
61
|
+
\`suitsProfiles\` describes design intent and changes catalogue ordering only.
|
|
62
|
+
|
|
63
|
+
## What each declaration owns
|
|
64
|
+
|
|
65
|
+
- \`supports.layout\` owns the visible header, navigation and footer around platform pages. The
|
|
66
|
+
platform still owns the document head, consent and identity.
|
|
67
|
+
- \`supports.pages\` owns section based bodies for \`home\` and \`about\` through the declared
|
|
68
|
+
renderers in \`sections/\`.
|
|
69
|
+
- \`supports.pageTemplates: ["events"]\` owns the events listing only. Event details, RSVP and
|
|
70
|
+
ticket purchase remain platform owned.
|
|
71
|
+
- \`supports.pageTemplates: ["course"]\` owns a course detail presentation only. The course
|
|
72
|
+
listing stays platform owned and enrolment remains the \`course_enrol\` island.
|
|
73
|
+
- \`supports.pageTemplates: ["articles"]\` owns the article front page and archive listings.
|
|
74
|
+
- \`supports.pageTemplates: ["article"]\` owns article detail presentation; engagement and
|
|
75
|
+
comments stay the \`article_engagement\` and \`article_comments\` islands.
|
|
76
|
+
- Other platform routes keep their platform body and render inside your layout. Capability flags
|
|
77
|
+
expose documented optional context; they do not transfer transaction or route ownership.
|
|
78
|
+
|
|
79
|
+
## The content model
|
|
80
|
+
|
|
81
|
+
Everything you read comes from ONE tree: \`site\`, \`site.brand\`, \`site.nav\`,
|
|
82
|
+
\`site.socials\`, \`site.locale\` and the typed collections under \`site.content.*\`
|
|
83
|
+
(services, events, articles, campaigns, causes, courses, volunteering, media, resources,
|
|
84
|
+
locations, schedules, about). Four rules it never breaks, so neither should you:
|
|
85
|
+
|
|
86
|
+
- Every collection is BOUNDED (a documented cap plus a \`moreHref\`), link onward, never
|
|
87
|
+
assume you have everything.
|
|
88
|
+
- Enum fields are OPEN, branch on the values you style and fall back for the rest; the
|
|
89
|
+
validator proves your template survives values it has never seen.
|
|
90
|
+
- Optional fields are explicitly nullable, always branch.
|
|
91
|
+
- The model only grows. The validator computes your content footprint from the paths you read
|
|
92
|
+
and stamps the minimum model version at publish; you never declare versions, and dynamic
|
|
93
|
+
indexing into \`site.content\` is refused so that stays decidable.
|
|
94
|
+
|
|
95
|
+
SEE the model: \`npm run dev\` serves the full reference with live example data at \`/model\`;
|
|
96
|
+
\`npx p60-template-kit model --json\` prints the machine-readable registry; the same reference
|
|
97
|
+
lives at https://developers.port60.com/reference/content-model/.
|
|
98
|
+
|
|
99
|
+
Bring your own content and imagery: \`npx p60-template-kit content .\` ejects every collection,
|
|
100
|
+
fully populated, as an editable JSON file; replace the copy and the imageUrl values, then
|
|
101
|
+
\`npx p60-template-kit dev . --content my-org.json\` renders YOUR data (the preview admits
|
|
102
|
+
exactly the image hosts your file names, nothing else).
|
|
103
|
+
|
|
104
|
+
You may replace the preview DATA with your own via \`preview-content.json\` beside the
|
|
105
|
+
manifest ({ collection: [items] }, schema-checked, hot-reloaded). The shape is fixed, packaging
|
|
106
|
+
excludes it, and conformance proofs always run on the canonical fixtures.
|
|
59
107
|
|
|
60
108
|
## What to build with
|
|
61
109
|
|
|
62
110
|
- Theme via CSS custom properties and the settings knobs you declare in
|
|
63
|
-
\`manifest.settings.schema
|
|
111
|
+
\`manifest.settings.schema\`, they surface in the charity's Appearance editor as
|
|
64
112
|
\`--p60s-<key>\` variables and \`data-p60s-<key>\` body attributes.
|
|
65
113
|
- Hero photographs (\`homeHero.images\`, when you declare \`supports.heroImagery\`): render one
|
|
66
114
|
photo directly as a TREATED backdrop (a scrim/tint built from your own palette variables via
|
|
67
|
-
color-mix
|
|
68
|
-
\`.hero-slide-scrim\`), and design the no-photo state as a gradient/colour
|
|
115
|
+
color-mix, never raw), place the \`hero_carousel\` island for two or more (style its
|
|
116
|
+
\`.hero-slide-scrim\`), and design the no-photo state as a gradient/colour, never a
|
|
69
117
|
placeholder. The starter's \`.lq-homehero\` is the reference; the validator checks all of this
|
|
70
118
|
behaviourally.
|
|
71
119
|
- "Looks" = named one-click bundles of knob values in \`manifest.looks\`.
|
|
72
120
|
- Fonts: only families from the platform font catalogue, declared with the weights you use.
|
|
121
|
+
- Navigation can contain two levels below a top item. Render every supplied child and branch on
|
|
122
|
+
optional \`group\`, \`description\`, \`imageUrl\` and \`megaMenu\` promo metadata. Never hardcode
|
|
123
|
+
menu groups that are not in \`nav\`.
|
|
124
|
+
- Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources, locations,
|
|
125
|
+
volunteering opportunities and structured media. Derive or omit when a collection is empty and
|
|
126
|
+
use the supplied URLs rather than constructing routes.
|
|
127
|
+
- Submission and behaviour surfaces such as \`newsletter_signup\`, \`volunteer_signup\`, \`form\`,
|
|
128
|
+
\`search\`, \`language_switch\` and \`next_prayer\` are platform islands. Place and style them;
|
|
129
|
+
never reproduce their API calls or consent behaviour.
|
|
73
130
|
- \`manifest.imagery.hero\` (optional but recommended): declare the photo shape YOUR hero
|
|
74
|
-
composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`)
|
|
131
|
+
composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`), the charity's editor
|
|
75
132
|
measures their actual upload against it and advises. Advice, never enforcement.
|
|
76
133
|
|
|
77
134
|
## Which contract this is
|
|
78
135
|
|
|
79
|
-
The section catalogue, islands and fixtures here are the **Charity Platform contract v1
|
|
136
|
+
The section catalogue, islands and fixtures here are the **Charity Platform contract v1**, the
|
|
80
137
|
platform's first product surface. The dialect, the rules above and this toolchain are
|
|
81
138
|
platform-wide; other Port60 products will ship their own contract packs. Do not assume the
|
|
82
139
|
current section list is universal.
|
|
@@ -84,7 +141,7 @@ current section list is universal.
|
|
|
84
141
|
## Reference
|
|
85
142
|
|
|
86
143
|
The full generated reference (sections, islands, context variables, tokens, dialect) lives at
|
|
87
|
-
https://developers.port60.com
|
|
144
|
+
https://developers.port60.com, also available in one file for agents at
|
|
88
145
|
https://developers.port60.com/llms-full.txt
|
|
89
146
|
`;
|
|
90
147
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "The behaviour catalogue: engine-attached JavaScript a template opts into with data attributes on its own markup. Templates never ship code; they author complete static markup and declare behaviours in supports.behaviors. The engine adds the p60-js class to the root element when its script runs; any CSS that hides pre-behaviour content MUST be scoped under .p60-js so the no-JS render stays complete. All behaviours honour prefers-reduced-motion centrally. State classes are toggled by the engine; their appearance is entirely the template's CSS.",
|
|
3
|
+
"behaviours": [
|
|
4
|
+
{
|
|
5
|
+
"name": "reveal",
|
|
6
|
+
"status": "available",
|
|
7
|
+
"label": "Reveal on scroll",
|
|
8
|
+
"description": "Adds is-revealed when the element first enters the viewport; the template's CSS does the actual motion. data-p60-reveal-group on a container staggers its children by stamping --p60-reveal-index on each (use it in a transition-delay calc). Reduced motion: is-revealed applies immediately on load.",
|
|
9
|
+
"attributes": [
|
|
10
|
+
{ "attr": "data-p60-reveal", "value": "optional variant name for the CSS to key on" },
|
|
11
|
+
{ "attr": "data-p60-reveal-group", "value": "none; staggers direct children" }
|
|
12
|
+
],
|
|
13
|
+
"stateClasses": ["is-revealed"],
|
|
14
|
+
"cssVars": ["--p60-reveal-index"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "counter",
|
|
18
|
+
"status": "available",
|
|
19
|
+
"label": "Animated counter",
|
|
20
|
+
"description": "Counts the element's number up from zero when first revealed. The static text IS the final formatted value (the no-JS render); data-p60-count carries the numeric target the engine animates to, grouped with the page locale. Reduced motion: the final number shows immediately.",
|
|
21
|
+
"attributes": [
|
|
22
|
+
{ "attr": "data-p60-count", "value": "the numeric target, digits only" },
|
|
23
|
+
{ "attr": "data-p60-count-duration", "value": "optional ms, capped at 4000 (default 1200)" }
|
|
24
|
+
],
|
|
25
|
+
"stateClasses": ["is-revealed"],
|
|
26
|
+
"cssVars": []
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "progress",
|
|
30
|
+
"status": "available",
|
|
31
|
+
"label": "Progress bar sweep",
|
|
32
|
+
"description": "Animates the marked element's width from zero to its authored width when first revealed. The authored width (inline style or CSS) is the target and the no-JS render. Reduced motion: the authored width stands untouched.",
|
|
33
|
+
"attributes": [
|
|
34
|
+
{ "attr": "data-p60-progress", "value": "none; the element's own width is the target" }
|
|
35
|
+
],
|
|
36
|
+
"stateClasses": ["is-revealed"],
|
|
37
|
+
"cssVars": []
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "countdown",
|
|
41
|
+
"status": "available",
|
|
42
|
+
"label": "Countdown",
|
|
43
|
+
"description": "Replaces the element's content with a live compact remainder to data-p60-until (\"3d 12h 45m\"). The static content is the authored date text (the no-JS render). On expiry the engine adds is-elapsed and swaps to data-p60-elapsed-text when provided. Countdowns keep ticking under reduced motion: they are information, not decoration. v1 unit labels are compact and language-neutral (d/h/m/s).",
|
|
44
|
+
"attributes": [
|
|
45
|
+
{ "attr": "data-p60-countdown", "value": "segments: dhms, dhm (default) or hm" },
|
|
46
|
+
{ "attr": "data-p60-until", "value": "ISO-8601 instant, required" },
|
|
47
|
+
{ "attr": "data-p60-elapsed-text", "value": "optional text shown once elapsed" }
|
|
48
|
+
],
|
|
49
|
+
"stateClasses": ["is-elapsed"],
|
|
50
|
+
"cssVars": []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "accordion",
|
|
54
|
+
"status": "available",
|
|
55
|
+
"label": "Accordion",
|
|
56
|
+
"description": "On a container of native details elements: exclusive-open (opening one closes the others) plus smooth height animation. data-p60-accordion=\"multi\" keeps independent opening. Built on details/summary so the no-JS render is already functional and semantics are free. Reduced motion: no height animation, instant toggle.",
|
|
57
|
+
"attributes": [
|
|
58
|
+
{ "attr": "data-p60-accordion", "value": "none for exclusive-open; \"multi\" for independent" }
|
|
59
|
+
],
|
|
60
|
+
"stateClasses": ["is-open"],
|
|
61
|
+
"cssVars": []
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "stickyHeader",
|
|
65
|
+
"status": "available",
|
|
66
|
+
"label": "Sticky header condense",
|
|
67
|
+
"description": "State classes for scroll-aware headers: past a small threshold the engine adds is-condensed; with the \"auto-hide\" value, scrolling down past 160px adds is-hidden and scrolling up removes it. The engine only ever toggles classes (rAF-throttled), what condensing or hiding looks like, and whether it animates, is entirely the template's CSS (gate transitions behind prefers-reduced-motion yourself; the classes always apply because a condensed header is layout, not decoration).",
|
|
68
|
+
"attributes": [
|
|
69
|
+
{ "attr": "data-p60-sticky-header", "value": "none for condense-only; \"auto-hide\" adds the hide-on-scroll-down pair" }
|
|
70
|
+
],
|
|
71
|
+
"stateClasses": ["is-condensed", "is-hidden"],
|
|
72
|
+
"cssVars": []
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "stickyCta",
|
|
76
|
+
"status": "available",
|
|
77
|
+
"label": "Sticky call-to-action bar",
|
|
78
|
+
"description": "Adds is-stuck once the visitor scrolls past the threshold (the attribute value in px, default 480) and removes it above. The bar should DUPLICATE an action that already exists in the page (a donate button, a ticket link); because of that, this is the one behaviour whose element may be hidden unconditionally in CSS rather than .p60-js-scoped: a no-JS visitor loses nothing; the original action is still on the page.",
|
|
79
|
+
"attributes": [
|
|
80
|
+
{ "attr": "data-p60-sticky-cta", "value": "optional scroll threshold in px (default 480)" }
|
|
81
|
+
],
|
|
82
|
+
"stateClasses": ["is-stuck"],
|
|
83
|
+
"cssVars": []
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "lightbox",
|
|
87
|
+
"status": "available",
|
|
88
|
+
"label": "Lightbox",
|
|
89
|
+
"description": "On a group container: each data-p60-lightbox-item is an anchor whose href is the full image (the no-JS render simply navigates to it, already functional). The engine intercepts the click and opens an injected overlay (.p60-lightbox) with the image, a caption from data-p60-caption or the thumbnail's alt, a close button, backdrop and Escape close, arrow-key prev/next within the group, and a focus trap. Reduced motion: no transition on the overlay (the engine sets none; style the steady state).",
|
|
90
|
+
"attributes": [
|
|
91
|
+
{ "attr": "data-p60-lightbox", "value": "none; the group container" },
|
|
92
|
+
{ "attr": "data-p60-lightbox-item", "value": "none; on each anchor to a full image" },
|
|
93
|
+
{ "attr": "data-p60-caption", "value": "optional caption text on an item" }
|
|
94
|
+
],
|
|
95
|
+
"stateClasses": ["is-open"],
|
|
96
|
+
"cssVars": [],
|
|
97
|
+
"injects": [".p60-lightbox", ".p60-lightbox-img", ".p60-lightbox-caption", ".p60-lightbox-close", ".p60-lightbox-prev", ".p60-lightbox-next"]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "tabs",
|
|
101
|
+
"status": "available",
|
|
102
|
+
"label": "Tabs",
|
|
103
|
+
"description": "data-p60-tab=\"key\" on each tab control and data-p60-panel=\"key\" on each panel, inside a data-p60-tabs container. The engine wires the tablist/tab/tabpanel roles, aria-selected, roving tabindex, arrow/Home/End keys, toggles is-active on the active pair and hides inactive panels with the hidden attribute, so the no-JS render shows every panel stacked and complete, with no CSS scoping needed. The first tab (or the one authored with is-active) starts active.",
|
|
104
|
+
"attributes": [
|
|
105
|
+
{ "attr": "data-p60-tabs", "value": "none; the container" },
|
|
106
|
+
{ "attr": "data-p60-tab", "value": "the key of the panel this control activates" },
|
|
107
|
+
{ "attr": "data-p60-panel", "value": "the key matching its control" }
|
|
108
|
+
],
|
|
109
|
+
"stateClasses": ["is-active"],
|
|
110
|
+
"cssVars": []
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "carousel",
|
|
114
|
+
"status": "available",
|
|
115
|
+
"label": "Carousel",
|
|
116
|
+
"description": "On a container with one data-p60-slide per child: the engine manages is-active across slides, injects a dots nav (.p60-carousel-dots with one button per slide), wires optional template-provided data-p60-carousel-prev/data-p60-carousel-next controls, adds swipe, keyboard arrows, pause on hover and focus, and hides inactive slides from assistive tech. Value \"auto\" enables auto-advance (data-p60-interval ms, min 3000, default 6000); reduced motion disables auto-advance while controls keep working. The engine stamps --p60-carousel-index on the container for track-translate designs; class-based fade on is-active is the simplest pattern. CSS hiding non-active slides MUST be .p60-js-scoped so the no-JS render shows all slides.",
|
|
117
|
+
"attributes": [
|
|
118
|
+
{ "attr": "data-p60-carousel", "value": "none for manual; \"auto\" for auto-advance" },
|
|
119
|
+
{ "attr": "data-p60-slide", "value": "none; one per slide child" },
|
|
120
|
+
{ "attr": "data-p60-interval", "value": "optional auto-advance ms, min 3000 (default 6000)" },
|
|
121
|
+
{ "attr": "data-p60-carousel-prev", "value": "none; optional template-provided control" },
|
|
122
|
+
{ "attr": "data-p60-carousel-next", "value": "none; optional template-provided control" }
|
|
123
|
+
],
|
|
124
|
+
"stateClasses": ["is-active"],
|
|
125
|
+
"cssVars": ["--p60-carousel-index"],
|
|
126
|
+
"injects": [".p60-carousel-dots", ".p60-carousel-dot", ".p60-carousel-dot--active"]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "The maximum lengths of every admin-entered display field a template renders. The admin screens stop input at these caps and the API rejects anything longer, so a template can design cards, headings and captions against known worst cases. Additive-only within a contract major: a bound may loosen, never tighten. Long-form bodies (article bodies, course About pages, service pages) are deliberately outside this table
|
|
2
|
+
"description": "The maximum lengths of every admin-entered display field a template renders. The admin screens stop input at these caps and the API rejects anything longer, so a template can design cards, headings and captions against known worst cases. Additive-only within a contract major: a bound may loosen, never tighten. Long-form bodies (article bodies, course About pages, service pages) are deliberately outside this table; they are sanitised HTML rendered in dedicated full-width regions, never inside cards.",
|
|
3
3
|
"groups": [
|
|
4
4
|
{
|
|
5
5
|
"name": "Brand (layout chrome)",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
{
|
|
69
69
|
"field": "Tag",
|
|
70
70
|
"max": 40,
|
|
71
|
-
"note": "Each
|
|
71
|
+
"note": "Each, rendered as chips."
|
|
72
72
|
}
|
|
73
73
|
]
|
|
74
74
|
},
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
{
|
|
110
110
|
"field": "Page name",
|
|
111
111
|
"max": 200,
|
|
112
|
-
"note": "The body is sanitised HTML rendered full-width
|
|
112
|
+
"note": "The body is sanitised HTML rendered full-width, not card content."
|
|
113
113
|
}
|
|
114
114
|
]
|
|
115
115
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "The CONTENT MODEL REGISTRY (docs/template-content-model.md, v1): the typed `site.content.*` tree every template reads. One realistic organisation, one shape. Four disciplines are baked in from birth because they cannot be retrofitted: every collection is BOUNDED (a documented cap and an explicit moreHref, so a big tenant can never break a render budget); every enum field is OPEN (values grow; templates must survive unknown values, proven behaviourally); every optional field is EXPLICITLY nullable (branch-on-it is the norm); and the model is ADD-ONLY (fields and collections gain `deprecated: true` markers, they are never removed within a major, deprecated means it stops being documented, never that it stops being served). Versioning is COMPUTED, never author-declared: every collection and field carries `since`, the validator derives a template's required minimum from its content footprint at publish, and authors never think about versions. New list or field = minor. Fixture or doc clarification = patch.",
|
|
3
|
+
"version": "1.0",
|
|
4
|
+
"root": "site",
|
|
5
|
+
"siblings": {
|
|
6
|
+
"description": "site.brand, site.nav, site.socials and site.locale carry the SAME shapes documented in context.json for the flat era; the tree re-addresses them, it does not reshape them.",
|
|
7
|
+
"keys": ["brand", "nav", "socials", "locale"]
|
|
8
|
+
},
|
|
9
|
+
"collections": {
|
|
10
|
+
"services": {
|
|
11
|
+
"since": "1.0",
|
|
12
|
+
"cap": 24,
|
|
13
|
+
"moreHref": "/services",
|
|
14
|
+
"description": "The organisation's published content pages (the services CMS), newest curation order first.",
|
|
15
|
+
"item": {
|
|
16
|
+
"id": { "type": "string", "since": "1.0" },
|
|
17
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
18
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
19
|
+
"summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
20
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
21
|
+
"href": { "type": "string", "since": "1.0" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"events": {
|
|
25
|
+
"since": "1.0",
|
|
26
|
+
"cap": 12,
|
|
27
|
+
"moreHref": "/events",
|
|
28
|
+
"description": "Published events, soonest first. `registrationMode` is an OPEN enum: INFO, RSVP and TICKETED exist today and new modes will arrive; branch on the ones you style and fall back for the rest.",
|
|
29
|
+
"item": {
|
|
30
|
+
"id": { "type": "string", "since": "1.0" },
|
|
31
|
+
"name": { "type": "string", "since": "1.0", "max": 200 },
|
|
32
|
+
"description": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
|
|
33
|
+
"registrationMode": { "type": "string", "since": "1.0", "enumOpen": ["INFO", "RSVP", "TICKETED"] },
|
|
34
|
+
"startsAt": { "type": "string", "since": "1.0" },
|
|
35
|
+
"endsAt": { "type": "string", "since": "1.0", "nullable": true },
|
|
36
|
+
"venueName": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
|
|
37
|
+
"online": { "type": "boolean", "since": "1.0" },
|
|
38
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
39
|
+
"onSale": { "type": "boolean", "since": "1.0" },
|
|
40
|
+
"soldOut": { "type": "boolean", "since": "1.0" },
|
|
41
|
+
"priceFrom": { "type": "number", "since": "1.0", "nullable": true },
|
|
42
|
+
"currency": { "type": "string", "since": "1.0", "nullable": true },
|
|
43
|
+
"detailHref": { "type": "string", "since": "1.0" }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"articles": {
|
|
47
|
+
"since": "1.0",
|
|
48
|
+
"cap": 12,
|
|
49
|
+
"moreHref": "/articles/all",
|
|
50
|
+
"description": "Published articles, newest first. Detail bodies stay editor-owned sanitised richtext on the article page BY DESIGN; the card model carries everything a listing needs.",
|
|
51
|
+
"item": {
|
|
52
|
+
"id": { "type": "string", "since": "1.0" },
|
|
53
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
54
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
55
|
+
"excerpt": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
56
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
57
|
+
"authorName": { "type": "string", "since": "1.0", "nullable": true, "max": 120 },
|
|
58
|
+
"authorRole": { "type": "string", "since": "1.0", "nullable": true, "max": 120 },
|
|
59
|
+
"categories": { "type": "string[]", "since": "1.0" },
|
|
60
|
+
"publishedAt": { "type": "string", "since": "1.0" },
|
|
61
|
+
"readingMinutes": { "type": "number", "since": "1.0", "nullable": true },
|
|
62
|
+
"href": { "type": "string", "since": "1.0" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"campaigns": {
|
|
66
|
+
"since": "1.0",
|
|
67
|
+
"cap": 12,
|
|
68
|
+
"moreHref": "/campaigns",
|
|
69
|
+
"description": "Live campaigns, curation order.",
|
|
70
|
+
"item": {
|
|
71
|
+
"id": { "type": "string", "since": "1.0" },
|
|
72
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
73
|
+
"href": { "type": "string", "since": "1.0" },
|
|
74
|
+
"summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
75
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"causes": {
|
|
79
|
+
"since": "1.0",
|
|
80
|
+
"cap": 12,
|
|
81
|
+
"moreHref": "/donate",
|
|
82
|
+
"description": "Causes currently accepting donations, quick-give first. `paymentOptions` mirrors the public cause contract.",
|
|
83
|
+
"item": {
|
|
84
|
+
"id": { "type": "string", "since": "1.0" },
|
|
85
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
86
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
87
|
+
"description": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
88
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
89
|
+
"targetAmount": { "type": "number", "since": "1.0", "nullable": true },
|
|
90
|
+
"raisedAmount": { "type": "number", "since": "1.0", "nullable": true },
|
|
91
|
+
"progressPercentage": { "type": "number", "since": "1.0", "nullable": true },
|
|
92
|
+
"href": { "type": "string", "since": "1.0" },
|
|
93
|
+
"paymentOptions": { "type": "object", "since": "1.0", "nullable": true }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"courses": {
|
|
97
|
+
"since": "1.0",
|
|
98
|
+
"cap": 12,
|
|
99
|
+
"moreHref": "/courses",
|
|
100
|
+
"description": "Enrollable courses. The long-form About body is sanitised richtext on the course page, never in cards.",
|
|
101
|
+
"item": {
|
|
102
|
+
"id": { "type": "string", "since": "1.0" },
|
|
103
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
104
|
+
"description": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
105
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
106
|
+
"venue": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
|
|
107
|
+
"audience": { "type": "string", "since": "1.0", "enumOpen": ["ADULT", "CHILD", "FAMILY"] },
|
|
108
|
+
"priceFrom": { "type": "number", "since": "1.0", "nullable": true },
|
|
109
|
+
"currency": { "type": "string", "since": "1.0", "nullable": true },
|
|
110
|
+
"soldOut": { "type": "boolean", "since": "1.0" },
|
|
111
|
+
"href": { "type": "string", "since": "1.0" }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"volunteering": {
|
|
115
|
+
"since": "1.0",
|
|
116
|
+
"cap": 12,
|
|
117
|
+
"moreHref": "/volunteering",
|
|
118
|
+
"description": "Open volunteering opportunities.",
|
|
119
|
+
"item": {
|
|
120
|
+
"id": { "type": "string", "since": "1.0" },
|
|
121
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
122
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
123
|
+
"summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
124
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
125
|
+
"content": { "type": "object", "since": "1.0", "nullable": true }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"media": {
|
|
129
|
+
"since": "1.0",
|
|
130
|
+
"cap": 12,
|
|
131
|
+
"moreHref": "/media",
|
|
132
|
+
"description": "Published media items. `content.mediaType` is an OPEN enum (VIDEO and AUDIO today).",
|
|
133
|
+
"item": {
|
|
134
|
+
"id": { "type": "string", "since": "1.0" },
|
|
135
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
136
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
137
|
+
"summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
138
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
139
|
+
"content": { "type": "object", "since": "1.0", "nullable": true }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"resources": {
|
|
143
|
+
"since": "1.0",
|
|
144
|
+
"cap": 24,
|
|
145
|
+
"moreHref": "/resources",
|
|
146
|
+
"description": "Published documents, grouped by folder.",
|
|
147
|
+
"item": {
|
|
148
|
+
"id": { "type": "string", "since": "1.0" },
|
|
149
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
150
|
+
"folder": { "type": "string", "since": "1.0", "nullable": true },
|
|
151
|
+
"url": { "type": "string", "since": "1.0" },
|
|
152
|
+
"contentType": { "type": "string", "since": "1.0", "nullable": true },
|
|
153
|
+
"bytes": { "type": "number", "since": "1.0", "nullable": true },
|
|
154
|
+
"publishedAt": { "type": "string", "since": "1.0", "nullable": true }
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"locations": {
|
|
158
|
+
"since": "1.0",
|
|
159
|
+
"cap": 12,
|
|
160
|
+
"moreHref": "/locations",
|
|
161
|
+
"description": "The organisation's physical places, primary first.",
|
|
162
|
+
"item": {
|
|
163
|
+
"id": { "type": "string", "since": "1.0" },
|
|
164
|
+
"name": { "type": "string", "since": "1.0", "max": 200 },
|
|
165
|
+
"address": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
166
|
+
"latitude": { "type": "number", "since": "1.0", "nullable": true },
|
|
167
|
+
"longitude": { "type": "number", "since": "1.0", "nullable": true },
|
|
168
|
+
"primary": { "type": "boolean", "since": "1.0" },
|
|
169
|
+
"directionsHref": { "type": "string", "since": "1.0", "nullable": true }
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"schedules": {
|
|
173
|
+
"since": "1.0",
|
|
174
|
+
"cap": 4,
|
|
175
|
+
"moreHref": null,
|
|
176
|
+
"description": "TYPE-DERIVED recurring schedules. `type` is an OPEN enum; 'worship' is the first (its shape is the layout worship contract: times, labels, today, next). Other institutions arrive as new types; render the types you know, ignore the rest.",
|
|
177
|
+
"item": {
|
|
178
|
+
"type": { "type": "string", "since": "1.0", "enumOpen": ["worship"] },
|
|
179
|
+
"content": { "type": "object", "since": "1.0" }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"about": {
|
|
183
|
+
"since": "1.0",
|
|
184
|
+
"cap": 24,
|
|
185
|
+
"moreHref": null,
|
|
186
|
+
"description": "The admin-authored page composition: the ordered sections of the home/about surface. `type` is the section catalogue's OPEN enum; `content` is that section type's fields. This is what `section.*` re-addresses; a mega-page template can walk it directly.",
|
|
187
|
+
"item": {
|
|
188
|
+
"type": { "type": "string", "since": "1.0", "enumOpen": [] },
|
|
189
|
+
"content": { "type": "object", "since": "1.0" }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|