@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.
Files changed (53) hide show
  1. package/README.md +42 -3
  2. package/bin/cli.mjs +10 -0
  3. package/package.json +4 -2
  4. package/src/commands/content.mjs +4 -3
  5. package/src/commands/create.mjs +11 -3
  6. package/src/commands/dev.mjs +9 -3
  7. package/src/commands/model.mjs +3 -3
  8. package/src/commands/packageCmd.mjs +1 -1
  9. package/src/commands/publish.mjs +1 -1
  10. package/src/commands/refresh.mjs +8 -3
  11. package/src/commands/release.mjs +22 -0
  12. package/src/commands/setupPreviews.mjs +19 -0
  13. package/src/commands/validate.mjs +1 -1
  14. package/src/lib/agentsMd.mjs +108 -162
  15. package/src/lib/designer-bridge.js +26 -0
  16. package/src/lib/designerPalette.mjs +62 -0
  17. package/src/lib/galleryPosters.mjs +105 -0
  18. package/src/lib/previewOptions.mjs +1 -1
  19. package/src/lib/releaseBundle.mjs +141 -0
  20. package/src/vendor/contract/v2/behaviours.json +346 -0
  21. package/src/vendor/contract/v2/content-bounds.json +117 -0
  22. package/src/vendor/contract/v2/content-model.json +766 -0
  23. package/src/vendor/contract/v2/context.json +1569 -0
  24. package/src/vendor/contract/v2/dialect.json +105 -0
  25. package/src/vendor/contract/v2/fonts.json +910 -0
  26. package/src/vendor/contract/v2/imagery.json +32 -0
  27. package/src/vendor/contract/v2/islands.json +263 -0
  28. package/src/vendor/contract/v2/layout.json +28 -0
  29. package/src/vendor/contract/v2/manifest.schema.json +388 -0
  30. package/src/vendor/contract/v2/sections.json +947 -0
  31. package/src/vendor/contract/v2/site.schema.json +1353 -0
  32. package/src/vendor/contract/v2/tokens.json +101 -0
  33. package/src/vendor/contract/v2.lock.json +6931 -0
  34. package/src/vendor/engine/content-footprint.mjs +76 -1
  35. package/src/vendor/engine/majors.mjs +42 -0
  36. package/src/vendor/validator/gift-aid-logo.svg +6 -0
  37. package/src/vendor/validator/model-reference-v2.mjs +95 -0
  38. package/src/vendor/validator/platform-base.css +310 -0
  39. package/src/vendor/validator/preview-v1.mjs +691 -0
  40. package/src/vendor/validator/preview-v2.mjs +654 -0
  41. package/src/vendor/validator/preview.mjs +8 -688
  42. package/src/vendor/validator/site-context-v2.mjs +113 -0
  43. package/src/vendor/validator/validate-v1.mjs +666 -0
  44. package/src/vendor/validator/validate-v2.mjs +642 -0
  45. package/src/vendor/validator/validate.mjs +21 -663
  46. package/starter/assets/theme.css +3 -0
  47. package/starter/layout.liquid +15 -18
  48. package/starter/manifest.json +5 -4
  49. package/starter/preview/config.json +4 -0
  50. package/starter/preview/media/README.md +9 -0
  51. package/starter/sections/campaigns.liquid +5 -4
  52. package/starter/sections/homeHero.liquid +25 -21
  53. package/starter/sections/values.liquid +1 -1
