@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
@@ -25,6 +25,9 @@ h3 { font-size: 1.15rem; }
25
25
  p { color: var(--muted); line-height: 1.7; margin: 0 0 1rem; }
26
26
  strong { color: var(--ink); }
27
27
  .site-header { background: rgba(14, 22, 20, 0.86); }
28
+ /* Non-link groups keep the same typography and hit area as neighbouring navigation links. */
29
+ .site-nav button.nav-dropdown-toggle { appearance: none; border: 0; background: transparent; padding: 0.6rem 0.95rem; border-radius: 999px; color: var(--muted); font: inherit; font-weight: 600; cursor: pointer; }
30
+ .site-nav button.nav-dropdown-toggle:hover { background: rgba(31, 42, 51, 0.05); color: var(--ink); }
28
31
  .lq-hero { padding: 4.5rem 0 2.5rem; border-bottom: 1px solid var(--line); }
29
32
  .lq-kicker { display: inline-block; border: 1px solid var(--line-strong); color: var(--primary); border-radius: 4px; padding: 0.3rem 0.8rem; font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 700; }
30
33
  .lq-hero h1 { margin-top: 1rem; max-width: 18ch; }
@@ -25,23 +25,25 @@
25
25
  <span aria-hidden="true"></span>
26
26
  </button>
27
27
 
28
- <nav class="site-nav" id="site-nav" aria-label="Primary">
29
- {% for item in site.nav.items %}
30
- {% if item.children %}
31
- <span class="nav-dropdown">
32
- <a href="{{ item.href }}" class="nav-dropdown-toggle" aria-haspopup="true">{{ item.label }} ▾</a>
33
- <span class="nav-dropdown-menu">
28
+ <nav class="site-nav" id="site-nav" aria-label="Primary" data-p60-nav>
29
+ {% for item in site.nav.header %}
30
+ {% if item.children.size > 0 %}
31
+ <span class="nav-dropdown" data-p60-nav-item>
32
+ {% if item.kind == 'group' %}<button type="button" class="nav-dropdown-toggle" data-p60-nav-toggle aria-expanded="false" aria-controls="starter-menu-{{ forloop.index }}">{{ item.label }} ▾</button>{% else %}<a href="{{ item.href }}" class="nav-dropdown-toggle" data-p60-nav-toggle aria-expanded="false" aria-controls="starter-menu-{{ forloop.index }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }} ▾</a>{% endif %}
33
+ <span class="nav-dropdown-menu" id="starter-menu-{{ forloop.index }}" data-p60-nav-menu>
34
34
  {% for child in item.children %}
35
- <a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a>
35
+ {% if child.kind == 'group' %}<span class="nav-group-label">{{ child.label }}</span>{% else %}<a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a>{% endif %}
36
+ {% for grandchild in child.children %}{% if grandchild.kind == 'group' %}<span class="nav-group-label">{{ grandchild.label }}</span>{% else %}<a href="{{ grandchild.href }}"{% if grandchild.external %} target="_blank" rel="noopener"{% endif %}>{{ grandchild.label }}</a>{% endif %}{% endfor %}
36
37
  {% endfor %}
37
38
  </span>
38
39
  </span>
39
- {% elsif item.cta %}
40
- <a class="nav-cta" href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a>
40
+ {% elsif item.kind == 'group' %}
41
+ <span class="nav-group-label">{{ item.label }}</span>
41
42
  {% else %}
42
43
  <a href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a>
43
44
  {% endif %}
44
45
  {% endfor %}
46
+ {% if site.actions.header %}<a class="nav-cta" href="{{ site.actions.header.href }}">{{ site.actions.header.label }}</a>{% endif %}
45
47
  {% island 'search' %}
46
48
  {% island 'member_menu' %}
47
49
  </nav>
@@ -59,19 +61,14 @@
59
61
  {% if site.brand.tagline %}<h2>{{ site.brand.tagline }}</h2>{% endif %}
60
62
  </div>
61
63
  <div class="footer-links">
62
- {% comment %} A tenant-curated footer menu when they've built one, else the main site.nav. {% endcomment %}
63
- {% if site.nav.footer.size > 0 %}
64
+ {% comment %} The platform resolves the footer independently; an empty footer stays empty. {% endcomment %}
64
65
  {% for item in site.nav.footer %}
65
- <a href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a>
66
+ {% if item.kind == 'group' %}<span class="nav-group-label">{{ item.label }}</span>{% else %}<a href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a>{% endif %}
66
67
  {% for child in item.children %}
