@port60/template-kit 0.4.2 → 0.6.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/package.json +1 -1
- package/src/lib/agentsMd.mjs +3 -0
- package/src/vendor/contract/v1/imagery.json +23 -0
- package/src/vendor/contract/v1/manifest.schema.json +29 -0
- package/src/vendor/contract/v1/sections.json +92 -2
- package/starter/assets/theme.css +27 -0
- package/starter/manifest.json +11 -3
- package/starter/sections/people.liquid +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@port60/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
package/src/lib/agentsMd.mjs
CHANGED
|
@@ -70,6 +70,9 @@ platform accepts it; if it fails here, the upload will fail identically.
|
|
|
70
70
|
behaviourally.
|
|
71
71
|
- "Looks" = named one-click bundles of knob values in \`manifest.looks\`.
|
|
72
72
|
- Fonts: only families from the platform font catalogue, declared with the weights you use.
|
|
73
|
+
- \`manifest.imagery.hero\` (optional but recommended): declare the photo shape YOUR hero
|
|
74
|
+
composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`) — the charity's editor
|
|
75
|
+
measures their actual upload against it and advises. Advice, never enforcement.
|
|
73
76
|
|
|
74
77
|
## Which contract this is
|
|
75
78
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Platform imagery envelopes — ADVICE, never enforcement. Entity images (events, courses, articles) render on SEVERAL surfaces at once (cards, carousel slides, detail banners), including platform-owned components, so their guidance is platform-level: the envelope every surface can live with. Template-composed slots (the home hero) are different — their ideal is the TEMPLATE's own design call, declared per-template in manifest.imagery and surfaced beside the upload. Editors measure the actual upload against the relevant guidance and advise; the charity always decides.",
|
|
3
|
+
"entityImages": {
|
|
4
|
+
"eventPromo": {
|
|
5
|
+
"idealAspect": "3:2",
|
|
6
|
+
"aspectRange": ["4:3", "16:9"],
|
|
7
|
+
"minWidth": 1600,
|
|
8
|
+
"note": "One photo, several surfaces: the events card, the home carousel slide, and the event page banner — each crops differently, so keep the subject centred and avoid text baked into the image."
|
|
9
|
+
},
|
|
10
|
+
"courseImage": {
|
|
11
|
+
"idealAspect": "3:2",
|
|
12
|
+
"aspectRange": ["4:3", "16:9"],
|
|
13
|
+
"minWidth": 1600,
|
|
14
|
+
"note": "Renders as the course card and the course page opener — subject centred, no baked-in text."
|
|
15
|
+
},
|
|
16
|
+
"articleCover": {
|
|
17
|
+
"idealAspect": "16:9",
|
|
18
|
+
"aspectRange": ["3:2", "2:1"],
|
|
19
|
+
"minWidth": 1600,
|
|
20
|
+
"note": "The article card thumbnail and the article page cover — wide editorial shots sit best."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -78,6 +78,35 @@
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
+
"imagery": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"additionalProperties": false,
|
|
84
|
+
"description": "The template's OWN image advice, surfaced beside uploads in the charity's editor — a design statement (what shape suits THIS composition), advisory only, never enforced. Entity images (events, courses, articles) are platform-scoped instead: see the contract's imagery.json envelopes.",
|
|
85
|
+
"properties": {
|
|
86
|
+
"hero": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"additionalProperties": false,
|
|
89
|
+
"required": ["idealAspect", "minWidth"],
|
|
90
|
+
"properties": {
|
|
91
|
+
"idealAspect": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"pattern": "^\\d+:\\d+$",
|
|
94
|
+
"description": "The aspect this hero composes best with, e.g. \"16:9\"."
|
|
95
|
+
},
|
|
96
|
+
"minWidth": {
|
|
97
|
+
"type": "integer",
|
|
98
|
+
"minimum": 800,
|
|
99
|
+
"description": "Below this pixel width the photo may look soft full-bleed."
|
|
100
|
+
},
|
|
101
|
+
"note": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"maxLength": 240,
|
|
104
|
+
"description": "One human line of advice, e.g. how panoramas or portraits behave in this hero."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
81
110
|
"fonts": {
|
|
82
111
|
"type": "array",
|
|
83
112
|
"items": {
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"name": "buttonHref",
|
|
143
143
|
"kind": "url",
|
|
144
144
|
"required": true,
|
|
145
|
-
"description": "The button's destination (scheme-guarded on save)."
|
|
145
|
+
"description": "The button's destination (scheme-guarded on save). The editor's cause picker writes /donate/{slug} here and records causeId beside it (editor bookkeeping — renderers use buttonHref only and may ignore causeId)."
|
|
146
146
|
}
|
|
147
147
|
],
|
|
148
148
|
"minimal": {
|
|
@@ -575,7 +575,7 @@
|
|
|
575
575
|
"name": "buttonHref",
|
|
576
576
|
"kind": "url",
|
|
577
577
|
"required": false,
|
|
578
|
-
"description": "Card action link (default: /donate). Scheme-guarded on save."
|
|
578
|
+
"description": "Card action link (default: /donate). Scheme-guarded on save. The cause picker writes /donate/{slug} and records causeId beside it (editor bookkeeping; renderers use buttonHref only)."
|
|
579
579
|
}
|
|
580
580
|
]
|
|
581
581
|
}
|
|
@@ -606,6 +606,96 @@
|
|
|
606
606
|
}
|
|
607
607
|
]
|
|
608
608
|
}
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
"type": "people",
|
|
612
|
+
"label": "People / team",
|
|
613
|
+
"pages": [
|
|
614
|
+
"about"
|
|
615
|
+
],
|
|
616
|
+
"description": "The key people behind the charity — trustees, staff, volunteers — as simple cards: name, position, an optional photograph (uploaded through the platform pipeline) and an optional one-line statement. Templates render a card grid that collapses to a single column on small screens; a card without a photo renders cleanly (initials or plain), never a placeholder face.",
|
|
617
|
+
"fields": [
|
|
618
|
+
{
|
|
619
|
+
"name": "eyebrow",
|
|
620
|
+
"kind": "text",
|
|
621
|
+
"required": false,
|
|
622
|
+
"description": "Short kicker, e.g. Who we are."
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
"name": "heading",
|
|
626
|
+
"kind": "text",
|
|
627
|
+
"required": false,
|
|
628
|
+
"description": "The section heading, e.g. Our team."
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"name": "text",
|
|
632
|
+
"kind": "text",
|
|
633
|
+
"required": false,
|
|
634
|
+
"description": "An optional intro line."
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
"name": "items",
|
|
638
|
+
"kind": "items",
|
|
639
|
+
"required": true,
|
|
640
|
+
"description": "The people.",
|
|
641
|
+
"itemFields": [
|
|
642
|
+
{
|
|
643
|
+
"name": "name",
|
|
644
|
+
"kind": "text",
|
|
645
|
+
"required": true,
|
|
646
|
+
"description": "The person's name."
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
"name": "role",
|
|
650
|
+
"kind": "text",
|
|
651
|
+
"required": true,
|
|
652
|
+
"description": "Their position, e.g. Chair of Trustees."
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"name": "imageUrl",
|
|
656
|
+
"kind": "image",
|
|
657
|
+
"required": false,
|
|
658
|
+
"description": "A photograph (platform pipeline). Absent renders cleanly — never a placeholder face."
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"name": "statement",
|
|
662
|
+
"kind": "text",
|
|
663
|
+
"required": false,
|
|
664
|
+
"description": "One line in their voice, e.g. why they serve."
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
],
|
|
669
|
+
"minimal": {
|
|
670
|
+
"items": [
|
|
671
|
+
{
|
|
672
|
+
"name": "Amina Khan",
|
|
673
|
+
"role": "Chair of Trustees"
|
|
674
|
+
}
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
"sample": {
|
|
678
|
+
"eyebrow": "Who we are",
|
|
679
|
+
"heading": "The people behind the work",
|
|
680
|
+
"text": "A small team and a lot of volunteers, accountable to you.",
|
|
681
|
+
"items": [
|
|
682
|
+
{
|
|
683
|
+
"name": "Amina Khan",
|
|
684
|
+
"role": "Chair of Trustees",
|
|
685
|
+
"imageUrl": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23355e4b'/%3E%3Ccircle cx='48' cy='38' r='16' fill='%23e8e2d4'/%3E%3Cpath d='M16 88c4-20 18-28 32-28s28 8 32 28z' fill='%23e8e2d4'/%3E%3C/svg%3E",
|
|
686
|
+
"statement": "Every pound is a trust — we treat it that way."
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
"name": "Daniel Osei",
|
|
690
|
+
"role": "Treasurer"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
"name": "Sara Malik",
|
|
694
|
+
"role": "Volunteer Coordinator",
|
|
695
|
+
"statement": "Fifty pairs of hands, one purpose."
|
|
696
|
+
}
|
|
697
|
+
]
|
|
698
|
+
}
|
|
609
699
|
}
|
|
610
700
|
]
|
|
611
701
|
}
|
package/starter/assets/theme.css
CHANGED
|
@@ -160,3 +160,30 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
160
160
|
@media (max-width: 899px) {
|
|
161
161
|
.lq-vista-overlap { margin-top: 0; padding-top: 1.6rem; }
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/* People / team cards — grid to single column on phones. */
|
|
165
|
+
.lq-people {
|
|
166
|
+
display: grid;
|
|
167
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
168
|
+
gap: 1.2rem;
|
|
169
|
+
text-align: center;
|
|
170
|
+
}
|
|
171
|
+
.lq-person {
|
|
172
|
+
background: var(--surface);
|
|
173
|
+
border: 1px solid var(--line);
|
|
174
|
+
border-radius: 12px;
|
|
175
|
+
padding: 1.4rem 1.1rem;
|
|
176
|
+
}
|
|
177
|
+
.lq-person-photo {
|
|
178
|
+
width: 88px;
|
|
179
|
+
height: 88px;
|
|
180
|
+
border-radius: 50%;
|
|
181
|
+
object-fit: cover;
|
|
182
|
+
display: block;
|
|
183
|
+
margin: 0 auto 0.8rem;
|
|
184
|
+
}
|
|
185
|
+
.lq-person-role { margin: 0.15rem 0 0; font-size: 0.9rem; color: var(--muted); }
|
|
186
|
+
.lq-person-statement { margin: 0.7rem 0 0; font-style: italic; color: var(--muted); }
|
|
187
|
+
@media (max-width: 640px) {
|
|
188
|
+
.lq-people { grid-template-columns: 1fr; }
|
|
189
|
+
}
|
package/starter/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starter",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"format": "port60-liquid@1",
|
|
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.",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"homeHero",
|
|
14
14
|
"hero",
|
|
15
15
|
"values",
|
|
16
|
-
"cta"
|
|
16
|
+
"cta",
|
|
17
|
+
"people"
|
|
17
18
|
],
|
|
18
19
|
"islands": [
|
|
19
20
|
"donation_widget",
|
|
@@ -40,5 +41,12 @@
|
|
|
40
41
|
}
|
|
41
42
|
]
|
|
42
43
|
},
|
|
43
|
-
"changelog": "
|
|
44
|
+
"changelog": "People / team: the new About-page section — the key people behind the charity as cards, stacking on phones.",
|
|
45
|
+
"imagery": {
|
|
46
|
+
"hero": {
|
|
47
|
+
"idealAspect": "16:9",
|
|
48
|
+
"minWidth": 1600,
|
|
49
|
+
"note": "Landscape around 16:9 fills the hero cleanly; panoramas suit the whole-photo framing."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
44
52
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
People / team — the reference implementation: simple cards (name, role, optional photo and
|
|
3
|
+
one-line statement), a grid that collapses to one column on phones. No placeholder faces:
|
|
4
|
+
a person without a photo just renders text (derive or omit, never fabricate).
|
|
5
|
+
{% endcomment %}
|
|
6
|
+
<section class="lq-section">
|
|
7
|
+
<div class="container">
|
|
8
|
+
{% if section.eyebrow or section.heading or section.text %}
|
|
9
|
+
<div class="lq-section-head">
|
|
10
|
+
{% if section.eyebrow %}<p class="lq-kicker">{{ section.eyebrow }}</p>{% endif %}
|
|
11
|
+
{% if section.heading %}<h2>{{ section.heading }}</h2>{% endif %}
|
|
12
|
+
{% if section.text %}<p class="lq-muted">{{ section.text }}</p>{% endif %}
|
|
13
|
+
</div>
|
|
14
|
+
{% endif %}
|
|
15
|
+
<div class="lq-people">
|
|
16
|
+
{% for person in section.items %}
|
|
17
|
+
<article class="lq-person">
|
|
18
|
+
{% if person.imageUrl %}<img class="lq-person-photo" src="{{ person.imageUrl }}" alt="" loading="lazy">{% endif %}
|
|
19
|
+
<h3>{{ person.name }}</h3>
|
|
20
|
+
<p class="lq-person-role">{{ person.role }}</p>
|
|
21
|
+
{% if person.statement %}<p class="lq-person-statement">“{{ person.statement }}”</p>{% endif %}
|
|
22
|
+
</article>
|
|
23
|
+
{% endfor %}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|