@port60/template-kit 0.21.0 → 1.1.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 (55) hide show
  1. package/README.md +58 -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 +116 -161
  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 +107 -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 +6933 -0
  34. package/src/vendor/engine/content-footprint.mjs +76 -1
  35. package/src/vendor/engine/dialect.mjs +4 -0
  36. package/src/vendor/engine/locale.mjs +74 -0
  37. package/src/vendor/engine/majors.mjs +42 -0
  38. package/src/vendor/validator/gift-aid-logo.svg +6 -0
  39. package/src/vendor/validator/model-reference-v2.mjs +95 -0
  40. package/src/vendor/validator/platform-base.css +320 -11
  41. package/src/vendor/validator/preview-v1.mjs +691 -0
  42. package/src/vendor/validator/preview-v2.mjs +654 -0
  43. package/src/vendor/validator/preview.mjs +8 -688
  44. package/src/vendor/validator/site-context-v2.mjs +113 -0
  45. package/src/vendor/validator/validate-v1.mjs +666 -0
  46. package/src/vendor/validator/validate-v2.mjs +642 -0
  47. package/src/vendor/validator/validate.mjs +21 -663
  48. package/starter/assets/theme.css +3 -0
  49. package/starter/layout.liquid +15 -18
  50. package/starter/manifest.json +5 -4
  51. package/starter/preview/config.json +4 -0
  52. package/starter/preview/media/README.md +9 -0
  53. package/starter/sections/campaigns.liquid +5 -4
  54. package/starter/sections/homeHero.liquid +25 -21
  55. package/starter/sections/values.liquid +1 -1
package/README.md CHANGED
@@ -33,6 +33,8 @@ newer contract asks of you.
33
33
  | `dev [dir]` | Live preview over the contract's sample fixtures; validation re-runs on save. |
34
34
  | `validate [dir] [--json]` | The exact checks the platform runs at upload. `--json` emits `{ok, errors, warnings, provenSupports}`. |
35
35
  | `package [dir]` | Validate, then build the contract-shaped zip the studio accepts as-is. |
36
+ | `release [dir]` | Build separate runtime and independent designer-preview folders for a trusted store release. Does not upload. |
37
+ | `setup-previews [--with-deps]` | Install the pinned headless browser for automatic gallery images. Add `--with-deps` in Linux CI. |
36
38
  | `model [--json]` | The content model, in hand; `--json` for agents. |
37
39
  | `content [dir]` | Eject the model's data as your editable copy; render it with `dev --content`. |
38
40
  | `upgrade [dir]` | Latest kit + contract for an existing template; re-briefs and re-validates. |