67
- <a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a>
68
+ {% if child.kind == 'group' %}<span class="nav-group-label">{{ child.label }}</span>{% else %}<a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a>{% endif %}
69
+ {% for grandchild in child.children %}{% if grandchild.kind == 'group' %}<span class="nav-group-label">{{ grandchild.label }}</span>{% else %}<a href="{{ grandchild.href }}">{{ grandchild.label }}</a>{% endif %}{% endfor %}
68
70
  {% endfor %}
69
71
  {% endfor %}
70
- {% else %}
71
- {% for item in site.nav.items %}
72
- <a href="{{ item.href }}">{{ item.label }}</a>
73
- {% endfor %}
74
- {% endif %}
75
72
  </div>
76
73
  </div>
77
74
  <div class="container">
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starter",
3
- "version": "1.14.0",
4
- "format": "port60-liquid@1",
3
+ "version": "2.0.0",
4
+ "format": "port60-liquid@2",
5
5
  "label": "Starter",
6
6
  "description": "The reference template for the Port60 dialect, the developer docs' worked example and the base to copy when building your own. 1.1.0 adds the layout slot: the template owns the header, navigation and footer chrome.",
7
7
  "supports": {
@@ -32,7 +32,8 @@
32
32
  "heroImagery": true,
33
33
  "behaviors": [
34
34
  "reveal",
35
- "carousel"
35
+ "carousel",
36
+ "nav"
36
37
  ],
37
38
  "focus": [
38
39
  "donate",
@@ -94,7 +95,7 @@
94
95
  }
95
96
  ]
96
97
  },
97
- "changelog": "Marks which node shows which field (data-p60-field), so the editor puts the caret on the page itself instead of in a side panel.",
98
+ "changelog": "Uses the v2 site object, resolved header actions and collection envelopes while preserving authored field markers.",
98
99
  "imagery": {
99
100
  "hero": {
100
101
  "idealAspect": "16:9",
@@ -0,0 +1,4 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "focus": "none"
4
+ }
@@ -0,0 +1,9 @@
1
+ # Designer preview imagery
2
+
3
+ Place authorised JPEG, PNG or WebP images here, at most 2 MiB each. Reference an image in
4
+ your independent author content as `p60preview:filename.jpg`. Configure that JSON content
5
+ file through `preview/config.json`, for example `"content": "content.json"`.
6
+
7
+ These images ship only in the separate designer preview bundle. They never enter the
8
+ runtime Liquid/CSS artifact or a tenant's media library. Keep provenance and required
9
+ attribution beside your source images. No tenant content or secrets belong here.
@@ -4,13 +4,14 @@
4
4
  platform's published site.content.campaigns; hrefs are platform-derived, always link via campaign.href.
5
5
  Derive or omit: nothing renders when no campaign is live.
6
6
  {% endcomment %}
7
- {% if site.content.campaigns.size > 0 %}
7
+ {% if site.content.campaigns.items.size > 0 %}
8
8
  <section class="lq-site.content.campaigns">
9
9
  <div class="container">
10
- <h2 data-p60-field="heading">{{ section.heading | default: campaignsLabel }}</h2>
11
- {% if section.text %}<p data-p60-field="text">{{ section.text }}</p>{% endif %}
10
+ {% if section.eyebrow != nil and section.eyebrow != '' %}<p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow }}</p>{% endif %}
11
+ {% if section.title == nil %}<h2>{{ site.content.campaigns.label }}</h2>{% elsif section.title != '' %}<h2 data-p60-field="title">{{ section.title }}</h2>{% endif %}
12
+ {% if section.subtitle != nil and section.subtitle != '' %}<p data-p60-field="subtitle">{{ section.subtitle }}</p>{% endif %}
12
13
  <ul class="lq-campaign-list">
13
- {% for campaign in site.content.campaigns %}
14
+ {% for campaign in site.content.campaigns.items %}
14
15
  <li class="lq-campaign">
15
16
  <h3>{{ campaign.title }}</h3>
16
17
  {% if campaign.summary %}<p>{{ campaign.summary }}</p>{% endif %}
@@ -2,7 +2,7 @@
2
2
  The home opener, and the REFERENCE IMPLEMENTATION of supports.heroImagery. Rules worth
3
3
  copying:
4
4
 
5
- 1. EVERY field is optional. Copy derives from brand (| default) or omits; no photos means the
5
+ 1. EVERY field is optional. The host resolves inherited copy; explicit blanks omit; no photos means the
6
6
  designed gradient below, never a placeholder image.
