@port60/template-kit 0.9.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 +6 -6
- package/src/commands/dev.mjs +97 -9
- 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 +47 -19
- package/src/vendor/contract/v1/behaviours.json +4 -4
- 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 +1667 -275
- 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 -235
- package/src/vendor/contract/v1/layout.json +4 -4
- package/src/vendor/contract/v1/manifest.schema.json +9 -9
- package/src/vendor/contract/v1/sections.json +248 -92
- 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/fixture-art.mjs +109 -0
- package/src/vendor/validator/model-reference.mjs +92 -0
- package/src/vendor/validator/platform-base.css +21 -0
- package/src/vendor/validator/preview.mjs +149 -56
- package/src/vendor/validator/site-context.mjs +97 -0
- package/src/vendor/validator/validate.mjs +71 -14
- package/starter/assets/theme.css +10 -10
- package/starter/layout.liquid +15 -16
- package/starter/manifest.json +2 -2
- package/starter/sections/campaigns.liquid +5 -5
- package/starter/sections/homeHero.liquid +11 -11
- package/starter/sections/people.liquid +1 -1
- package/starter/sections/values.liquid +1 -1
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"name": "stickyHeader",
|
|
65
65
|
"status": "available",
|
|
66
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)
|
|
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
68
|
"attributes": [
|
|
69
69
|
{ "attr": "data-p60-sticky-header", "value": "none for condense-only; \"auto-hide\" adds the hide-on-scroll-down pair" }
|
|
70
70
|
],
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"name": "stickyCta",
|
|
76
76
|
"status": "available",
|
|
77
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)
|
|
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
79
|
"attributes": [
|
|
80
80
|
{ "attr": "data-p60-sticky-cta", "value": "optional scroll threshold in px (default 480)" }
|
|
81
81
|
],
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"name": "lightbox",
|
|
87
87
|
"status": "available",
|
|
88
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
|
|
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
90
|
"attributes": [
|
|
91
91
|
{ "attr": "data-p60-lightbox", "value": "none; the group container" },
|
|
92
92
|
{ "attr": "data-p60-lightbox-item", "value": "none; on each anchor to a full image" },
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"name": "tabs",
|
|
101
101
|
"status": "available",
|
|
102
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
|
|
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
104
|
"attributes": [
|
|
105
105
|
{ "attr": "data-p60-tabs", "value": "none; the container" },
|
|
106
106
|
{ "attr": "data-p60-tab", "value": "the key of the panel this control activates" },
|
|
@@ -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
|
+
}
|