@@ -48,9 +50,62 @@ See the [AI quickstart](https://developers.port60.com/guides/ai-quickstart/).
48
50
 
49
51
  ## The contract
50
52
 
51
- The vendored contract in `src/vendor/contract` is the **Charity Platform contract v1**, the
52
- platform's first product surface. The dialect, validation rules and this toolchain are
53
- platform-wide; future Port60 products ship their own contract packs for the same kit.
53
+ Kit 1.1.0 authors **port60-liquid@2**, content model **2.0**, using the contract under
54
+ `src/vendor/contract/v2`. Collection envelopes, independent header/footer navigation,
55
+ resolved actions and page-scoped sections are explicit. V1 sources need a deliberate migration,
56
+ not a manifest-only relabel. Historical v1 contracts remain frozen for existing platform pins.
57
+
58
+ ### Added in 1.1.0
59
+
60
+ The v2 dialect adds `t` and `local_date`, matching the compatible platform host. Use
61
+ `t: site.locale.code` only for supported interface phrases, not tenant-authored content.
62
+ The initial phrase catalogue is Arabic; unknown phrases or languages preserve the supplied
63
+ text. `local_date` formats English, Welsh or Arabic Gregorian dates in UTC with fixed
64
+ `date` and `datetime` styles. It does not convert the tenant's prayer wall-clock times.
65
+
66
+ Schedule rows can carry stable `key` values for prayer icons, independently of their supplied
67
+ display names. Shared mobile island styling also uses logical offsets for both directions.
68
+ Historical v1 filters and contracts are unchanged. These additions do not imply complete
69
+ Arabic live-island, typography or legal-copy support.
70
+
71
+ Deploy the compatible host before uploading templates that use the new filters. Kit 1.0.0
72
+ will correctly reject them. See the [localisation guide](https://developers.port60.com/guides/localisation/).
73
+
74
+ ## Designer previews
75
+
76
+ The scaffold creates `preview/config.json` and `preview/media/`. Optional `content` points to
77
+ fictional v2 sample JSON relative to `preview/`; omission uses contract fixtures. `focus` is
78
+ `none`, `donate` or `volunteer`. Use approved local JPEG, PNG or WebP images via
79
+ `p60preview:filename.jpg` and retain provenance with the source. Never use selecting-tenant data.
80
+
81
+ `npm run release` writes `dist/release/NAME/VERSION/`: lightweight `template/` files, separate
82
+ `preview/` HTML/media/metadata, and a `release.json` completion record. Every Look is generated
83
+ from actual Liquid/CSS with matching palettes. Existing output is refused. Limits are 24 Looks,
84
+ 2 MiB per image/page and 24 MiB per release. Demo forms, navigation and transactions are inactive.
85
+
86
+ The first-party publisher uploads completion last. The catalogue supplies matching preview
87
+ metadata to tenant-admin; charity-site reads only runtime files. Previews are not bundled into
88
+ admin and do not become tenant content. Preview-only changes also need a new template version.
89
+ The Studio ZIP `package`/`publish` path remains separate and runtime-only.
90
+
91
+ Before your first release build, run `npm run preview:setup` in a new scaffold, or
92
+ `p60-template-kit setup-previews` in an existing project. Repeat after upgrading the kit.
93
+ The release builder automatically captures each Look from the actual template at 1440x900
94
+ and writes a 960x600 WebP to `preview/gallery/`, capped at 160 KiB. No separate screenshot
95
+ authoring or upload is needed. The gallery uses these images; opening a design loads its HTML.
96
+ The pinned browser is a build dependency only, never part of the live website.
97
+
98
+ Linux capture requires a working Chromium sandbox. Ubuntu 24.04 hosted CI installs the
99
+ repository's browser-path-scoped AppArmor profile to allow its user namespaces.
100
+ No `--no-sandbox` or global AppArmor disablement is used. On another Linux
101
+ host, configure an administrator-approved sandbox according to
102
+ [Chromium's guidance](https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md).
103
+ Browser installation alone does not change the host's security policy.
104
+
105
+ Only packaged preview assets and fonts.bunny.net are accessible during capture. Failed required
106
+ imagery or Latin fonts stop generation. Arabic-specific poster font fidelity is deferred;
107
+ full previews and live-site typography are unchanged. Preserve the same kit/browser/OS for
108
+ immutable upload retries; output changes need a new version, not an overwrite.
54
109
 
55
110
  Full documentation: [developers.port60.com](https://developers.port60.com)
56
111
 
package/bin/cli.mjs CHANGED
@@ -12,6 +12,8 @@ import { upgrade } from '../src/commands/upgrade.mjs';
12
12
  import { refresh } from '../src/commands/refresh.mjs';
13
13
  import { login, logout, whoami } from '../src/commands/auth.mjs';
14
14
  import { publish } from '../src/commands/publish.mjs';
15
+ import { releaseCmd } from '../src/commands/release.mjs';
16
+ import { setupPreviews } from '../src/commands/setupPreviews.mjs';
15
17
 
16
18
  function parseArgs(argv) {
17
19
  const args = { _: [] };
@@ -37,6 +39,12 @@ const [command, ...rest] = process.argv.slice(2);
37
39
  const args = parseArgs(rest);
38
40
 
39
41
  switch (command) {
42
+ case 'setup-previews':
43
+ setupPreviews(args);
44
+ break;
45
+ case 'release':
46
+ await releaseCmd(args);
47
+ break;
40
48
  case 'create':
41
49
  create(args);
42
50
  break;
@@ -85,6 +93,8 @@ usage:
85
93
  p60-template-kit content [dir] [--out file] eject the model's data as your editable copy
86
94
  p60-template-kit validate [dir] [--json] the platform's exact conformance checks
87
95
  p60-template-kit package [dir] validate + build the uploadable zip
96
+ p60-template-kit release [dir] build separate template/ and preview/ store bundles
97
+ p60-template-kit setup-previews [--with-deps] install the release builder's headless browser
88
98
  p60-template-kit upgrade [dir] move an existing template to the latest kit
89
99
  and contract, re-brief, re-validate
90
100
  p60-template-kit login sign in from the terminal (the browser does the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@port60/template-kit",
3
- "version": "0.21.0",
3
+ "version": "1.1.0",
4
4
  "description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready: every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,8 +16,10 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "ajv": "^8.20.0",
19
+ "jsdom": "29.1.1",
19
20
  "liquidjs": "^10.27.2",
20
- "lucide-static": "^1.41.0"
21
+ "lucide-static": "^1.41.0",
22
+ "playwright": "1.63.0"
21
23
  },
22
24
  "license": "MIT",
23
25
  "repository": {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, writeFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { buildSiteFixture, composePage, validatePreviewContent } from '../vendor/validator/site-context.mjs';
3
+ import { buildSiteFixture, composePage, validatePreviewContent } from '../vendor/validator/site-context-v2.mjs';
4
4
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
5
5
 
6
6
  /**
@@ -34,9 +34,10 @@ export function content(args) {
34
34
  }
35
35
  const data = {
36
36
  brand: site.brand,
37
- nav: { items: site.nav?.items ?? [] },
37
+ nav: site.nav,
38
+ actions: site.actions,
38
39
  ...(manifest ? { pages: { home: composePage(manifest, 'home'), about: composePage(manifest, 'about') } } : {}),
39
- ...Object.fromEntries(Object.entries(site.content).filter(([name]) => name !== 'about'))
40
+ ...site.content
40
41
  };
41
42
  const problems = validatePreviewContent(data);
42
43
  if (problems.length > 0) {
@@ -19,7 +19,7 @@ export function create(args) {
19
19
  const name = (args.name ?? basename(target)).toLowerCase();
20
20
  if (!NAME_PATTERN.test(name)) {
21
21
  console.error(`✗ '${name}' is not a valid template name (lowercase letters, digits, hyphens, `
22
- + '3–50 chars, starting with a letter). Pass --name.');
22
+ + '3 to 50 chars, starting with a letter). Pass --name.');
23
23
  process.exit(1);
24
24
  }
25
25
  if (existsSync(join(target, 'manifest.json'))) {
@@ -50,7 +50,9 @@ export function create(args) {
50
50
  dev: 'p60-template-kit dev .',
51
51
  validate: 'p60-template-kit validate .',
52
52
  'validate:json': 'p60-template-kit validate . --json',
53
- package: 'p60-template-kit package .'
53
+ package: 'p60-template-kit package .',
54
+ release: 'p60-template-kit release .',
55
+ 'preview:setup': 'p60-template-kit setup-previews'
54
56
  },
55
57
  devDependencies: {
56
58
  '@port60/template-kit': `^${KIT_PACKAGE.version}`
@@ -58,11 +60,17 @@ export function create(args) {
58
60
  }, null, 2) + '\n');
59
61
  writeFileSync(join(target, 'README.md'), `# ${label}
60
62
 
61
- A Port60 site template. Start with \`npm install\`, then:
63
+ A Port60 site template using \`port60-liquid@2\`, content model \`2.0\` and kit \`${KIT_PACKAGE.version}\`.
64
+ Existing v1 platform pins keep historical support; this kit accepts v2 authoring only.
65
+ Start with \`npm install\`, then:
62
66
 
63
67
  - \`npm run dev\`, live preview at http://localhost:4400
64
68
  - \`npm run validate\`, conformance against the platform contract
65
69
  - \`npm run package\`, the uploadable zip in \`dist/\` (recreated on every run)
70
+ - \`npm run release\`, separate runtime and designer-preview folders in \`dist/release/\`
71
+ - \`npm run preview:setup\`, install the pinned browser before your first release build
72
+ - \`preview/config.json\` selects author sample content and widget state; put optional
73
+ author images in \`preview/media/\` and reference them as \`p60preview:filename.jpg\`.
66
74
 
67
75
  **Working with an AI agent?** Point it at this directory, \`AGENTS.md\` (and \`CLAUDE.md\`)
68
76
  brief it on the contract, the rules and the validate loop.
@@ -3,9 +3,9 @@ import { watch, readFileSync, existsSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { surfaceFor, knobOverridesFromQuery, previewAssetPath, previewAssetType } from '../lib/previewOptions.mjs';
5
5
  import { renderStudioPreview } from '../vendor/validator/preview.mjs';
6
- import { validateArtifact } from '../vendor/validator/validate.mjs';
7
- import { applyPreviewContent, buildSiteFixture, validatePreviewContent } from '../vendor/validator/site-context.mjs';
8
- import { renderModelReferenceHtml } from '../vendor/validator/model-reference.mjs';
6
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
7
+ import { applyPreviewContent, buildSiteFixture, validatePreviewContent } from '../vendor/validator/site-context-v2.mjs';
8
+ import { renderModelReferenceHtml } from '../vendor/validator/model-reference-v2.mjs';
9
9
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
10
10
  import { join } from 'node:path';
11
11
 
@@ -80,6 +80,12 @@ function imageOriginsOf(json) {
80
80
  */
81
81
  export async function dev(args) {
82
82
  const dir = resolve(args._[0] ?? '.');
83
+ const initialManifest = JSON.parse(loadArtifactDir(dir)['manifest.json'] ?? '{}');
84
+ if (initialManifest.format !== 'port60-liquid@2') {
85
+ console.error('✗ Kit 1.0.0 requires port60-liquid@2. Existing v1 pins remain supported by the platform, not by the new authoring kit.');
86
+ process.exitCode = 1;
87
+ return;
88
+ }
83
89
  const port = Number(args.port ?? 4400);
84
90
  // The dev-richer half of the imagery split: point P60_FIXTURE_IMAGES at the platform's fixture
85
91
  // imagery base and the preview loads photographic fixtures from that ONE origin instead of the
@@ -1,4 +1,4 @@
1
- import { contentModel } from '../vendor/engine/content-footprint.mjs';
1
+ import { contentModelV2 as contentModel } from '../vendor/engine/content-footprint.mjs';
2
2
 
3
3
  /**
4
4
  * `model [--json]`, the content model, in hand. Prints every `site.content.*` collection with
@@ -15,7 +15,7 @@ export function model(args) {
15
15
  console.log('site also carries: ' + contentModel.siblings.keys.map((k) => `site.${k}`).join(', '));
16
16
  console.log('');
17
17
  for (const [name, collection] of Object.entries(contentModel.collections)) {
18
- const more = collection.moreHref ? ` · more at ${collection.moreHref}` : '';
18
+ const more = collection.shape === 'envelope' ? ' · {label, href, items, pagination}' : ' · array';
19
19
  console.log(`site.content.${name} (bounded at ${collection.cap}${more}, since ${collection.since})`);
20
20
  for (const [field, spec] of Object.entries(collection.item)) {
21
21
  const notes = [
@@ -27,7 +27,7 @@ export function model(args) {
27
27
  }
28
28
  console.log('');
29
29
  }
30
- console.log('Rules: collections are bounded (link onward via moreHref); enums are open (branch and');
30
+ console.log('Rules: collections are bounded (link onward via href and pagination); enums are open (branch and');
31
31
  console.log('fall back); nullable fields need a branch; the model only grows. Your footprint and');
32
32
  console.log('minimum model version are computed at publish, you never declare them. Live examples:');
33
33
  console.log('the /model page on your dev preview.');
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { resolve, join } from 'node:path';
3
- import { validateArtifact } from '../vendor/validator/validate.mjs';
3
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
4
4
  import { loadArtifactDir, skippedNotice } from '../lib/artifactFiles.mjs';
5
5
  import { buildZip } from '../lib/zip.mjs';
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { validateArtifact } from '../vendor/validator/validate.mjs';
2
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
3
3
  import { loadArtifactDir, skippedNotice } from '../lib/artifactFiles.mjs';
4
4
  import { buildZip } from '../lib/zip.mjs';
5
5
  import { accessToken, api, apiBase, loadCredentials, requireCredentials } from '../lib/auth.mjs';
@@ -2,9 +2,9 @@ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { resolve, join } from 'node:path';
3
3
  import { agentsMd } from '../lib/agentsMd.mjs';
4
4
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
5
- import { validateArtifact } from '../vendor/validator/validate.mjs';
6
- import { contentModel } from '../vendor/engine/content-footprint.mjs';
7
- import dialect from '../vendor/contract/v1/dialect.json' with { type: 'json' };
5
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
6
+ import { contentModelV2 as contentModel } from '../vendor/engine/content-footprint.mjs';
7
+ import dialect from '../vendor/contract/v2/dialect.json' with { type: 'json' };
8
8
 
9
9
  const KIT_PACKAGE = JSON.parse(
10
10
  readFileSync(resolve(import.meta.dirname, '../../package.json'), 'utf8')
@@ -25,6 +25,11 @@ export async function refresh(args) {
25
25
  process.exit(1);
26
26
  }
27
27
  const manifest = JSON.parse(files['manifest.json']);
28
+ if (manifest.format !== dialect.format) {
29
+ console.error(`✗ Kit ${KIT_PACKAGE.version} requires ${dialect.format}. Migrate the template before refreshing its instructions. No files changed.`);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
28
33
 
29
34
  const briefing = agentsMd(manifest.name);
30
35
  writeFileSync(join(dir, 'AGENTS.md'), briefing);
@@ -0,0 +1,22 @@
1
+ import { mkdirSync, writeFileSync, existsSync, lstatSync } from 'node:fs';
2
+ import { resolve, join, dirname } from 'node:path';
3
+ import { buildReleaseBundle } from '../lib/releaseBundle.mjs';
4
+
5
+ export async function releaseCmd(args) {
6
+ const root = resolve(args._[0] ?? '.');
7
+ const bundle = await buildReleaseBundle(root);
8
+ const output = join(root, 'dist', 'release', bundle.manifest.name, bundle.manifest.version);
9
+ // Never follow a linked build destination or silently mix two builds of an immutable release.
10
+ for (const dir of [join(root, 'dist'), join(root, 'dist', 'release'), join(root, 'dist', 'release', bundle.manifest.name), output]) {
11
+ if (existsSync(dir) && lstatSync(dir).isSymbolicLink()) throw new Error('Linked release destination refused');
12
+ }
13
+ if (existsSync(output)) throw new Error(`Release output already exists: ${output}. Review/remove that generated directory or bump the version before rebuilding.`);
14
+ for (const [path, file] of Object.entries(bundle.files)) {
15
+ const destination = join(output, path);
16
+ mkdirSync(dirname(destination), { recursive: true });
17
+ writeFileSync(destination, file.bytes, { flag: 'wx' });
18
+ }
19
+ console.log(`Validated release: ${output}`);
20
+ console.log('template/ is runtime only; preview/ is the independent designer gallery. Publish release.json last.');
21
+ console.log('Studio package/publish still accepts the runtime ZIP; store release publication uses the first-party pipeline.');
22
+ }
@@ -0,0 +1,19 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+
5
+ export function previewSetupCommand(args = {}) {
6
+ const require = createRequire(import.meta.url);
7
+ // Playwright exports its manifest, not a ./cli module. Use its declared binary so
8
+ // this resolves the kit's pinned installation even inside another npm project.
9
+ const manifestPath = require.resolve('playwright/package.json');
10
+ const command = [resolve(dirname(manifestPath), require(manifestPath).bin.playwright), 'install', 'chromium', '--only-shell'];
11
+ if (args['with-deps']) command.push('--with-deps');
12
+ return command;
13
+ }
14
+
15
+ export function setupPreviews(args) {
16
+ const result = spawnSync(process.execPath, previewSetupCommand(args), { stdio: 'inherit' });
17
+ if (result.error) throw result.error;
18
+ process.exitCode = result.status ?? 1;
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { validateArtifact } from '../vendor/validator/validate.mjs';
2
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
3
3
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
4
4
 
5
5
  /**