7
7
  2. Photos are NEVER placed raw. The treatments in theme.css lay scrims built from the palette
8
8
  variables (color-mix over var(--bg)/var(--bg-tint)), so any uploaded photograph is toned
@@ -14,6 +14,10 @@
14
14
  toward the photo's own shape (height-clamped) so a wide landscape stays entirely visible,
15
15
  with your copy stacked after the band.
16
16
  {% endcomment %}
17
+ {% assign heroButtonLabel = site.actions.hero.label %}
18
+ {% assign heroButtonHref = site.actions.hero.href %}
19
+ {% if section.primaryLabel != nil %}{% assign heroButtonLabel = section.primaryLabel %}{% endif %}
20
+ {% if section.primaryHref != nil %}{% assign heroButtonHref = section.primaryHref %}{% endif %}
17
21
  {% assign heroCount = section.images | size %}
18
22
  {% if heroCount == 1 %}
19
23
  {% assign heroPhoto = section.images.first.imageUrl %}
@@ -31,19 +35,19 @@
31
35
  {% endif %}
32
36
  {% assign heroFraming = section.photoFraming | default: 'fill' %}
33
37
  {% if heroCount > 0 and heroFraming == 'whole' %}
34
- {% if section.actionStyle == 'button' or section.givingStyle == 'button' %}
38
+ {% if section.actionStyle == 'button' %}
35
39
  <section class="lq-homehero lq-homehero--vista{% if heroCount > 1 %} lq-homehero--carousel{% endif %}">
36
40
  <div class="lq-vista">
37
41
  <img class="lq-vista-sizer" src="{{ section.images.first.imageUrl }}" alt="{{ section.images.first.alt }}" style="object-position: {{ heroFocus }}" fetchpriority="high" decoding="async">
38
42
  {% if heroCount > 1 %}{% island 'hero_carousel' %}{% else %}<div class="lq-vista-scrim" aria-hidden="true"></div>{% endif %}
39
43
  <div class="lq-vista-copy">
40
44
  <div class="container">
41
- <p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow | default: site.brand.name }}</p>
42
- <h1 data-p60-field="title">{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
45
+ {% if section.eyebrow != nil and section.eyebrow != '' %}<p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow }}</p>{% endif %}
46
+ {% if section.title != nil and section.title != '' %}<h1 data-p60-field="title">{{ section.title }}</h1>{% endif %}
43
47
  {% if section.lead %}<p class="lq-homehero-lead" data-p60-field="lead">{{ section.lead }}</p>{% endif %}
44
48
  <div class="lq-homehero-actions">
45
- <a class="button button-primary" href="{{ section.primaryHref | default: '/donate' }}">{{ section.primaryLabel | default: 'Donate now' }}</a>
46
- <a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a>
49
+ {% if heroButtonLabel != nil and heroButtonLabel != '' and heroButtonHref != nil and heroButtonHref != '' %}<a class="button button-primary" href="{{ heroButtonHref }}">{% if section.primaryLabel != nil %}<span data-p60-field="primaryLabel">{{ section.primaryLabel }}</span>{% else %}{{ heroButtonLabel }}{% endif %}</a>{% endif %}
50
+ {% if section.secondaryLabel != nil and section.secondaryLabel != '' and section.secondaryHref != nil and section.secondaryHref != '' %}<a class="button" href="{{ section.secondaryHref }}"><span data-p60-field="secondaryLabel">{{ section.secondaryLabel }}</span></a>{% endif %}
47
51
  </div>
48
52
  </div>
49
53
  </div>
@@ -59,18 +63,18 @@
59
63
  </div>
60
64
  <div class="container lq-homehero-inner lq-vista-overlap">
61
65
  <div>
62
- <p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow | default: site.brand.name }}</p>
63
- <h1 data-p60-field="title">{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
66
+ {% if section.eyebrow != nil and section.eyebrow != '' %}<p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow }}</p>{% endif %}
67
+ {% if section.title != nil and section.title != '' %}<h1 data-p60-field="title">{{ section.title }}</h1>{% endif %}
64
68
  {% if section.lead %}<p class="lq-homehero-lead" data-p60-field="lead">{{ section.lead }}</p>{% endif %}
65
69
  <div class="lq-homehero-actions">
