@port60/template-kit 0.9.0 → 0.10.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 +10 -3
- package/bin/cli.mjs +26 -4
- package/package.json +1 -1
- package/src/commands/content.mjs +42 -0
- package/src/commands/create.mjs +6 -6
- package/src/commands/dev.mjs +97 -9
- package/src/commands/model.mjs +34 -0
- package/src/commands/packageCmd.mjs +3 -3
- package/src/commands/refresh.mjs +48 -0
- package/src/commands/upgrade.mjs +44 -0
- package/src/commands/validate.mjs +2 -3
- package/src/lib/agentsMd.mjs +47 -19
- package/src/vendor/contract/v1/behaviours.json +4 -4
- package/src/vendor/contract/v1/content-bounds.json +3 -3
- package/src/vendor/contract/v1/content-model.json +193 -0
- package/src/vendor/contract/v1/context.json +1667 -275
- package/src/vendor/contract/v1/dialect.json +3 -3
- package/src/vendor/contract/v1/fonts.json +1 -1
- package/src/vendor/contract/v1/imagery.json +4 -4
- package/src/vendor/contract/v1/islands.json +256 -235
- package/src/vendor/contract/v1/layout.json +4 -4
- package/src/vendor/contract/v1/manifest.schema.json +9 -9
- package/src/vendor/contract/v1/sections.json +248 -92
- package/src/vendor/contract/v1/tokens.json +1 -1
- package/src/vendor/contract/v1.lock.json +1 -1
- package/src/vendor/engine/content-footprint.mjs +79 -0
- package/src/vendor/validator/fixture-art.mjs +109 -0
- package/src/vendor/validator/model-reference.mjs +92 -0
- package/src/vendor/validator/platform-base.css +21 -0
- package/src/vendor/validator/preview.mjs +149 -56
- package/src/vendor/validator/site-context.mjs +97 -0
- package/src/vendor/validator/validate.mjs +71 -14
- package/starter/assets/theme.css +10 -10
- package/starter/layout.liquid +15 -16
- package/starter/manifest.json +2 -2
- package/starter/sections/campaigns.liquid +5 -5
- package/starter/sections/homeHero.liquid +11 -11
- package/starter/sections/people.liquid +1 -1
- package/starter/sections/values.liquid +1 -1
|
@@ -22,6 +22,7 @@ import contextContract from '../contract/v1/context.json' with { type: 'json' };
|
|
|
22
22
|
import fontCatalogue from '../contract/v1/fonts.json' with { type: 'json' };
|
|
23
23
|
import layoutContract from '../contract/v1/layout.json' with { type: 'json' };
|
|
24
24
|
import behaviourCatalogue from '../contract/v1/behaviours.json' with { type: 'json' };
|
|
25
|
+
import { buildSiteFixture, extractContentFootprint, contentModel } from './site-context.mjs';
|
|
25
26
|
|
|
26
27
|
const Ajv = Ajv2020.default ?? Ajv2020;
|
|
27
28
|
|
|
@@ -55,6 +56,18 @@ export async function validateArtifact(files) {
|
|
|
55
56
|
} catch (e) {
|
|
56
57
|
return { errors: [`manifest.json unreadable: ${e.message}`], warnings, manifest: null };
|
|
57
58
|
}
|
|
59
|
+
// preview-content.json is a DEV-ONLY data override: package excludes it and the intake
|
|
60
|
+
// refuses it — an artifact must never carry data, only shape.
|
|
61
|
+
if (has('preview-content.json')) {
|
|
62
|
+
errors.push('preview-content.json: development preview data never ships in an artifact — remove it (package excludes it automatically)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Content model v1: the footprint is decidable from the sources (closed dialect); unknown
|
|
66
|
+
// paths, dynamic indexing and tree aliasing are errors from site-context.mjs.
|
|
67
|
+
const contentAnalysis = extractContentFootprint(files);
|
|
68
|
+
errors.push(...contentAnalysis.errors);
|
|
69
|
+
const siteFx = buildSiteFixture(manifest);
|
|
70
|
+
|
|
58
71
|
const ajv = new Ajv({ allErrors: true });
|
|
59
72
|
const validateManifest = ajv.compile(manifestSchema);
|
|
60
73
|
if (!validateManifest(manifest)) {
|
|
@@ -212,15 +225,20 @@ export async function validateArtifact(files) {
|
|
|
212
225
|
// the data itself — in which case both directions are proven behaviourally (the worship
|
|
213
226
|
// pattern): the populated fixture's sentinel must appear, and the EMPTY context must render it
|
|
214
227
|
// away (derive or omit — nothing invented, nothing dangling).
|
|
228
|
+
// Sentinels are DERIVED from the canonical fixtures (the first item's display field), never
|
|
229
|
+
// hardcoded — the fixture data is free to become richer without touching a proof, and a proof
|
|
230
|
+
// can never drift from the data it renders.
|
|
231
|
+
const sentinelOf = (type, dataKey, field) =>
|
|
232
|
+
(contextContract.fixtures.sections?.[type]?.[dataKey] ?? [])[0]?.[field] ?? null;
|
|
215
233
|
const WIDGET_SECTIONS = {
|
|
216
|
-
events: { island: 'events_carousel', dataKey: 'events', sentinel: '
|
|
217
|
-
whatsOn: { island: 'whats_on_strip', dataKey: 'infoEvents', sentinel: '
|
|
218
|
-
articles: { island: 'latest_articles', dataKey: 'latestArticles', sentinel: '
|
|
219
|
-
campaigns: { island: null, dataKey: 'campaigns', sentinel: '
|
|
220
|
-
resources: { island: null, dataKey: 'resources', sentinel: '
|
|
221
|
-
locations: { island: null, dataKey: 'locations', sentinel: '
|
|
222
|
-
volunteering: { island: 'volunteer_signup', dataKey: 'opportunities', sentinel: '
|
|
223
|
-
media: { island: null, dataKey: 'media', sentinel: '
|
|
234
|
+
events: { island: 'events_carousel', dataKey: 'events', sentinel: sentinelOf('events', 'events', 'name') },
|
|
235
|
+
whatsOn: { island: 'whats_on_strip', dataKey: 'infoEvents', sentinel: sentinelOf('whatsOn', 'infoEvents', 'name') },
|
|
236
|
+
articles: { island: 'latest_articles', dataKey: 'latestArticles', sentinel: sentinelOf('articles', 'latestArticles', 'title') },
|
|
237
|
+
campaigns: { island: null, dataKey: 'campaigns', sentinel: sentinelOf('campaigns', 'campaigns', 'title') },
|
|
238
|
+
resources: { island: null, dataKey: 'resources', sentinel: sentinelOf('resources', 'resources', 'title') },
|
|
239
|
+
locations: { island: null, dataKey: 'locations', sentinel: sentinelOf('locations', 'locations', 'name') },
|
|
240
|
+
volunteering: { island: 'volunteer_signup', dataKey: 'opportunities', sentinel: sentinelOf('volunteering', 'opportunities', 'title') },
|
|
241
|
+
media: { island: null, dataKey: 'media', sentinel: sentinelOf('media', 'media', 'title') }
|
|
224
242
|
};
|
|
225
243
|
|
|
226
244
|
// 2–4. Sections: catalogue membership, parse, fixture renders.
|
|
@@ -251,12 +269,13 @@ export async function validateArtifact(files) {
|
|
|
251
269
|
const populated = await liquid.render(parsed, {
|
|
252
270
|
section: {},
|
|
253
271
|
brand: contextContract.fixtures.brand,
|
|
272
|
+
site: siteFx,
|
|
254
273
|
...dataFixture
|
|
255
274
|
});
|
|
256
275
|
const placesIsland = widget.island !== null
|
|
257
276
|
&& splitIslandParts(populated).some((p) => p.island === widget.island);
|
|
258
277
|
if (!placesIsland) {
|
|
259
|
-
if (!populated.includes(widget.sentinel)) {
|
|
278
|
+
if (widget.sentinel && !populated.includes(widget.sentinel)) {
|
|
260
279
|
errors.push(
|
|
261
280
|
widget.island
|
|
262
281
|
? `section '${type}': neither places the ${widget.island} island nor renders the ${widget.dataKey} context — render the data (the fixture's "${widget.sentinel}" must appear) or place the island`
|
|
@@ -266,9 +285,10 @@ export async function validateArtifact(files) {
|
|
|
266
285
|
const empty = await liquid.render(parsed, {
|
|
267
286
|
section: {},
|
|
268
287
|
brand: contextContract.fixtures.brand,
|
|
288
|
+
site: { ...siteFx, content: Object.fromEntries(Object.keys(siteFx.content).map((k) => [k, []])) },
|
|
269
289
|
[widget.dataKey]: []
|
|
270
290
|
});
|
|
271
|
-
if (empty.includes(widget.sentinel)) {
|
|
291
|
+
if (widget.sentinel && empty.includes(widget.sentinel)) {
|
|
272
292
|
errors.push(`section '${type}': still shows fixture content with an empty ${widget.dataKey} — content must come from the context`);
|
|
273
293
|
}
|
|
274
294
|
if (/\bundefined\b|\bnull\b/.test(empty.replace(/data-[a-z-]+="[^"]*"/g, ''))) {
|
|
@@ -285,19 +305,23 @@ export async function validateArtifact(files) {
|
|
|
285
305
|
const html = await liquid.render(parsed, {
|
|
286
306
|
section: fixture,
|
|
287
307
|
brand: contextContract.fixtures.brand,
|
|
308
|
+
site: siteFx,
|
|
288
309
|
// Widget data rides the ordinary fixture renders too, so a hand-rendering section
|
|
289
310
|
// doesn't fail the generic pass for want of its context.
|
|
290
311
|
...(widget ? (contextContract.fixtures.sections?.[type] ?? {}) : {})
|
|
291
312
|
});
|
|
292
313
|
if (type === 'homeHero' && fixtureName === 'sample') {
|
|
293
|
-
|
|
314
|
+
// The sample's own first image reference is the probe: `p60fixture:` refs are quote-free
|
|
315
|
+
// and survive HTML escaping, so "does the hero display the photos?" stays a substring check.
|
|
316
|
+
const probe = (fixture.images ?? [])[0]?.imageUrl ?? 'p60fixture:';
|
|
294
317
|
homeHeroMultiShows = html.includes(probe)
|
|
295
318
|
|| splitIslandParts(html).some((p) => p.island === 'hero_carousel');
|
|
296
319
|
// The single-photo path proven separately: same fixture, first photo only.
|
|
297
320
|
try {
|
|
298
321
|
const single = await liquid.render(parsed, {
|
|
299
322
|
section: { ...fixture, images: (fixture.images ?? []).slice(0, 1) },
|
|
300
|
-
brand: contextContract.fixtures.brand
|
|
323
|
+
brand: contextContract.fixtures.brand,
|
|
324
|
+
site: siteFx
|
|
301
325
|
});
|
|
302
326
|
homeHeroSingleShows = single.includes(probe);
|
|
303
327
|
} catch {
|
|
@@ -359,6 +383,7 @@ export async function validateArtifact(files) {
|
|
|
359
383
|
if (parsedLayout) {
|
|
360
384
|
try {
|
|
361
385
|
const html = await liquid.render(parsedLayout, {
|
|
386
|
+
site: siteFx,
|
|
362
387
|
brand: contextContract.fixtures.brand,
|
|
363
388
|
nav: contextContract.fixtures.layout.nav,
|
|
364
389
|
socials: contextContract.fixtures.layout.socials ?? [],
|
|
@@ -394,10 +419,12 @@ export async function validateArtifact(files) {
|
|
|
394
419
|
}
|
|
395
420
|
if (manifest?.supports?.worship) {
|
|
396
421
|
const nullHtml = await liquid.render(parsedLayout, {
|
|
422
|
+
site: siteFx,
|
|
397
423
|
brand: contextContract.fixtures.brand,
|
|
398
424
|
nav: contextContract.fixtures.layout.nav,
|
|
399
425
|
socials: contextContract.fixtures.layout.socials ?? [],
|
|
400
426
|
worship: null,
|
|
427
|
+
site: { ...siteFx, content: { ...siteFx.content, schedules: [] } },
|
|
401
428
|
locale: contextContract.fixtures.layout.locale
|
|
402
429
|
});
|
|
403
430
|
if (nullHtml.includes(worshipProbe)) {
|
|
@@ -434,7 +461,7 @@ export async function validateArtifact(files) {
|
|
|
434
461
|
continue;
|
|
435
462
|
}
|
|
436
463
|
try {
|
|
437
|
-
const html = await liquid.render(parsedPage, { ...fixture, brand: contextContract.fixtures.brand });
|
|
464
|
+
const html = await liquid.render(parsedPage, { ...fixture, site: siteFx, brand: contextContract.fixtures.brand });
|
|
438
465
|
for (const part of splitIslandParts(html)) {
|
|
439
466
|
if (part.island === CONTENT_SLOT) {
|
|
440
467
|
errors.push(`page template '${pageName}': uses {% content %} — that tag is layout-only`);
|
|
@@ -495,5 +522,35 @@ export async function validateArtifact(files) {
|
|
|
495
522
|
}
|
|
496
523
|
}
|
|
497
524
|
|
|
498
|
-
|
|
525
|
+
// Open enums, proven survivable (content model v1 discipline 2): a template whose footprint
|
|
526
|
+
// reads site.content.events must survive a registration mode it has never heard of — new modes
|
|
527
|
+
// WILL arrive within the major. No throw, and no undefined/null literal leaking into markup.
|
|
528
|
+
if (contentAnalysis.footprint.includes('content.events')) {
|
|
529
|
+
const futureEvent = {
|
|
530
|
+
...(siteFx.content.events[0] ?? {}),
|
|
531
|
+
id: 'ffffffff-ffff-4fff-8fff-ffffffffffff',
|
|
532
|
+
name: 'Open Enum Probe Event',
|
|
533
|
+
registrationMode: 'X_FUTURE_MODE',
|
|
534
|
+
detailHref: '/events?event=ffffffff-ffff-4fff-8fff-ffffffffffff'
|
|
535
|
+
};
|
|
536
|
+
const doctored = { ...siteFx, content: { ...siteFx.content, events: [...siteFx.content.events, futureEvent] } };
|
|
537
|
+
for (const type of manifest?.supports?.sections ?? []) {
|
|
538
|
+
const file = `sections/${type}.liquid`;
|
|
539
|
+
if (!has(file) || !/\bsite\s*[.[]/.test(read(file))) continue;
|
|
540
|
+
try {
|
|
541
|
+
const html = await liquid.render(liquid.parse(read(file)), {
|
|
542
|
+
section: catalogueByType.get(type)?.sample ?? {},
|
|
543
|
+
brand: contextContract.fixtures.brand,
|
|
544
|
+
site: doctored
|
|
545
|
+
});
|
|
546
|
+
if (/\bundefined\b|\bnull\b/.test(html.replace(/data-[a-z-]+="[^"]*"/g, ''))) {
|
|
547
|
+
errors.push(`section '${type}': renders 'undefined'/'null' literals for an unknown event registrationMode — the enum is OPEN, branch on the modes you style and fall back for the rest`);
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {
|
|
550
|
+
errors.push(`section '${type}': failed rendering an unknown event registrationMode — the enum is OPEN and new modes will arrive (${e.message})`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return { errors, warnings, manifest, contentFootprint: contentAnalysis.footprint, minContentVersion: contentAnalysis.minModelVersion };
|
|
499
556
|
}
|
package/starter/assets/theme.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* starter theme
|
|
1
|
+
/* starter theme, a deliberately distinct look (midnight/emerald) proving the artifact owns the skin */
|
|
2
2
|
:root:root {
|
|
3
3
|
--bg: #0e1614;
|
|
4
4
|
--bg-tint: #14201d;
|
|
@@ -39,13 +39,13 @@ strong { color: var(--ink); }
|
|
|
39
39
|
.lq-cta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem; align-items: start; }
|
|
40
40
|
@media (max-width: 900px) { .lq-cta-grid { grid-template-columns: 1fr; } }
|
|
41
41
|
|
|
42
|
-
/* The one font slot
|
|
42
|
+
/* The one font slot, headings above already follow it; body text too. */
|
|
43
43
|
body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-sans-serif, system-ui, sans-serif; }
|
|
44
44
|
|
|
45
|
-
/* ── Home hero (supports.heroImagery
|
|
45
|
+
/* ── Home hero (supports.heroImagery, the reference treatment) ─────────────────────────────
|
|
46
46
|
The no-photo state is a DESIGNED gradient (rule: derive or omit, never a placeholder image).
|
|
47
47
|
Photos get a scrim built from the palette itself via color-mix, so uploads tone into the
|
|
48
|
-
template
|
|
48
|
+
template, and any palette change re-tones them for free. Never place a hero photo raw. */
|
|
49
49
|
.lq-homehero {
|
|
50
50
|
position: relative;
|
|
51
51
|
padding: 4.5rem 0;
|
|
@@ -73,7 +73,7 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
73
73
|
.lq-homehero-lead { color: var(--muted); max-width: 46ch; margin: 0.9rem 0 1.4rem; }
|
|
74
74
|
.lq-homehero-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; }
|
|
75
75
|
|
|
76
|
-
/* The platform hero_carousel island, styled through its class API
|
|
76
|
+
/* The platform hero_carousel island, styled through its class API, the island owns motion
|
|
77
77
|
(auto-advance, swipe, reduced-motion), this owns the look. Each slide's .hero-slide-scrim is
|
|
78
78
|
the same palette treatment as a single photo. */
|
|
79
79
|
.lq-homehero--carousel { overflow: hidden; }
|
|
@@ -114,7 +114,7 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
114
114
|
}
|
|
115
115
|
.lq-homehero--carousel .hero-dot--active { background: var(--accent); }
|
|
116
116
|
|
|
117
|
-
/* ── Vista framing (photoFraming 'whole')
|
|
117
|
+
/* ── Vista framing (photoFraming 'whole'), the hero adapts to the PHOTO's shape ───────────── */
|
|
118
118
|
.lq-homehero--vista { padding-top: 0; }
|
|
119
119
|
.lq-vista { position: relative; overflow: hidden; }
|
|
120
120
|
.lq-vista-sizer { display: block; width: 100%; height: auto; max-height: 74vh; object-fit: cover; }
|
|
@@ -153,7 +153,7 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
153
153
|
.lq-vista-sizer { min-height: 160px; }
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/* Vista + form (1.5.2): the widget rides the seam
|
|
156
|
+
/* Vista + form (1.5.2): the widget rides the seam, grid pulled up over the band (under the
|
|
157
157
|
280px floor), gradient filling where the image ends; narrow screens stack back underneath. */
|
|
158
158
|
.lq-homehero--vista-form { padding-bottom: 2.6rem; }
|
|
159
159
|
.lq-vista-overlap { position: relative; z-index: 1; margin-top: -240px; align-items: start; }
|
|
@@ -161,7 +161,7 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
161
161
|
.lq-vista-overlap { margin-top: 0; padding-top: 1.6rem; }
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/* People / team cards
|
|
164
|
+
/* People / team cards, grid to single column on phones. */
|
|
165
165
|
.lq-people {
|
|
166
166
|
display: grid;
|
|
167
167
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
@@ -188,13 +188,13 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
188
188
|
.lq-people { grid-template-columns: 1fr; }
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
/* Campaigns
|
|
191
|
+
/* Campaigns, the same quiet rail as values: title, summary, one action. */
|
|
192
192
|
.lq-campaigns { padding: 3.5rem 0; }
|
|
193
193
|
.lq-campaign-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.6rem; max-width: 700px; }
|
|
194
194
|
.lq-campaign h3 { margin: 0 0 0.25rem; }
|
|
195
195
|
.lq-campaign p { margin: 0 0 0.6rem; }
|
|
196
196
|
|
|
197
|
-
/* Behaviours (engine-wired via data-p60
|
|
197
|
+
/* Behaviours (engine-wired via data-p60-*, docs: Motion and behaviour guide). Everything that
|
|
198
198
|
hides pre-behaviour content is .p60-js-scoped: the no-JS render stays complete. */
|
|
199
199
|
|
|
200
200
|
/* Reveal: the values rail rises in with a stagger. */
|
package/starter/layout.liquid
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
{% comment %}
|
|
2
|
-
The starter's chrome
|
|
2
|
+
The starter's chrome, header/nav/footer around the {% content %} slot. It reuses the platform
|
|
3
3
|
chrome classes (.site-header/.site-nav/…) so the base mechanics (sticky header, burger
|
|
4
4
|
breakpoint, dropdown hover) apply, and emits the documented behaviour attributes
|
|
5
5
|
(data-nav-burger, #site-nav) so the platform tail script wires the mobile burger. Identity is
|
|
6
|
-
PLACED, never rebuilt: {% island 'member_menu' %}. The footer derives everything from brand/nav
|
|
7
|
-
no fabricated copy (derive or omit).
|
|
6
|
+
PLACED, never rebuilt: {% island 'member_menu' %}. The footer derives everything from brand/nav, no fabricated copy (derive or omit).
|
|
8
7
|
{% endcomment %}
|
|
9
8
|
<header class="site-header">
|
|
10
9
|
<div class="container header-inner">
|
|
11
10
|
<a class="brand-lockup" href="/">
|
|
12
|
-
{% if brand.logoUrl and brand.logoType != 'icon' %}
|
|
13
|
-
<img class="brand-logo" src="{{ brand.logoUrl }}" alt="{{ brand.name }}">
|
|
14
|
-
{% elsif brand.logoUrl %}
|
|
15
|
-
<img class="brand-logo-icon" src="{{ brand.logoUrl }}" alt="">
|
|
16
|
-
<strong>{{ brand.name }}</strong>
|
|
11
|
+
{% if site.brand.logoUrl and site.brand.logoType != 'icon' %}
|
|
12
|
+
<img class="brand-logo" src="{{ site.brand.logoUrl }}" alt="{{ site.brand.name }}">
|
|
13
|
+
{% elsif site.brand.logoUrl %}
|
|
14
|
+
<img class="brand-logo-icon" src="{{ site.brand.logoUrl }}" alt="">
|
|
15
|
+
<strong>{{ site.brand.name }}</strong>
|
|
17
16
|
{% else %}
|
|
18
17
|
<span class="brand-mark" aria-hidden="true">♥</span>
|
|
19
|
-
<strong>{{ brand.name }}</strong>
|
|
18
|
+
<strong>{{ site.brand.name }}</strong>
|
|
20
19
|
{% endif %}
|
|
21
20
|
</a>
|
|
22
21
|
|
|
@@ -27,7 +26,7 @@
|
|
|
27
26
|
</button>
|
|
28
27
|
|
|
29
28
|
<nav class="site-nav" id="site-nav" aria-label="Primary">
|
|
30
|
-
{% for item in nav.items %}
|
|
29
|
+
{% for item in site.nav.items %}
|
|
31
30
|
{% if item.children %}
|
|
32
31
|
<span class="nav-dropdown">
|
|
33
32
|
<a href="{{ item.href }}" class="nav-dropdown-toggle" aria-haspopup="true">{{ item.label }} ▾</a>
|
|
@@ -55,20 +54,20 @@
|
|
|
55
54
|
<footer class="site-footer">
|
|
56
55
|
<div class="container footer-grid">
|
|
57
56
|
<div>
|
|
58
|
-
<p class="lq-kicker">{{ brand.name }}</p>
|
|
59
|
-
{% if brand.tagline %}<h2>{{ brand.tagline }}</h2>{% endif %}
|
|
57
|
+
<p class="lq-kicker">{{ site.brand.name }}</p>
|
|
58
|
+
{% if site.brand.tagline %}<h2>{{ site.brand.tagline }}</h2>{% endif %}
|
|
60
59
|
</div>
|
|
61
60
|
<div class="footer-links">
|
|
62
|
-
{% comment %} A tenant-curated footer menu when they've built one, else the main nav. {% endcomment %}
|
|
63
|
-
{% if nav.footer.size > 0 %}
|
|
64
|
-
{% for item in nav.footer %}
|
|
61
|
+
{% comment %} A tenant-curated footer menu when they've built one, else the main site.nav. {% endcomment %}
|
|
62
|
+
{% if site.nav.footer.size > 0 %}
|
|
63
|
+
{% for item in site.nav.footer %}
|
|
65
64
|
<a href="{{ item.href }}"{% if item.external %} target="_blank" rel="noopener"{% endif %}>{{ item.label }}</a>
|
|
66
65
|
{% for child in item.children %}
|
|
67
66
|
<a href="{{ child.href }}"{% if child.external %} target="_blank" rel="noopener"{% endif %}>{{ child.label }}</a>
|
|
68
67
|
{% endfor %}
|
|
69
68
|
{% endfor %}
|
|
70
69
|
{% else %}
|
|
71
|
-
{% for item in nav.items %}
|
|
70
|
+
{% for item in site.nav.items %}
|
|
72
71
|
<a href="{{ item.href }}">{{ item.label }}</a>
|
|
73
72
|
{% endfor %}
|
|
74
73
|
{% endif %}
|
package/starter/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"format": "port60-liquid@1",
|
|
5
5
|
"label": "Starter",
|
|
6
|
-
"description": "The reference template for the Port60 dialect
|
|
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": {
|
|
8
8
|
"pages": [
|
|
9
9
|
"home",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{% comment %}
|
|
2
|
-
Live campaigns
|
|
2
|
+
Live site.content.campaigns, the starter's plain rendering of the `site.content.campaigns` context, kept simple on
|
|
3
3
|
purpose: this template is the worked example template authors copy from. The context is the
|
|
4
|
-
platform's published campaigns; hrefs are platform-derived
|
|
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 campaigns.size > 0 %}
|
|
8
|
-
<section class="lq-campaigns">
|
|
7
|
+
{% if site.content.campaigns.size > 0 %}
|
|
8
|
+
<section class="lq-site.content.campaigns">
|
|
9
9
|
<div class="container">
|
|
10
10
|
<h2>{{ section.heading | default: campaignsLabel }}</h2>
|
|
11
11
|
{% if section.text %}<p>{{ section.text }}</p>{% endif %}
|
|
12
12
|
<ul class="lq-campaign-list">
|
|
13
|
-
{% for campaign in campaigns %}
|
|
13
|
+
{% for campaign in site.content.campaigns %}
|
|
14
14
|
<li class="lq-campaign">
|
|
15
15
|
<h3>{{ campaign.title }}</h3>
|
|
16
16
|
{% if campaign.summary %}<p>{{ campaign.summary }}</p>{% endif %}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{% comment %}
|
|
2
|
-
The home opener
|
|
2
|
+
The home opener, and the REFERENCE IMPLEMENTATION of supports.heroImagery. Rules worth
|
|
3
3
|
copying:
|
|
4
4
|
|
|
5
5
|
1. EVERY field is optional. Copy derives from brand (| default) or omits; no photos means the
|
|
6
|
-
designed gradient below
|
|
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
|
|
9
9
|
into the template. Tune the treatment, don't remove it.
|
|
10
10
|
3. Rotation is the PLATFORM's hero_carousel island (templates ship no JavaScript): with two or
|
|
11
11
|
more photos, place it and style its documented classes.
|
|
12
12
|
4. photoFraming is the charity's call, honour BOTH: 'fill' (default) covers your designed hero
|
|
13
|
-
shape (per-photo focus picks the surviving slice); 'whole' is the VISTA
|
|
13
|
+
shape (per-photo focus picks the surviving slice); 'whole' is the VISTA, the hero adapts
|
|
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 %}
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
{% if heroCount > 1 %}{% island 'hero_carousel' %}{% else %}<div class="lq-vista-scrim" aria-hidden="true"></div>{% endif %}
|
|
39
39
|
<div class="lq-vista-copy">
|
|
40
40
|
<div class="container">
|
|
41
|
-
<p class="lq-kicker">{{ section.eyebrow | default: brand.name }}</p>
|
|
42
|
-
<h1>{{ section.title | default: brand.tagline | default: brand.name }}</h1>
|
|
41
|
+
<p class="lq-kicker">{{ section.eyebrow | default: site.brand.name }}</p>
|
|
42
|
+
<h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
|
|
43
43
|
{% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %}
|
|
44
44
|
<div class="lq-homehero-actions">
|
|
45
45
|
<a class="button button-primary" href="{{ section.primaryHref | default: '/donate' }}">{{ section.primaryLabel | default: 'Donate now' }}</a>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
</div>
|
|
51
51
|
</section>
|
|
52
52
|
{% else %}
|
|
53
|
-
{% comment %} Form mode: the grid pulls up over the band
|
|
53
|
+
{% comment %} Form mode: the grid pulls up over the band, copy on the photo, the widget riding
|
|
54
54
|
the seam onto the gradient below (never a stacked strip). {% endcomment %}
|
|
55
55
|
<section class="lq-homehero lq-homehero--vista lq-homehero--vista-form{% if heroCount > 1 %} lq-homehero--carousel{% endif %}">
|
|
56
56
|
<div class="lq-vista">
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
</div>
|
|
60
60
|
<div class="container lq-homehero-inner lq-vista-overlap">
|
|
61
61
|
<div>
|
|
62
|
-
<p class="lq-kicker">{{ section.eyebrow | default: brand.name }}</p>
|
|
63
|
-
<h1>{{ section.title | default: brand.tagline | default: brand.name }}</h1>
|
|
62
|
+
<p class="lq-kicker">{{ section.eyebrow | default: site.brand.name }}</p>
|
|
63
|
+
<h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
|
|
64
64
|
{% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %}
|
|
65
65
|
<div class="lq-homehero-actions">
|
|
66
66
|
<a class="button button-primary" href="#donate">{{ section.primaryLabel | default: 'Donate now' }}</a>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
{% else %}
|
|
77
77
|
<section class="lq-homehero{% if heroPhoto %} lq-homehero--photo{% endif %}{% if heroCount > 1 %} lq-homehero--carousel{% endif %}"{% if heroPhoto %} style="background-image: url('{{ heroPhoto }}'); background-position: {{ heroFocus }}"{% endif %}>
|
|
78
78
|
{% comment %}
|
|
79
|
-
Several photos → the BEHAVIOUR carousel (data-p60-carousel, engine-wired
|
|
79
|
+
Several photos → the BEHAVIOUR carousel (data-p60-carousel, engine-wired, the template ships
|
|
80
80
|
no JavaScript): complete static markup, one data-p60-slide per photo, auto-advance opt-in.
|
|
81
81
|
Hiding non-active slides is .p60-js-scoped in theme.css so a no-JS visitor sees the photos
|
|
82
82
|
stacked, never a blank hero. The vista framings above show the other route: the ready-made
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
{% endif %}
|
|
94
94
|
<div class="container lq-homehero-inner">
|
|
95
95
|
<div>
|
|
96
|
-
<p class="lq-kicker">{{ section.eyebrow | default: brand.name }}</p>
|
|
97
|
-
<h1>{{ section.title | default: brand.tagline | default: brand.name }}</h1>
|
|
96
|
+
<p class="lq-kicker">{{ section.eyebrow | default: site.brand.name }}</p>
|
|
97
|
+
<h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
|
|
98
98
|
{% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %}
|
|
99
99
|
<div class="lq-homehero-actions">
|
|
100
100
|
{% if section.givingStyle == 'button' %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{% comment %}
|
|
2
|
-
People / team
|
|
2
|
+
People / team, the reference implementation: simple cards (name, role, optional photo and
|
|
3
3
|
one-line statement), a grid that collapses to one column on phones. No placeholder faces:
|
|
4
4
|
a person without a photo just renders text (derive or omit, never fabricate).
|
|
5
5
|
{% endcomment %}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<h2>{{ section.heading | default: "What guides us" }}</h2>
|
|
4
4
|
{% comment %}
|
|
5
5
|
data-p60-reveal-group: the engine stamps --p60-reveal-index on each child and adds
|
|
6
|
-
is-revealed when the rail scrolls into view
|
|
6
|
+
is-revealed when the rail scrolls into view, the stagger and motion are OURS, in theme.css,
|
|
7
7
|
and .p60-js-scoped so a no-JS visitor sees the rail complete and still.
|
|
8
8
|
{% endcomment %}
|
|
9
9
|
<ol class="lq-value-rail" data-p60-reveal-group>
|