@@ -0,0 +1,113 @@
1
+ import Ajv from 'ajv';
2
+ import context from '../contract/v2/context.json' with { type: 'json' };
3
+ import sections from '../contract/v2/sections.json' with { type: 'json' };
4
+ import schema from '../contract/v2/site.schema.json' with { type: 'json' };
5
+ import contentModel from '../contract/v2/content-model.json' with { type: 'json' };
6
+ export { extractContentFootprint } from '../engine/content-footprint.mjs';
7
+ export { contentModel };
8
+
9
+ export const PAGE_KEYS = ['home', 'about'];
10
+ const byType = new Map(sections.sections.map(section => [section.type, section]));
11
+ const validateSite = new Ajv({ allErrors: true, strict: false }).compile(schema);
12
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
13
+
14
+ /** Each example section has an identity before it is edited or moved. */
15
+ export function composePage(manifest, page) {
16
+ const declared = manifest?.compositions?.[page];
17
+ const types = Array.isArray(declared)
18
+ ? declared.filter(entry => entry?.role !== 'optional').map(entry => entry.type)
19
+ : (manifest?.supports?.sections ?? []).filter(type => byType.get(type)?.pages.includes(page));
20
+ return types.flatMap((type, index) => {
21
+ const entry = byType.get(type);
22
+ return entry ? [{ key: `fixture-${page}-${type}-${index}`, type, content: structuredClone(entry.sample ?? {}) }] : [];
23
+ });
24
+ }
25
+
26
+ export function buildSiteFixture(manifest, { page = 'home', path } = {}) {
27
+ const site = structuredClone(context.fixtures.site);
28
+ site.page = { key: page, path: path ?? (page === 'home' ? '/' : `/${page}`), sections: PAGE_KEYS.includes(page) ? composePage(manifest, page) : [] };
29
+ return site;
30
+ }
31
+
32
+ export function validateSiteFixture(site) {
33
+ const errors = validateSite(site) ? [] : validateSite.errors.map(error => `${error.instancePath || '/'}: ${error.message}`);
34
+ const walk = (items, depth, path) => {
35
+ if (!Array.isArray(items)) return;
36
+ if (depth > 2 && items.length) errors.push(`${path}: navigation may have only two levels below the root`);
37
+ items.forEach((item, index) => walk(item?.children, depth + 1, `${path}/${index}/children`));
38
+ };
39
+ walk(site?.nav?.header, 0, '/nav/header');
40
+ walk(site?.nav?.footer, 0, '/nav/footer');
41
+ return errors;
42
+ }
43
+
44
+ function validatePageSections(value, path, errors) {
45
+ if (!Array.isArray(value)) { errors.push(`${path}: must be an array of keyed sections`); return; }
46
+ const seen = new Set();
47
+ value.forEach((section, index) => {
48
+ const at = `${path}[${index}]`;
49
+ if (!object(section)) { errors.push(`${at}: must be a section object`); return; }
50
+ if (typeof section.key !== 'string' || !section.key || seen.has(section.key)) errors.push(`${at}.key: a unique stable key is required`);
51
+ seen.add(section.key);
52
+ const entry = byType.get(section.type);
53
+ if (!entry) { errors.push(`${at}.type: '${section.type}' is not a v2 section`); return; }
54
+ if (!object(section.content)) { errors.push(`${at}.content: must be an object`); return; }
55
+ const fields = new Map(entry.fields.map(field => [field.name, field]));
56
+ for (const [name, fieldValue] of Object.entries(section.content)) {
57
+ const field = fields.get(name);
58
+ if (!field) { errors.push(`${at}.content.${name}: not an authored ${section.type} field`); continue; }
59
+ if (field.kind === 'items') {
60
+ if (!Array.isArray(fieldValue)) { errors.push(`${at}.content.${name}: must be an array`); continue; }
61
+ const itemFields = new Set(field.itemFields.map(item => item.name));
62
+ fieldValue.forEach((item, itemIndex) => {
63
+ if (!object(item)) { errors.push(`${at}.content.${name}[${itemIndex}]: must be an object`); return; }
64
+ for (const key of Object.keys(item)) if (!itemFields.has(key)) errors.push(`${at}.content.${name}[${itemIndex}].${key}: unknown item field`);
65
+ });
66
+ } else if (typeof fieldValue !== 'string') errors.push(`${at}.content.${name}: must be a string; an empty string intentionally clears optional text`);
67
+ }
68
+ });
69
+ }
70
+
71
+ /** Author overrides replace one whole envelope, never widen public eligibility or add fields. */
72
+ export function validatePreviewContent(json) {
73
+ if (!object(json)) return ['preview-content.json: must be an object of v2 overrides'];
74
+ const errors = [];
75
+ const site = buildSiteFixture(null);
76
+ for (const [name, value] of Object.entries(json)) {
77
+ if (['brand', 'nav', 'actions', 'socials', 'locale'].includes(name)) {
78
+ site[name] = name === 'brand' && object(value) ? { ...site.brand, ...value } : value;
79
+ } else if (name === 'pages') {
80
+ if (!object(value)) { errors.push('preview-content.json: pages must be keyed by home or about'); continue; }
81
+ for (const [page, composition] of Object.entries(value)) {
82
+ if (!PAGE_KEYS.includes(page)) errors.push(`preview-content.json: pages.${page} is not an editable page`);
83
+ validatePageSections(composition, `pages.${page}`, errors);
84
+ }
85
+ } else if (Object.hasOwn(contentModel.collections, name)) site.content[name] = value;
86
+ else errors.push(`preview-content.json: '${name}' is not a v2 override; retired aliases cannot be used`);
87
+ }
88
+ return [...errors, ...validateSiteFixture(site).map(error => `preview-content.json: ${error}`)];
89
+ }
90
+
91
+ export function applyPreviewContent(site, json) {
92
+ if (validatePreviewContent(json).length) return site;
93
+ const next = structuredClone(site);
94
+ for (const [name, value] of Object.entries(json)) {
95
+ if (['brand', 'nav', 'actions', 'socials', 'locale'].includes(name)) next[name] = name === 'brand' ? { ...next.brand, ...value } : structuredClone(value);
96
+ else if (Object.hasOwn(contentModel.collections, name)) next.content[name] = structuredClone(value);
97
+ }
98
+ return next;
99
+ }
100
+
101
+ export function pageComposition(manifest, page, json) {
102
+ const override = json?.pages?.[page];
103
+ return Array.isArray(override) ? structuredClone(override) : composePage(manifest, page);
104
+ }
105
+
106
+ /** Keep missing fields missing so a generated label never becomes an authored field marker. */
107
+ export function resolveSectionFixture(section, _site) {
108
+ return structuredClone(section.content ?? {});
109
+ }
110
+
111
+ export function emptyCollections(site) {
112
+ return { ...site, content: Object.fromEntries(Object.entries(site.content).map(([name, value]) => [name, Array.isArray(value) ? [] : { ...value, items: [], pagination: null }])) };
113
+ }