66
- {% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '#donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %}
67
- <a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a>
70
+ {% if heroButtonLabel != nil and heroButtonLabel != '' and heroButtonHref != nil and heroButtonHref != '' %}<a class="button button-primary" href="{{ heroButtonHref }}">{% if section.primaryLabel != nil %}<span data-p60-field="primaryLabel">{{ section.primaryLabel }}</span>{% else %}{{ heroButtonLabel }}{% endif %}</a>{% endif %}
71
+ {% if section.secondaryLabel != nil and section.secondaryLabel != '' and section.secondaryHref != nil and section.secondaryHref != '' %}<a class="button" href="{{ section.secondaryHref }}"><span data-p60-field="secondaryLabel">{{ section.secondaryLabel }}</span></a>{% endif %}
68
72
  </div>
69
73
  </div>
70
- <aside id="donate">
71
- {%- comment -%} The site focus decides which widget leads (section.primary); this island becomes it: the donation widget or the volunteer sign-up. {%- endcomment -%}
74
+ {% if site.actions.widget != nil and site.actions.widget != 'none' %}<aside id="donate">
75
+ {%- comment -%} site.actions.widget selects the platform widget. The template places it without changing that choice. {%- endcomment -%}
72
76
  {% island 'primary_action_widget' %}
73
- </aside>
77
+ </aside>{% endif %}
74
78
  </div>
75
79
  </section>
76
80
  {% endif %}
@@ -94,23 +98,23 @@
94
98
  {% endif %}
95
99
  <div class="container lq-homehero-inner">
96
100
  <div>
97
- <p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow | default: site.brand.name }}</p>
98
- <h1 data-p60-field="title">{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
101
+ {% if section.eyebrow != nil and section.eyebrow != '' %}<p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow }}</p>{% endif %}
102
+ {% if section.title != nil and section.title != '' %}<h1 data-p60-field="title">{{ section.title }}</h1>{% endif %}
99
103
  {% if section.lead %}<p class="lq-homehero-lead" data-p60-field="lead">{{ section.lead }}</p>{% endif %}
100
104
  <div class="lq-homehero-actions">
101
- {% if section.actionStyle == 'button' or section.givingStyle == 'button' %}
102
- {% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '/donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %}
105
+ {% if section.actionStyle == 'button' %}
106
+ {% if heroButtonLabel != nil and heroButtonLabel != '' and heroButtonHref != nil and heroButtonHref != '' %}<a class="button button-primary" href="{{ heroButtonHref }}">{% if section.primaryLabel != nil %}<span data-p60-field="primaryLabel">{{ section.primaryLabel }}</span>{% else %}{{ heroButtonLabel }}{% endif %}</a>{% endif %}
103
107
  {% else %}
104
- {% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '#donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %}
108
+ {% if heroButtonLabel != nil and heroButtonLabel != '' and heroButtonHref != nil and heroButtonHref != '' %}<a class="button button-primary" href="{{ heroButtonHref }}">{% if section.primaryLabel != nil %}<span data-p60-field="primaryLabel">{{ section.primaryLabel }}</span>{% else %}{{ heroButtonLabel }}{% endif %}</a>{% endif %}
105
109
  {% endif %}
106
- <a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a>
110
+ {% if section.secondaryLabel != nil and section.secondaryLabel != '' and section.secondaryHref != nil and section.secondaryHref != '' %}<a class="button" href="{{ section.secondaryHref }}"><span data-p60-field="secondaryLabel">{{ section.secondaryLabel }}</span></a>{% endif %}
107
111
  </div>
108
112
  </div>
109
- {% unless section.actionStyle == 'button' or section.givingStyle == 'button' %}
113
+ {% if section.actionStyle != 'button' and site.actions.widget != nil and site.actions.widget != 'none' %}
110
114
  <aside id="donate">
111
115
  {% island 'primary_action_widget' %}
112
116
  </aside>
113
- {% endunless %}
117
+ {% endif %}
114
118
  </div>
115
119
  </section>
116
120
  {% endif %}
@@ -1,6 +1,6 @@
1
1
  <section class="lq-values">
2
2
  <div class="container">
3
- <h2 data-p60-field="heading">{{ section.heading | default: "What guides us" }}</h2>
3
+ {% if section.heading != nil and section.heading != '' %}<h2 data-p60-field="heading">{{ section.heading }}</h2>{% endif %}
4
4
  {% comment %}
5
5
  data-p60-reveal-group: the engine stamps --p60-reveal-index on each child and adds
6
6
  is-revealed when the rail scrolls into view, the stagger and motion are OURS, in theme.css,