@port60/template-kit 0.21.0 → 1.0.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 +42 -3
- package/bin/cli.mjs +10 -0
- package/package.json +4 -2
- package/src/commands/content.mjs +4 -3
- package/src/commands/create.mjs +11 -3
- package/src/commands/dev.mjs +9 -3
- package/src/commands/model.mjs +3 -3
- package/src/commands/packageCmd.mjs +1 -1
- package/src/commands/publish.mjs +1 -1
- package/src/commands/refresh.mjs +8 -3
- package/src/commands/release.mjs +22 -0
- package/src/commands/setupPreviews.mjs +19 -0
- package/src/commands/validate.mjs +1 -1
- package/src/lib/agentsMd.mjs +108 -162
- package/src/lib/designer-bridge.js +26 -0
- package/src/lib/designerPalette.mjs +62 -0
- package/src/lib/galleryPosters.mjs +105 -0
- package/src/lib/previewOptions.mjs +1 -1
- package/src/lib/releaseBundle.mjs +141 -0
- package/src/vendor/contract/v2/behaviours.json +346 -0
- package/src/vendor/contract/v2/content-bounds.json +117 -0
- package/src/vendor/contract/v2/content-model.json +766 -0
- package/src/vendor/contract/v2/context.json +1569 -0
- package/src/vendor/contract/v2/dialect.json +105 -0
- package/src/vendor/contract/v2/fonts.json +910 -0
- package/src/vendor/contract/v2/imagery.json +32 -0
- package/src/vendor/contract/v2/islands.json +263 -0
- package/src/vendor/contract/v2/layout.json +28 -0
- package/src/vendor/contract/v2/manifest.schema.json +388 -0
- package/src/vendor/contract/v2/sections.json +947 -0
- package/src/vendor/contract/v2/site.schema.json +1353 -0
- package/src/vendor/contract/v2/tokens.json +101 -0
- package/src/vendor/contract/v2.lock.json +6931 -0
- package/src/vendor/engine/content-footprint.mjs +76 -1
- package/src/vendor/engine/majors.mjs +42 -0
- package/src/vendor/validator/gift-aid-logo.svg +6 -0
- package/src/vendor/validator/model-reference-v2.mjs +95 -0
- package/src/vendor/validator/platform-base.css +310 -0
- package/src/vendor/validator/preview-v1.mjs +691 -0
- package/src/vendor/validator/preview-v2.mjs +654 -0
- package/src/vendor/validator/preview.mjs +8 -688
- package/src/vendor/validator/site-context-v2.mjs +113 -0
- package/src/vendor/validator/validate-v1.mjs +666 -0
- package/src/vendor/validator/validate-v2.mjs +642 -0
- package/src/vendor/validator/validate.mjs +21 -663
- package/starter/assets/theme.css +3 -0
- package/starter/layout.liquid +15 -18
- package/starter/manifest.json +5 -4
- package/starter/preview/config.json +4 -0
- package/starter/preview/media/README.md +9 -0
- package/starter/sections/campaigns.liquid +5 -4
- package/starter/sections/homeHero.liquid +25 -21
- package/starter/sections/values.liquid +1 -1
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
// the production loader computes the same footprint the validator stamps, the render path
|
|
4
4
|
// fetches precisely what a template reads, and the two can never disagree.
|
|
5
5
|
import contentModel from '../contract/v1/content-model.json' with { type: 'json' };
|
|
6
|
+
import contentModelV2 from '../contract/v2/content-model.json' with { type: 'json' };
|
|
7
|
+
import siteSchemaV2 from '../contract/v2/site.schema.json' with { type: 'json' };
|
|
6
8
|
|
|
7
9
|
export { contentModel };
|
|
10
|
+
export { contentModelV2 };
|
|
8
11
|
|
|
9
12
|
// ── Extraction ─────────────────────────────────────────────────────────
|
|
10
13
|
// The dialect is closed, so the site paths a template reads are decidable from its sources: every
|
|
@@ -29,7 +32,7 @@ function segmentsOf(chain) {
|
|
|
29
32
|
* Returns { footprint: string[], minModelVersion: string|null, errors: string[] }, footprint
|
|
30
33
|
* entries are 'brand' | 'nav' | 'socials' | 'locale' | 'content.<collection>'.
|
|
31
34
|
*/
|
|
32
|
-
|
|
35
|
+
function extractV1ContentFootprint(files) {
|
|
33
36
|
const errors = [];
|
|
34
37
|
const touched = new Set();
|
|
35
38
|
const siblings = new Set(contentModel.siblings.keys);
|
|
@@ -77,3 +80,75 @@ export function extractContentFootprint(files) {
|
|
|
77
80
|
return { footprint: [...touched].sort(), minModelVersion, errors };
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
/** Metadata-only collection reads do not require fetching the collection's records. */
|
|
84
|
+
export function needsCollectionData(footprint, name) {
|
|
85
|
+
return footprint.includes(`content.${name}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function artifactFormat(files) {
|
|
89
|
+
try { return JSON.parse(files['manifest.json'] ?? '{}').format ?? 'port60-liquid@1'; }
|
|
90
|
+
catch { return 'port60-liquid@1'; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The same explicit major decision is used by validation and the public host. */
|
|
94
|
+
export function extractContentFootprint(files, options = {}) {
|
|
95
|
+
const format = options.format ?? artifactFormat(files);
|
|
96
|
+
if (format === 'port60-liquid@1') return extractV1ContentFootprint(files);
|
|
97
|
+
if (format !== 'port60-liquid@2') return { footprint: [], minModelVersion: null, errors: [`Unsupported content-model format '${format}'.`] };
|
|
98
|
+
const errors = [];
|
|
99
|
+
const touched = new Set();
|
|
100
|
+
const siblings = new Set(contentModelV2.siblings.keys);
|
|
101
|
+
for (const [path, source] of Object.entries(files)) {
|
|
102
|
+
if (!path.endsWith('.liquid')) continue;
|
|
103
|
+
const code = source.replace(/\{%[-]?\s*comment\s*[-]?%\}[\s\S]*?\{%[-]?\s*endcomment\s*[-]?%\}/g, '');
|
|
104
|
+
const bindings = new Set([...code.matchAll(/\b(?:assign|for)\s+([A-Za-z_][A-Za-z0-9_]*)\s+(?:=|in)\s+/g)].map(match => match[1]));
|
|
105
|
+
for (const token of code.matchAll(/\{(?:\{[\s\S]*?\}\}|%[\s\S]*?%\})/g)) {
|
|
106
|
+
for (const match of token[0].matchAll(/(?<![.\w])\b(brand|nav|socials|locale|worship|focus|events|infoEvents|latestArticles|campaigns|campaignsLabel|causes|services|courses|resources|locations|relatedArticles|backHref)(?=\.|\[|\s*(?:%\}|\}\}|\|))/g)) {
|
|
107
|
+
if (!bindings.has(match[1])) errors.push(`${path}: flat context '${match[1]}' is not available in v2; read the canonical site tree.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (DYNAMIC_INDEX.test(source)) errors.push(`${path}: dynamic indexing into site.* is refused; use a named path.`);
|
|
111
|
+
if (BARE_SITE.test(source)) errors.push(`${path}: aliasing the bare site tree is refused; use a named path.`);
|
|
112
|
+
for (const match of source.matchAll(new RegExp(CHAIN.source, 'g'))) {
|
|
113
|
+
const [head, member, ...rest] = segmentsOf(match[1]);
|
|
114
|
+
if (head === 'content') {
|
|
115
|
+
const collection = contentModelV2.collections[member];
|
|
116
|
+
if (!collection) {
|
|
117
|
+
errors.push(`${path}: site.content.${member ?? ''} is not in content model 2.0. Use the canonical collection or site.page.`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (collection.shape === 'array') {
|
|
121
|
+
touched.add(`content.${member}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const field = rest[0];
|
|
125
|
+
if (field && !['label', 'href', 'items', 'pagination'].includes(field)) {
|
|
126
|
+
errors.push(`${path}: site.content.${member} is an envelope with label, href, items and pagination; '${field}' is not a member.`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (field === 'items') {
|
|
130
|
+
const itemField = ['first', 'last'].includes(rest[1]) ? rest[2] : rest[1];
|
|
131
|
+
if (itemField && !['size', 'first', 'last'].includes(itemField) && !collection.item[itemField]) errors.push(`${path}: '${itemField}' is not a ${member} item field.`);
|
|
132
|
+
touched.add(`content.${member}`);
|
|
133
|
+
} else {
|
|
134
|
+
if (field === 'pagination' && rest[1] && !['page', 'size', 'totalElements', 'totalPages', 'nextHref', 'previousHref'].includes(rest[1])) errors.push(`${path}: '${rest[1]}' is not a pagination field.`);
|
|
135
|
+
touched.add(field ? `content.${member}.${field}` : `content.${member}`);
|
|
136
|
+
}
|
|
137
|
+
} else if (siblings.has(head)) {
|
|
138
|
+
const properties = siteSchemaV2.properties[head]?.properties;
|
|
139
|
+
if (member && properties && !properties[member]) errors.push(`${path}: site.${head}.${member} is not part of the v2 model.`);
|
|
140
|
+
touched.add(head);
|
|
141
|
+
} else errors.push(`${path}: site.${head} is not part of the v2 model.`);
|
|
142
|
+
}
|
|
143
|
+
// Local aliases of nav/collection objects are valid. Retired members are not.
|
|
144
|
+
const retired = [
|
|
145
|
+
[/\b(?:site\.)?nav\.(?:items|derived)\b/, 'nav.header and nav.footer replace nav.items/nav.derived'],
|
|
146
|
+
[/\b(?:site\.)?actions\.(?:primary|secondary)\b/, 'actions.header, actions.hero and actions.widget replace action aliases'],
|
|
147
|
+
[/\bsection\.(?:primary|secondary)\b/, 'use site.actions rather than section action aliases'],
|
|
148
|
+
[/\bmegaMenu\.columns\b|\.megaMenu\.columns\b/, 'the template owns responsive menu columns'],
|
|
149
|
+
[/\b(?:item|link|child|node|navItem)\.cta\b/, 'navigation CTA flags are retired; use site.actions.header']
|
|
150
|
+
];
|
|
151
|
+
for (const [pattern, advice] of retired) if (pattern.test(source)) errors.push(`${path}: ${advice}.`);
|
|
152
|
+
}
|
|
153
|
+
return { footprint: [...touched].sort(), minModelVersion: touched.size ? '2.0' : null, errors };
|
|
154
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The CONTRACT MAJORS registry (roadmap 8.6, multi-major engine support): the engine resolves a
|
|
2
|
+
// template's declared `format` to the contract set that renders it, so `port60-liquid@1` keeps
|
|
3
|
+
// rendering after `@2` ships. Today one major exists; when `@2` arrives it gets its own entry
|
|
4
|
+
// (own dialect/catalogues, own configured Liquid instance) and @1 artifacts keep resolving here
|
|
5
|
+
// untouched, with the published deprecation window governing how long.
|
|
6
|
+
import dialect from '../contract/v1/dialect.json' with { type: 'json' };
|
|
7
|
+
import sections from '../contract/v1/sections.json' with { type: 'json' };
|
|
8
|
+
import islands from '../contract/v1/islands.json' with { type: 'json' };
|
|
9
|
+
import context from '../contract/v1/context.json' with { type: 'json' };
|
|
10
|
+
import dialectV2 from '../contract/v2/dialect.json' with { type: 'json' };
|
|
11
|
+
import sectionsV2 from '../contract/v2/sections.json' with { type: 'json' };
|
|
12
|
+
import islandsV2 from '../contract/v2/islands.json' with { type: 'json' };
|
|
13
|
+
import contextV2 from '../contract/v2/context.json' with { type: 'json' };
|
|
14
|
+
|
|
15
|
+
const MAJORS = {
|
|
16
|
+
1: { format: dialect.format, dialect, sections, islands, context },
|
|
17
|
+
2: { format: dialectV2.format, dialect: dialectV2, sections: sectionsV2, islands: islandsV2, context: contextV2 }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Parse "port60-liquid@N" → N, or null when the string isn't even format-shaped. */
|
|
21
|
+
export function parseFormatMajor(format) {
|
|
22
|
+
const m = /^port60-liquid@(\d+)$/.exec(String(format ?? ''));
|
|
23
|
+
return m ? Number(m[1]) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the contract set for a declared format. Throws a NAMED error for unknown majors,
|
|
28
|
+
* callers (the artifact loader) let that ride their normal artifact-failure fallback, so a
|
|
29
|
+
* template declaring a future major degrades the site to the built-in chrome, never downs it.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveMajor(format) {
|
|
32
|
+
const major = parseFormatMajor(format);
|
|
33
|
+
const entry = major === null ? undefined : MAJORS[major];
|
|
34
|
+
if (!entry) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`unsupported template format '${format}', this engine speaks major(s) [${Object.keys(MAJORS).join(', ')}]`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return entry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SUPPORTED_MAJORS = Object.freeze(Object.keys(MAJORS).map(Number));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!-- Static preview copy of components/GiftAidLogo.tsx. Provenance: docs/living-templates/gift-aid-logo-source.md. -->
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 671.722 236.156"
|
|
3
|
+
class="donation-giftaid-logo" aria-hidden="true" focusable="false">
|
|
4
|
+
<path fill="currentColor" d="M425.721,293c18.637-29.194,9.291-33.682,8.339-33.142-18.131,10.215-31.939,40.964-30.78,51.206,1.6,14.1,12.543-2.569,22.441-18.064m70.606-26.791a53.62,53.62,0,0,0-6.419,19.5c-.477,3.424-1.033,18.3-.91,20.322a70.845,70.845,0,0,0,1.253,9.775c.443,1.611,3.206,7.4-5.661,8.623-5.218.718-5.116-12.339-5.218-14.476-.2-4.554.555-20.554,1.038-22.929.51-2.429-.507.145-1.467,1.735-2.211,3.7-4.784,8.706-7.865,13.635-4.047,6.481-13.073,21.919-25.383,21-4.605-.348-9.289-.941-11.965-13.775-1.026-4.919-.628-11.386-.523-15.268-.128-.8-.019-1.717-.688-1.343C428.159,303.18,414,333.158,398,326.419c-7.41-3.112-11.061-12.678-2.881-31.358,7.969-18.206,28.071-40.784,36.078-42.3,7.137-1.355,7.879,2.24,7.879,2.24.651-1.327,5.1-1.171,6.073.732,1.3,2.545,2.168,2.986,1.3,9.7-.491,3.872-4.088,28.092-3.467,36.414.286,3.9.955,13.509,4.919,13.972,6.924.829,15.315-13.9,16.268-15.439,1.865-3.009,16.805-28.056,18.644-31.713,1.979-3.943,3.23-6.822,4.507-9.332s5.126-2.789,8.481-.5S497.476,264.27,496.327,266.211ZM180.189,278.478c-8.784.946-24.8,22.18-28.1,29.721-3.374,7.7-4.592,15.123-.588,18.22,8.285,6.433,16.856-4.472,19.375-7.007,1.559-1.56,12.459-13.45,15.2-25.526C187.888,285.912,188.727,277.564,180.189,278.478Zm4.578,94.246q-.909,1.625-1.869,3.221c-6.472,10.744-14.962,20.331-25.489,27.241-14.258,9.356-27.649,12.95-42.611,12.77-8.7-.1-17.09-1.509-24.244-6.059-5.864-3.73-6.7-9.562-4.924-13.412,2.469-5.363,10.176-4.841,13.455-1.763,8.187,7.677,13.074,10.654,23.6,10.144a56.707,56.707,0,0,0,24.69-7.26c43.542-25.427,43.144-71.137,44.912-98.68.187-1.675-.723-.457-1.031.455a108.567,108.567,0,0,1-7.775,16.943,58.19,58.19,0,0,1-18.424,20.1c-5.514,3.642-14.469,5.405-20.775,1.294-2.286-1.5-15.654-9.325-4.324-32.638,7.125-14.668,22.118-24.843,25.5-27,8.27-5.282,14.18-7.794,20.377-7.318,3.156.23,3.611,1.472,5.957,1.41,1.336-1.568,2.073-2.72,4.27-3.327,2.95-.471,6.453,2,7.8,6.683s-2.161,12.206-3.047,15.824C200.822,291.35,203.938,338.277,184.767,372.724Zm49.607-105.731c-5.337.1-4.9,3.952-6.019,7.322-1.083,3.36-10.28,33.575-11.448,38.992s-1.969,10.282-.28,17.716c1.718,7.6,10.429,5.379,11.353,4.045,4.256-5.96.486-7.716.349-11.879-.109-4.351,1.082-8.915,1.981-14.806.32-2.1.962-11.649,1.277-12.945.355-1.4,3.36-12.033,7.689-18.5C241.129,274.166,242.76,266.848,234.374,266.993ZM248.9,231.656c-.73-1.5-4.7-6.225-9.882-2.557-7.708,5.458,1.18,13,2.607,16.7s9.132,6.8,8.007-.5C249.468,244.249,251.02,236,248.9,231.656Zm85.108,35.59c-2.509.559-7.516,1.569-10.666,2.1-4.77.8-7.369,3.8-7.189,6.832.18,3.054,1.225,4.493,5.481,3.685,3.848-.728,7.132-4,10.5-4.014,0,0-6.954,29.319-7.409,33.248s-2.512,14.678,1.713,20.777c9.4,13.576,28.2-1.985,29.957-3.68,1.291-1.253,13.168-11.668,17.194-20.021,2.04-4.239-1.433-5.175-3.784-6.04s-7.318.63-8.012,3.73c-1.512,6.734-17.96,24.9-26.1,21.258-9.594-4.3,7.713-52.479,7.713-52.479,2.064-.926,12.517-3.62,22.413-6.165,11.417-2.925,46.312-9.815,38.436-20.24-3.854-5.1-13.816,2.666-17.192,4.292-12.095,5.8-41.7,14.315-41.7,14.315s3.844-17.32,5.088-20.732c1.429-3.905,6.7-6.46,7.467-13.412.905-8.086-8.835-11.768-13.484-2.915-.962,1.817-2.287,6.832-2.417,6.957-1.249,1.2-12,5.493-22.571,10.533-3.332,1.588-22.256,8.469-22.291,8.424.026-.144,5.746-18.281,12.374-33.412,4.41-10.061,10.623-24.066,21.63-26.909,10.308-2.657,5.448,8.08,10.086,10.871,5.883,3.538,9.492-.924,10.393-5.191.706-3.342.519-20.879-17.31-19.126-16.965,1.673-30.484,27.571-47.2,75.964-.18.194-13.457,8.77-24.464,10.974-5.443,1.088-7.851,1.559-8.571,3.5a8.571,8.571,0,0,0,2,9.168c3.137,3.744,27.646-12.479,27.62-12.367-.5.661-20.609,77.281-20.77,78.175-1.159,6.381-7.642,23.206-7.291,30.843.194,3.929,1.734,12.107,7.391,11.742,11.846.777,9.561-15.865,8.765-18.917-1.659-6.313,6.851-48.045,14.488-78.8,1.557-6.273,4.756-16.566,5.313-19.014a11.455,11.455,0,0,1,6.22-8.233s42.583-20.073,42.979-19.957C340.811,243.054,334.008,267.246,334.008,267.246Zm291.747,38.3C634,313.343,642.2,308.4,646.421,305c12.945-10.41,39.635-57.853,39.635-57.853.152,1.588-11.121,56,3.95,62.033,9.664,3.86,32.156-15.33,34.732-17.107,14.851-10.251,5.751-14,2.886-14-7.658-.024-6.279,5.521-8.374,8.011-3.455,4.114-17.341,18.4-24.14,14.607-3.661-2.052-2.758-14.14-2.393-16.926,2.14-28.8,2.711-34,4.389-40.624.94-3.682,5.426-3.585,8.173-4.089,37.08-6.851,43.732-4.088,48.355-6.342,3.67-1.805,4.327-9.575-.879-11.246-2.382-.749-5.4,1.064-10.858,3.1-5.431,2.007-32.791,8.057-36.815,8.654-3.706.536-4.5.014-5.4-1.147-.886-1.149.82-14.663,6.135-25.865,2.313-4.848,2.374-12.267-5.178-12.841-3.106-.223-5.744.661-6.924,3.308-1.175,2.63-3.438,26.63-3.967,28.228-.535,1.578-3.4,2.81-4.033,5.549s-.192,4.448-2.035,5.064c-1.863.633-6.965,1.123-9.064,1.628-2.209.538-3.519,4.763-3.6,7.206s3.86.991,5.761.714c2.17-.316,7.476-2.967,3.7,1.578-1.431,1.737-27.17,41.973-37.222,50.258-11.566,9.521-10.739-9.2-10.45-12.389,1.748-19.092,10.1-31.9,11.3-34.94,1.18-3.055,1.677-12.107-8.183-10.107a6.979,6.979,0,0,0-4.938,4.637.992.992,0,0,0-.029.2C629.964,253.222,612.841,293.331,625.755,305.549Zm15.029-93.416c3.038,5.924,8.229,3.829,9.113,2.3.887-1.517,1.595-1.832,4.112-2.846,4.621-1.846-3.358-10.14-10.948-7.8a2.9,2.9,0,0,0-1.737,2.466C641.53,207.986,639.75,210.149,640.784,212.133ZM547.7,258.685s4.981-4.292,7.09-4.181c.979.048-.728,2.759,2.694,4.434,1.818.9-1.988,13.007-9.512,24.628-17.279,26.682-25.471,25.478-26.182,17.374C520.329,284.033,542.689,262.128,547.7,258.685Zm8.772,21.876s-.064,5.749.384,10.664c.4,4.412,1.908,13.644,3.462,16.63,2.408,4.6,7.514,4.6,11.446,2.346,5.147-2.957,2.713-8.488.753-10.329-1.711-1.621-5.869-3.7-7.128-10.645-.741-4.111-.135-22.637,1.083-29.118a86.628,86.628,0,0,1,5.621-17.3c2.05-4.045,14.4-27.206,14.581-34.981.075-3.393-3.42-6.206-9.131-5.618s-7.132,5.161-7.142,8.159c-.014,2.981-6.061,23.973-6.374,24.928-.5,1.434-1.491,10.029-4.441,10.048a28.671,28.671,0,0,0-18.159,6.078c-2.846,2.156-12.509,9.673-18.087,17.516-2.531,3.55-14.038,18.342-12.737,36,1.185,16.056,12.419,13.215,15.642,12.7,14.661-2.355,24.969-26.469,26.462-29.426C555.348,282.964,554.455,284.376,556.471,280.561ZM498.716,221.9c-1.074,1.32-.806,5.509,1.533,7.675,1.761,1.633,2.007,1.934,2.064,3.254.1,2.434,1.654,2.569,4.047,2.535,2.927-.042,5.32-2.073,5.616-6.206s-3.708-7.668-6.931-8.464S500.109,220.173,498.716,221.9Z"
|
|
5
|
+
transform="translate(-84.802 -179.806)" />
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// THE MODEL, VISIBLE (content model v2): a self-contained reference page the kit's dev server
|
|
2
|
+
// serves at /model, the registry's shape beside the LIVE example data the preview is rendering
|
|
3
|
+
// right now (preview-content.json overlay included), so "what can I read?" is answered where the
|
|
4
|
+
// developer already lives. Network-dead like every preview document.
|
|
5
|
+
import { contentModelV2 as contentModel } from '../engine/content-footprint.mjs';
|
|
6
|
+
|
|
7
|
+
const escapeHtml = (s) =>
|
|
8
|
+
String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9
|
+
|
|
10
|
+
function fieldRows(item) {
|
|
11
|
+
return Object.entries(item)
|
|
12
|
+
.map(([name, spec]) => {
|
|
13
|
+
const notes = [];
|
|
14
|
+
if (spec.nullable) notes.push('nullable, always branch');
|
|
15
|
+
if (spec.enumOpen) {
|
|
16
|
+
notes.push(spec.enumOpen.length
|
|
17
|
+
? `open enum: ${spec.enumOpen.join(', ')} today, more will arrive`
|
|
18
|
+
: 'open enum');
|
|
19
|
+
}
|
|
20
|
+
if (spec.max) notes.push(`max ${spec.max} chars`);
|
|
21
|
+
return `<tr><td><code>${escapeHtml(name)}</code></td><td>${escapeHtml(spec.type)}</td>` +
|
|
22
|
+
`<td>${escapeHtml(notes.join(' · '))}</td><td>${escapeHtml(spec.since)}</td></tr>`;
|
|
23
|
+
})
|
|
24
|
+
.join('');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exampleOf(site, name) {
|
|
28
|
+
const value = site?.content?.[name];
|
|
29
|
+
const items = Array.isArray(value) ? value : value?.items;
|
|
30
|
+
if (!Array.isArray(items) || items.length === 0) return null;
|
|
31
|
+
return JSON.stringify(items[0], null, 2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The /model page: registry + the live example values of THIS preview's site tree. */
|
|
35
|
+
export function renderModelReferenceHtml(site) {
|
|
36
|
+
const sectionsHtml = Object.entries(contentModel.collections)
|
|
37
|
+
.map(([name, model]) => {
|
|
38
|
+
const example = exampleOf(site, name);
|
|
39
|
+
const envelope = model.shape === "envelope" ? `<p>Envelope: <code>label</code>, nullable <code>href</code>, <code>items</code>, nullable <code>pagination</code>. Iterate <code>site.content.${name}.items</code>. Metadata reads do not fetch rows.</p>` : "<p>A bounded array.</p>";
|
|
40
|
+
return `<section id="${escapeHtml(name)}">
|
|
41
|
+
<h2><code>site.content.${escapeHtml(name)}</code></h2>
|
|
42
|
+
<p class="meta">bounded at <strong>${model.cap}</strong>${model.moreHref ? ` · more at <code>${escapeHtml(model.moreHref)}</code>` : ''} · since ${escapeHtml(model.since)}</p>
|
|
43
|
+
<p>${escapeHtml(model.description)}</p>
|
|
44
|
+
${envelope}
|
|
45
|
+
<table><thead><tr><th>Field</th><th>Type</th><th>Notes</th><th>Since</th></tr></thead>
|
|
46
|
+
<tbody>${fieldRows(model.item)}</tbody></table>
|
|
47
|
+
${example ? `<details><summary>Live example (item 0 of what this preview is rendering)</summary><pre>${escapeHtml(example)}</pre></details>` : '<p class="meta">No items in the current preview data.</p>'}
|
|
48
|
+
</section>`;
|
|
49
|
+
})
|
|
50
|
+
.join('\n');
|
|
51
|
+
|
|
52
|
+
const toc = Object.keys(contentModel.collections)
|
|
53
|
+
.map((name) => `<a href="#${escapeHtml(name)}">${escapeHtml(name)}</a>`)
|
|
54
|
+
.join(' · ');
|
|
55
|
+
|
|
56
|
+
return `<!doctype html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="utf-8">
|
|
60
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
|
|
61
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
62
|
+
<title>site.content, the content model</title>
|
|
63
|
+
<style>
|
|
64
|
+
body { font: 15px/1.6 system-ui, sans-serif; margin: 0 auto; max-width: 56rem; padding: 24px 20px 64px; color: #172029; background: #fbfbf9; }
|
|
65
|
+
h1 { margin: 0 0 4px; } h2 { margin: 40px 0 2px; font-size: 20px; }
|
|
66
|
+
.meta { color: #5a6672; font-size: 13px; margin: 2px 0 8px; }
|
|
67
|
+
.lede { color: #3c4854; max-width: 46rem; }
|
|
68
|
+
nav.toc { font-size: 13px; margin: 14px 0 6px; color: #5a6672; }
|
|
69
|
+
table { border-collapse: collapse; width: 100%; font-size: 13.5px; }
|
|
70
|
+
th, td { text-align: left; border-bottom: 1px solid #e3e3dc; padding: 5px 10px 5px 0; vertical-align: top; }
|
|
71
|
+
th { font-size: 11px; letter-spacing: .05em; text-transform: uppercase; color: #5a6672; }
|
|
72
|
+
code { background: #eeeee6; border-radius: 4px; padding: 1px 5px; font-size: 12.5px; }
|
|
73
|
+
pre { background: #10161c; color: #dbe4ec; border-radius: 8px; padding: 12px 14px; overflow-x: auto; font-size: 12.5px; }
|
|
74
|
+
details { margin: 8px 0 0; } summary { cursor: pointer; font-size: 13px; color: #5a6672; }
|
|
75
|
+
.rules { background: #f0f0e8; border-radius: 10px; padding: 14px 18px; font-size: 13.5px; }
|
|
76
|
+
.back { font-size: 13px; }
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<p class="back"><a href="/">← back to the preview</a></p>
|
|
81
|
+
<h1>The content model</h1>
|
|
82
|
+
<p class="lede">Everything your template reads lives on one tree: <code>site.brand</code>, <code>site.nav</code>, <code>site.socials</code>, <code>site.locale</code>, <code>site.actions</code>, <code>site.page</code> and the typed collections below. Model version <strong>${escapeHtml(contentModel.version)}</strong>. The validator computes your content footprint from the paths you read and stamps the minimum model version at publish, you never declare versions.</p>
|
|
83
|
+
<div class="rules">
|
|
84
|
+
<strong>Four rules the model never breaks (so neither should you):</strong>
|
|
85
|
+
every collection is bounded (cap + supplied <code>href</code> and <code>pagination</code>), link onward, never assume you have everything;
|
|
86
|
+
enum fields are open, branch on the values you style, fall back for the rest;
|
|
87
|
+
optional fields are explicitly nullable, always branch;
|
|
88
|
+
the model only grows, nothing you read today is ever removed within the major.
|
|
89
|
+
Replace the preview data with your own via <code>preview-content.json</code> (shape fixed, hot-reloaded, never packaged).
|
|
90
|
+
</div>
|
|
91
|
+
<nav class="toc">${toc}</nav>
|
|
92
|
+
${sectionsHtml}
|
|
93
|
+
</body>
|
|
94
|
+
</html>`;
|
|
95
|
+
}
|
|
@@ -1403,6 +1403,232 @@ textarea.p60-form-control {
|
|
|
1403
1403
|
color: var(--primary-strong);
|
|
1404
1404
|
}
|
|
1405
1405
|
|
|
1406
|
+
/* The donation island can sit in a narrow desktop column or a wide checkout.
|
|
1407
|
+
Size its fields from their available space, without changing other islands. */
|
|
1408
|
+
.donate-card.donation-widget {
|
|
1409
|
+
padding: 1.25rem;
|
|
1410
|
+
gap: 0.75rem;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.donation-widget .donor-fields {
|
|
1414
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.donation-widget .giftaid-grid {
|
|
1418
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.donation-widget .donor-fields > .marketing-optin,
|
|
1422
|
+
.donation-widget .giftaid-name-grid > .giftaid-title {
|
|
1423
|
+
grid-column: 1 / -1;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.donation-widget .giftaid-title {
|
|
1427
|
+
inline-size: min(100%, 10rem);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
.donation-widget .field,
|
|
1431
|
+
.donation-widget .field input,
|
|
1432
|
+
.donation-widget .field select,
|
|
1433
|
+
.donation-widget .freq-tab {
|
|
1434
|
+
min-inline-size: 0;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
.donation-widget .field select {
|
|
1438
|
+
inline-size: 100%;
|
|
1439
|
+
border: 1px solid var(--line);
|
|
1440
|
+
border-radius: 12px;
|
|
1441
|
+
padding: 0.85rem 0.95rem;
|
|
1442
|
+
background: var(--surface);
|
|
1443
|
+
color: var(--ink);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
.donation-widget .field select:focus-visible {
|
|
1447
|
+
outline: 2px solid var(--primary);
|
|
1448
|
+
outline-offset: 2px;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.donation-widget .freq-tabs {
|
|
1452
|
+
grid-template-columns: none;
|
|
1453
|
+
grid-auto-flow: column;
|
|
1454
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.donation-widget .freq-tab {
|
|
1458
|
+
overflow-wrap: anywhere;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
.donation-widget .donate-change {
|
|
1462
|
+
white-space: normal;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
.donation-widget .donation-giftaid-controls {
|
|
1466
|
+
display: grid;
|
|
1467
|
+
gap: 0.5rem;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
.donation-widget .donation-giftaid-summary {
|
|
1471
|
+
margin: 0;
|
|
1472
|
+
color: var(--ink);
|
|
1473
|
+
font-size: 0.9rem;
|
|
1474
|
+
line-height: 1.5;
|
|
1475
|
+
font-weight: 600;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.donation-widget .donation-giftaid-summary-actions {
|
|
1479
|
+
display: flex;
|
|
1480
|
+
flex-wrap: wrap;
|
|
1481
|
+
align-items: center;
|
|
1482
|
+
gap: 0.75rem;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/* One button surrounds the action and intact wordmark, preserving its clear space. */
|
|
1486
|
+
.donation-widget .donation-giftaid-trigger {
|
|
1487
|
+
display: inline-flex;
|
|
1488
|
+
align-items: center;
|
|
1489
|
+
justify-content: center;
|
|
1490
|
+
gap: 15px;
|
|
1491
|
+
min-block-size: 44px;
|
|
1492
|
+
padding: 15px;
|
|
1493
|
+
border: 1px solid color-mix(in srgb, var(--ink) 45%, transparent);
|
|
1494
|
+
border-radius: var(--radius);
|
|
1495
|
+
background: transparent;
|
|
1496
|
+
color: var(--ink);
|
|
1497
|
+
font: inherit;
|
|
1498
|
+
font-weight: 600;
|
|
1499
|
+
cursor: pointer;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.donation-widget .donation-giftaid-logo {
|
|
1503
|
+
display: block;
|
|
1504
|
+
flex: 0 0 auto;
|
|
1505
|
+
inline-size: 72px;
|
|
1506
|
+
block-size: auto;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.donation-widget .donation-giftaid-action {
|
|
1510
|
+
text-decoration: none;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.donation-widget .donation-giftaid-trigger:hover:not(:disabled) {
|
|
1514
|
+
border-color: var(--ink);
|
|
1515
|
+
background: color-mix(in srgb, var(--ink) 5%, transparent);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
.donation-widget .donation-giftaid-trigger:focus-visible {
|
|
1519
|
+
outline: 2px solid var(--ink);
|
|
1520
|
+
outline-offset: 2px;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
.donation-widget .donation-giftaid-trigger:disabled {
|
|
1524
|
+
opacity: 0.6;
|
|
1525
|
+
cursor: not-allowed;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/* Native top layer keeps the optional details clear of template hero clipping and motion. */
|
|
1529
|
+
.donation-widget .donation-giftaid-dialog {
|
|
1530
|
+
position: fixed;
|
|
1531
|
+
inset: 0;
|
|
1532
|
+
margin: auto;
|
|
1533
|
+
inline-size: min(42rem, calc(100% - 2rem));
|
|
1534
|
+
max-inline-size: none;
|
|
1535
|
+
max-block-size: calc(100dvh - 2rem);
|
|
1536
|
+
padding: 0;
|
|
1537
|
+
border: 1px solid var(--line);
|
|
1538
|
+
border-radius: var(--radius);
|
|
1539
|
+
background: var(--surface);
|
|
1540
|
+
color: var(--ink);
|
|
1541
|
+
font: inherit;
|
|
1542
|
+
text-align: start;
|
|
1543
|
+
box-shadow: var(--shadow);
|
|
1544
|
+
overflow: hidden;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.donation-widget .donation-giftaid-dialog:not([open]) { display: none; }
|
|
1548
|
+
.donation-widget .donation-giftaid-dialog::backdrop { background: rgb(0 0 0 / 0.5); }
|
|
1549
|
+
html:has(.donation-giftaid-dialog[open]) { overflow: hidden; }
|
|
1550
|
+
|
|
1551
|
+
.donation-widget .donation-giftaid-dialog > form {
|
|
1552
|
+
display: grid;
|
|
1553
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
1554
|
+
max-block-size: calc(100dvh - 2rem);
|
|
1555
|
+
margin: 0;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.donation-widget .donation-giftaid-heading {
|
|
1559
|
+
display: flex;
|
|
1560
|
+
align-items: flex-start;
|
|
1561
|
+
justify-content: space-between;
|
|
1562
|
+
gap: 1rem;
|
|
1563
|
+
padding: 1.5rem;
|
|
1564
|
+
border-block-end: 1px solid var(--line);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.donation-widget .donation-giftaid-heading h2 {
|
|
1568
|
+
font-size: clamp(1.25rem, 3vw, 1.6rem);
|
|
1569
|
+
line-height: 1.2;
|
|
1570
|
+
margin: 0;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.donation-widget .donation-giftaid-heading p {
|
|
1574
|
+
color: var(--muted);
|
|
1575
|
+
margin: 0.5rem 0 0;
|
|
1576
|
+
font-size: 0.9rem;
|
|
1577
|
+
line-height: 1.5;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.donation-widget .donation-giftaid-close {
|
|
1581
|
+
display: grid;
|
|
1582
|
+
place-items: center;
|
|
1583
|
+
flex: none;
|
|
1584
|
+
inline-size: 2.75rem;
|
|
1585
|
+
block-size: 2.75rem;
|
|
1586
|
+
padding: 0;
|
|
1587
|
+
border: 1px solid var(--line-strong);
|
|
1588
|
+
border-radius: 50%;
|
|
1589
|
+
background: var(--surface);
|
|
1590
|
+
color: var(--ink);
|
|
1591
|
+
font: inherit;
|
|
1592
|
+
font-size: 1.5rem;
|
|
1593
|
+
cursor: pointer;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
.donation-widget .donation-giftaid-body {
|
|
1597
|
+
display: grid;
|
|
1598
|
+
gap: 1.25rem;
|
|
1599
|
+
padding: 1.5rem;
|
|
1600
|
+
min-block-size: 0;
|
|
1601
|
+
overflow-y: auto;
|
|
1602
|
+
overscroll-behavior: contain;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
.donation-widget .donation-giftaid-dialog .giftaid-declaration { font-size: 0.875rem; line-height: 1.6; }
|
|
1606
|
+
|
|
1607
|
+
.donation-widget .donation-giftaid-actions {
|
|
1608
|
+
display: flex;
|
|
1609
|
+
flex-wrap: wrap;
|
|
1610
|
+
justify-content: flex-end;
|
|
1611
|
+
gap: 0.75rem;
|
|
1612
|
+
padding: 1rem 1.5rem;
|
|
1613
|
+
border-block-start: 1px solid var(--line);
|
|
1614
|
+
background: var(--surface);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.donation-widget .donation-giftaid-actions .button { min-block-size: 2.75rem; }
|
|
1618
|
+
.donation-widget .donation-giftaid-dialog :focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
|
1619
|
+
|
|
1620
|
+
@media (max-width: 480px) {
|
|
1621
|
+
.donation-widget .donation-giftaid-dialog {
|
|
1622
|
+
inline-size: calc(100% - 1rem);
|
|
1623
|
+
max-block-size: calc(100dvh - 1rem);
|
|
1624
|
+
}
|
|
1625
|
+
.donation-widget .donation-giftaid-dialog > form { max-block-size: calc(100dvh - 1rem); }
|
|
1626
|
+
.donation-widget .donation-giftaid-heading,
|
|
1627
|
+
.donation-widget .donation-giftaid-body,
|
|
1628
|
+
.donation-widget .donation-giftaid-actions { padding: 1rem; }
|
|
1629
|
+
.donation-widget .donation-giftaid-actions .button { flex: 1; }
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1406
1632
|
.card-host {
|
|
1407
1633
|
min-height: 2.5rem;
|
|
1408
1634
|
margin: 0.25rem 0 0.5rem;
|
|
@@ -1623,6 +1849,49 @@ textarea.p60-form-control {
|
|
|
1623
1849
|
color: var(--ink);
|
|
1624
1850
|
margin: 0 0 1rem;
|
|
1625
1851
|
}
|
|
1852
|
+
/* Side by side content from the rich-text editor (articles, events, courses). Structure only, and
|
|
1853
|
+
deliberately unscoped so a template page that renders a body with the raw filter gets the same
|
|
1854
|
+
floor: the theme owns the typography inside a pane and may restyle the gap, the breakpoint or the
|
|
1855
|
+
boxed look. Under 720px of page width the panes stack in reading order. Nothing about width or
|
|
1856
|
+
colour is stored with the content; the classes are exact tokens the sanitiser allow-lists. */
|
|
1857
|
+
.p60-cols {
|
|
1858
|
+
display: grid;
|
|
1859
|
+
gap: 1.5rem;
|
|
1860
|
+
margin: 1.25rem 0;
|
|
1861
|
+
align-items: start;
|
|
1862
|
+
}
|
|
1863
|
+
.p60-cols-2 {
|
|
1864
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1865
|
+
}
|
|
1866
|
+
.p60-cols-3 {
|
|
1867
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1868
|
+
}
|
|
1869
|
+
.p60-col > :first-child {
|
|
1870
|
+
margin-top: 0;
|
|
1871
|
+
}
|
|
1872
|
+
.p60-col > :last-child {
|
|
1873
|
+
margin-bottom: 0;
|
|
1874
|
+
}
|
|
1875
|
+
/* A floated image never escapes its pane. */
|
|
1876
|
+
.p60-col img,
|
|
1877
|
+
.p60-col figure {
|
|
1878
|
+
float: none;
|
|
1879
|
+
max-width: 100%;
|
|
1880
|
+
}
|
|
1881
|
+
/* The one named look: a box drawn from the theme's own tokens. */
|
|
1882
|
+
.p60-col-boxed {
|
|
1883
|
+
border: 1px solid var(--line);
|
|
1884
|
+
border-radius: var(--radius);
|
|
1885
|
+
background: var(--bg-tint);
|
|
1886
|
+
padding: 1rem 1.15rem;
|
|
1887
|
+
}
|
|
1888
|
+
@media (max-width: 720px) {
|
|
1889
|
+
.p60-cols {
|
|
1890
|
+
grid-template-columns: 1fr;
|
|
1891
|
+
gap: 1rem;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1626
1895
|
.service-body {
|
|
1627
1896
|
color: var(--ink);
|
|
1628
1897
|
line-height: 1.7;
|
|
@@ -4164,6 +4433,47 @@ textarea.p60-form-control {
|
|
|
4164
4433
|
}
|
|
4165
4434
|
.member-course-feedback-toggle:hover { opacity: 1; text-decoration: underline; }
|
|
4166
4435
|
.member-course-feedback-body { margin-top: 0.6rem; display: grid; gap: 0.75rem; }
|
|
4436
|
+
/* The course's structure as a family reads it: parts in order, where the class is, whether their
|
|
4437
|
+
learner was there, and the marks the course lets through. Indentation is the only thing that
|
|
4438
|
+
says which level a part sits at, because the composer named the levels. */
|
|
4439
|
+
.member-course-structure { margin-top: 0.6rem; display: grid; gap: 0.4rem; }
|
|
4440
|
+
.member-course-structure-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.2rem; }
|
|
4441
|
+
.member-course-structure-node { display: flex; justify-content: space-between; gap: 0.75rem; padding: 0.15rem 0; }
|
|
4442
|
+
.member-course-structure-node.is-heading .member-course-structure-name { font-weight: 600; }
|
|
4443
|
+
.member-course-structure-node.is-leaf:not(.is-covered) { opacity: 0.6; }
|
|
4444
|
+
.member-course-structure-node.is-current .member-course-structure-name { font-weight: 600; }
|
|
4445
|
+
.member-course-structure-now { font-style: normal; font-size: 0.8em; margin-inline-start: 0.4rem; padding: 0 0.4rem; border-radius: 999px; background: color-mix(in srgb, currentColor 12%, transparent); }
|
|
4446
|
+
.member-course-structure-state { display: inline-flex; gap: 0.5rem; align-items: baseline; white-space: nowrap; font-size: 0.9em; }
|
|
4447
|
+
.member-course-structure-mark { font-style: normal; font-weight: 600; }
|
|
4448
|
+
.member-course-structure-completion { margin: 0; font-weight: 600; }
|
|
4449
|
+
/* The progress meter: one segment per module, one cell per unit. Fills are the theme's primary so
|
|
4450
|
+
every template recolours it; every state also carries a word, a tick or a hatch, never colour
|
|
4451
|
+
alone. A 2px surface gap separates fills, and the current unit is lit rather than recoloured. */
|
|
4452
|
+
.member-course-meter { display: grid; gap: 0.75rem; padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: 0.75rem; background: color-mix(in srgb, var(--primary) 6%, transparent); }
|
|
4453
|
+
.member-course-meter-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; gap: 0.5rem 1.25rem; }
|
|
4454
|
+
.member-course-meter-hero { display: flex; align-items: baseline; gap: 0.6rem; }
|
|
4455
|
+
.member-course-meter-number { font-size: 2.1rem; font-weight: 800; line-height: 1; letter-spacing: -0.02em; color: var(--primary); }
|
|
4456
|
+
.member-course-meter-caption { font-size: 0.9rem; color: var(--muted); }
|
|
4457
|
+
.member-course-meter-now { display: grid; gap: 0.15rem; font-size: 0.9rem; font-weight: 600; text-align: end; }
|
|
4458
|
+
.member-course-meter-presence { font-weight: 400; color: var(--muted); }
|
|
4459
|
+
.member-course-meter-bar { display: flex; gap: 0.5rem; align-items: flex-start; }
|
|
4460
|
+
.member-course-meter-seg { display: grid; gap: 0.4rem; min-inline-size: 0; }
|
|
4461
|
+
.member-course-meter-cells { display: flex; gap: 2px; }
|
|
4462
|
+
.member-course-meter-cell { flex: 1 1 0; display: inline-block; min-inline-size: 0.6rem; block-size: 0.85rem; border-radius: 4px; background: color-mix(in srgb, currentColor 14%, transparent); }
|
|
4463
|
+
.member-course-meter-cell.is-covered { background: var(--primary); }
|
|
4464
|
+
.member-course-meter-cell.is-covered.is-away { background: repeating-linear-gradient(135deg, var(--primary) 0 3px, color-mix(in srgb, var(--primary) 35%, transparent) 3px 6px); }
|
|
4465
|
+
.member-course-meter-cell.is-current { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--primary); animation: member-course-meter-pulse 2.4s ease-in-out infinite; }
|
|
4466
|
+
@keyframes member-course-meter-pulse { 0%, 100% { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--primary); } 50% { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px color-mix(in srgb, var(--primary) 35%, transparent); } }
|
|
4467
|
+
@media (prefers-reduced-motion: reduce) { .member-course-meter-cell.is-current { animation: none; } }
|
|
4468
|
+
.member-course-meter-seg-label { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.5rem; font-size: 0.8rem; min-inline-size: 0; }
|
|
4469
|
+
.member-course-meter-seg-name { font-weight: 600; overflow-wrap: anywhere; }
|
|
4470
|
+
.member-course-meter-legend { display: flex; flex-wrap: wrap; gap: 0.35rem 1rem; font-size: 0.75rem; color: var(--muted); }
|
|
4471
|
+
.member-course-meter-legend i { display: inline-block; inline-size: 0.9rem; block-size: 0.6rem; vertical-align: middle; margin-inline-end: 0.15rem; }
|
|
4472
|
+
.member-course-meter-legend i.is-current { animation: none; }
|
|
4473
|
+
.member-course-structure-details summary { cursor: pointer; }
|
|
4474
|
+
.member-course-mini { display: block; inline-size: min(14rem, 100%); block-size: 4px; margin-block-start: 0.35rem; border-radius: 999px; background: color-mix(in srgb, currentColor 14%, transparent); overflow: hidden; }
|
|
4475
|
+
.member-course-mini-fill { display: block; block-size: 100%; background: var(--primary); border-radius: inherit; }
|
|
4476
|
+
@media (max-width: 560px) { .member-course-meter-bar { flex-direction: column; gap: 0.6rem; } .member-course-meter-now { text-align: start; } }
|
|
4167
4477
|
.member-course-feedback-note {
|
|
4168
4478
|
border-left: 3px solid currentColor;
|
|
4169
4479
|
padding: 0.35rem 0 0.35rem 0.75rem;
|