@port60/template-kit 1.2.0 → 1.3.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 +46 -1
- package/package.json +1 -1
- package/src/commands/create.mjs +11 -0
- package/src/lib/agentsMd.mjs +176 -0
- package/src/vendor/contract/v2/dialect.json +2 -1
- package/src/vendor/contract/v2/islands.json +3 -2
- package/src/vendor/contract/v2/manifest.schema.json +73 -2
- package/src/vendor/contract/v2/presentation.json +107 -0
- package/src/vendor/contract/v2/sections.json +120 -4
- package/src/vendor/contract/v2.lock.json +562 -9
- package/src/vendor/engine/budgets.mjs +0 -6
- package/src/vendor/engine/colour-roles.mjs +68 -0
- package/src/vendor/engine/content-footprint.mjs +0 -11
- package/src/vendor/engine/design-settings.mjs +34 -0
- package/src/vendor/engine/dialect.mjs +3 -10
- package/src/vendor/engine/locale.mjs +0 -5
- package/src/vendor/engine/majors.mjs +0 -5
- package/src/vendor/engine/presentation-capabilities.mjs +0 -2
- package/src/vendor/engine/section-fields.mjs +64 -0
- package/src/vendor/engine/section-heading-alignment.mjs +0 -2
- package/src/vendor/engine/section-presentation.mjs +127 -0
- package/src/vendor/validator/behaviors-runtime.js +1 -1
- package/src/vendor/validator/collection-link-visibility.mjs +0 -1
- package/src/vendor/validator/colour-treatment.mjs +154 -0
- package/src/vendor/validator/fixture-art.mjs +0 -15
- package/src/vendor/validator/focus.mjs +0 -5
- package/src/vendor/validator/fonts.mjs +0 -6
- package/src/vendor/validator/heading-alignment.mjs +0 -3
- package/src/vendor/validator/icons.mjs +0 -4
- package/src/vendor/validator/image-overlay.mjs +141 -0
- package/src/vendor/validator/intro-photo-framing.mjs +101 -0
- package/src/vendor/validator/model-reference-v2.mjs +0 -4
- package/src/vendor/validator/model-reference.mjs +0 -4
- package/src/vendor/validator/navigation-highlights.mjs +0 -3
- package/src/vendor/validator/platform-base.css +23 -0
- package/src/vendor/validator/presentation-proof.mjs +0 -2
- package/src/vendor/validator/preview-v1.mjs +3 -103
- package/src/vendor/validator/preview-v2.mjs +12 -105
- package/src/vendor/validator/section-fields.mjs +41 -0
- package/src/vendor/validator/section-layout.mjs +156 -0
- package/src/vendor/validator/section-presentation.mjs +225 -0
- package/src/vendor/validator/site-context-v2.mjs +17 -2
- package/src/vendor/validator/site-context.mjs +0 -14
- package/src/vendor/validator/validate-v1.mjs +4 -98
- package/src/vendor/validator/validate-v2.mjs +44 -103
- package/src/vendor/validator/validate.mjs +0 -2
- package/starter/assets/theme.css +55 -0
- package/starter/manifest.json +20 -0
- package/starter/sections/cta.liquid +2 -2
- package/starter/sections/hero.liquid +5 -3
- package/starter/sections/homeHero.liquid +6 -6
- package/starter/sections/values.liquid +2 -2
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
// The Port60 template CONFORMANCE VALIDATOR as a PURE MODULE (developer program T1.3): the same
|
|
2
|
-
// checks the publish-time CLI has always run, callable with an in-memory file map, no fs, no
|
|
3
|
-
// argv, no process.exit, so the studio upload lane (an HTTP endpoint) and the CLI share ONE
|
|
4
|
-
// implementation, and "validated ⇒ renders in production" keeps holding: the Liquid instance is
|
|
5
|
-
// configured identically to the engine's, dialect enforcement is the same shared module, and the
|
|
6
|
-
// render budgets here match production's.
|
|
7
|
-
//
|
|
8
|
-
// validateArtifact(files) → { errors: string[], warnings: string[], manifest: object|null }
|
|
9
|
-
//
|
|
10
|
-
// `files` is a plain object of artifact-relative path → string content (manifest.json,
|
|
11
|
-
// layout.liquid, sections/*.liquid, pages/*.liquid, assets/theme.css).
|
|
12
1
|
import { Liquid } from 'liquidjs';
|
|
13
2
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
14
3
|
import { CONTENT_SLOT, configureDialect, splitIslandParts } from '../engine/dialect.mjs';
|
|
@@ -25,6 +14,10 @@ import behaviourCatalogue from '../contract/v2/behaviours.json' with { type: 'js
|
|
|
25
14
|
import { buildSiteFixture, extractContentFootprint, contentModel, emptyCollections, resolveSectionFixture } from './site-context-v2.mjs';
|
|
26
15
|
import { proveNavigationHighlights } from './navigation-highlights.mjs';
|
|
27
16
|
import { proveHeadingAlignment } from './heading-alignment.mjs';
|
|
17
|
+
import { proveSectionPresentation } from './section-presentation.mjs';
|
|
18
|
+
import { readSectionPresentation, SECTION_PRESENTATION_CONTROLS } from '../engine/section-presentation.mjs';
|
|
19
|
+
import { readSectionFields, sectionFieldCapabilities } from '../engine/section-fields.mjs';
|
|
20
|
+
import { proveSectionFields } from './section-fields.mjs';
|
|
28
21
|
import { proveCollectionLinkVisibility } from './collection-link-visibility.mjs';
|
|
29
22
|
import { proveNavigationModes } from './navigation-modes.mjs';
|
|
30
23
|
import { resolvedNavigationMode } from '../engine/presentation-capabilities.mjs';
|
|
@@ -34,11 +27,6 @@ const Ajv = Ajv2020.default ?? Ajv2020;
|
|
|
34
27
|
|
|
35
28
|
export { dialect as contractDialect, sectionCatalogue, islandRegistry, contextContract, behaviourCatalogue };
|
|
36
29
|
|
|
37
|
-
// The behaviour to opt-in-attribute map is DERIVED from the catalogue (FR-5): `primaryAttribute` on
|
|
38
|
-
// each entry feeds the checks below, the generated reference and the kit, so adding a behaviour is
|
|
39
|
-
// one catalogue entry plus its runtime initialiser. The trailing word boundary keeps the original
|
|
40
|
-
// semantics: a companion attribute that starts with the primary one (data-p60-reveal-group,
|
|
41
|
-
// data-p60-nav-item) counts as the behaviour in use.
|
|
42
30
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
31
|
export const BEHAVIOUR_PRIMARY_ATTR = Object.fromEntries(
|
|
44
32
|
behaviourCatalogue.behaviours.map((b) => [b.name, b.primaryAttribute])
|
|
@@ -47,8 +35,6 @@ const PRIMARY_ATTR = Object.fromEntries(
|
|
|
47
35
|
behaviourCatalogue.behaviours.map((b) => [b.name, [new RegExp(`${escapeRegExp(b.primaryAttribute)}\\b`), b.primaryAttribute]])
|
|
48
36
|
);
|
|
49
37
|
|
|
50
|
-
// Inline declarations only: preserve quoted/function values while discarding CSS comments.
|
|
51
|
-
// A URL in a custom property, quoted string or comment is not a rendered background image.
|
|
52
38
|
function heroStyleDeclarations(style = '') {
|
|
53
39
|
const declarations = [];
|
|
54
40
|
let value = '', quote = '', depth = 0;
|
|
@@ -98,7 +84,6 @@ function heroNodeCanRender(node) {
|
|
|
98
84
|
if (/^none$/i.test(heroInlineValue(declarations, ['display']))
|
|
99
85
|
|| /^(?:hidden|collapse)$/i.test(heroInlineValue(declarations, ['visibility']))
|
|
100
86
|
|| /^0(?:\.0*)?%?$/.test(heroInlineValue(declarations, ['opacity']))) return false;
|
|
101
|
-
// aria-hidden and inert do not visually hide decorative photographs or carousel slides.
|
|
102
87
|
}
|
|
103
88
|
return true;
|
|
104
89
|
}
|
|
@@ -111,10 +96,6 @@ function heroBackgroundShows(node, imageUrl) {
|
|
|
111
96
|
return false;
|
|
112
97
|
}
|
|
113
98
|
|
|
114
|
-
// Templates are markup and attributes, NEVER code (docs/template-behaviours.md). These are hard
|
|
115
|
-
// errors over the RAW liquid source, even inside comments, because there is no legitimate reason
|
|
116
|
-
// for the tokens to appear at all. The handler pattern names real DOM event families rather than
|
|
117
|
-
// matching any on* word, so attributes like `once` or `online` never false-positive.
|
|
118
99
|
const FORBIDDEN_MARKUP = [
|
|
119
100
|
[/<script\b/i, 'a <script> tag'],
|
|
120
101
|
[/<(iframe|object|embed)\b/i, 'an embedded frame or plugin element'],
|
|
@@ -129,7 +110,6 @@ export async function validateArtifact(files) {
|
|
|
129
110
|
const has = (path) => Object.hasOwn(files, path);
|
|
130
111
|
const read = (path) => files[path];
|
|
131
112
|
|
|
132
|
-
// 1. Manifest against the schema.
|
|
133
113
|
let manifest = null;
|
|
134
114
|
if (!has('manifest.json')) {
|
|
135
115
|
return { errors: ['manifest.json is missing, every artifact starts with its manifest'], warnings, manifest };
|
|
@@ -139,14 +119,10 @@ export async function validateArtifact(files) {
|
|
|
139
119
|
} catch (e) {
|
|
140
120
|
return { errors: [`manifest.json unreadable: ${e.message}`], warnings, manifest: null };
|
|
141
121
|
}
|
|
142
|
-
// preview-content.json is a DEV-ONLY data override: package excludes it and the intake
|
|
143
|
-
// refuses it, an artifact must never carry data, only shape.
|
|
144
122
|
if (has('preview-content.json')) {
|
|
145
123
|
errors.push('preview-content.json: development preview data never ships in an artifact, remove it (package excludes it automatically)');
|
|
146
124
|
}
|
|
147
125
|
|
|
148
|
-
// Content model v1: the footprint is decidable from the sources (closed dialect); unknown
|
|
149
|
-
// paths, dynamic indexing and tree aliasing are errors from site-context.mjs.
|
|
150
126
|
const contentAnalysis = extractContentFootprint(files);
|
|
151
127
|
errors.push(...contentAnalysis.errors);
|
|
152
128
|
const siteFx = buildSiteFixture(manifest);
|
|
@@ -159,10 +135,6 @@ export async function validateArtifact(files) {
|
|
|
159
135
|
}
|
|
160
136
|
}
|
|
161
137
|
|
|
162
|
-
// Capability declarations are catalogue MATCHING metadata, not an entitlement shortcut. Keep
|
|
163
|
-
// them honest by requiring one corresponding template-facing surface. The mapping is deliberately
|
|
164
|
-
// structural: it proves the design can present a capability without inspecting tenant data or
|
|
165
|
-
// crossing the platform-owned transaction, identity and consent boundaries.
|
|
166
138
|
{
|
|
167
139
|
const sections = new Set(manifest?.supports?.sections ?? []);
|
|
168
140
|
const islands = new Set(manifest?.supports?.islands ?? []);
|
|
@@ -191,10 +163,6 @@ export async function validateArtifact(files) {
|
|
|
191
163
|
}
|
|
192
164
|
}
|
|
193
165
|
|
|
194
|
-
// Site focus honesty (docs/volunteering.md): a template that claims it can lead with
|
|
195
|
-
// volunteering must give the volunteer sign-up a way into the hero, either the
|
|
196
|
-
// primary_action_widget island (which becomes the sign-up when volunteering leads) or the
|
|
197
|
-
// volunteer_signup island placed directly.
|
|
198
166
|
{
|
|
199
167
|
const focusKinds = new Set(manifest?.supports?.focus ?? []);
|
|
200
168
|
if (focusKinds.has('volunteer')) {
|
|
@@ -205,9 +173,6 @@ export async function validateArtifact(files) {
|
|
|
205
173
|
}
|
|
206
174
|
}
|
|
207
175
|
|
|
208
|
-
// Markup and attributes, NEVER code, the machine-enforced JavaScript ban over every liquid
|
|
209
|
-
// source (docs/template-behaviours.md). Behaviour is engine-owned; a template wanting motion
|
|
210
|
-
// declares supports.behaviors and uses the data-p60-* grammar.
|
|
211
176
|
for (const [path, source] of Object.entries(files)) {
|
|
212
177
|
if (!path.endsWith('.liquid')) continue;
|
|
213
178
|
for (const [pattern, what] of FORBIDDEN_MARKUP) {
|
|
@@ -217,9 +182,6 @@ export async function validateArtifact(files) {
|
|
|
217
182
|
}
|
|
218
183
|
}
|
|
219
184
|
|
|
220
|
-
// Behaviour declaration and usage must agree in BOTH directions. Source-level, deliberately:
|
|
221
|
-
// usage often sits inside content-dependent branches the fixtures never take, so the grammar's
|
|
222
|
-
// presence in the source is the honest minimal proof.
|
|
223
185
|
{
|
|
224
186
|
const declaredBehaviours = new Set(manifest?.supports?.behaviors ?? []);
|
|
225
187
|
const liquidSource = Object.entries(files)
|
|
@@ -266,11 +228,12 @@ export async function validateArtifact(files) {
|
|
|
266
228
|
}
|
|
267
229
|
}
|
|
268
230
|
|
|
269
|
-
// Font knobs: the default family must be a real catalogue entry (the tenant-unset render uses
|
|
270
|
-
// it), and the slot must declare the weights the template's typographic system needs.
|
|
271
231
|
{
|
|
272
232
|
const familyNames = new Set(fontCatalogue.families.map((f) => f.name));
|
|
273
233
|
for (const knob of manifest?.settings?.schema ?? []) {
|
|
234
|
+
if (['header', 'footer'].includes(knob.group) && manifest?.supports?.layout !== true) {
|
|
235
|
+
errors.push(`settings knob '${knob.key}': group '${knob.group}' requires supports.layout`);
|
|
236
|
+
}
|
|
274
237
|
if (knob.kind !== 'font') continue;
|
|
275
238
|
if (typeof knob.default !== 'string' || !familyNames.has(knob.default)) {
|
|
276
239
|
errors.push(`settings knob '${knob.key}': font default '${knob.default}' is not in the font catalogue`);
|
|
@@ -280,8 +243,6 @@ export async function validateArtifact(files) {
|
|
|
280
243
|
}
|
|
281
244
|
}
|
|
282
245
|
|
|
283
|
-
// Looks: every value must target a declared knob and be valid for it, a look that half-applies
|
|
284
|
-
// would leave the tenant in a state no author designed.
|
|
285
246
|
{
|
|
286
247
|
const knobByKey = new Map((manifest?.settings?.schema ?? []).map((k) => [k.key, k]));
|
|
287
248
|
const familyNames = new Set(fontCatalogue.families.map((f) => f.name));
|
|
@@ -304,8 +265,6 @@ export async function validateArtifact(files) {
|
|
|
304
265
|
}
|
|
305
266
|
}
|
|
306
267
|
|
|
307
|
-
// Engine identical to production: escape-by-default, strict filters, restricted dialect,
|
|
308
|
-
// and the SAME DoS budgets the live renderer runs with.
|
|
309
268
|
const availableIslands = new Set(
|
|
310
269
|
islandRegistry.islands.filter((i) => i.status === 'available').map((i) => i.name)
|
|
311
270
|
);
|
|
@@ -314,6 +273,20 @@ export async function validateArtifact(files) {
|
|
|
314
273
|
configureDialect(liquid, dialect, allIslands);
|
|
315
274
|
|
|
316
275
|
const catalogueByType = new Map(sectionCatalogue.sections.map((s) => [s.type, s]));
|
|
276
|
+
if (manifest?.supports?.sectionFields !== undefined && readSectionFields(manifest.supports.sectionFields, manifest.supports.sections) === null) {
|
|
277
|
+
errors.push('manifest: sectionFields requires supported section types and paired imageUrl/imageAlt fields, with optional photoFraming');
|
|
278
|
+
}
|
|
279
|
+
if (manifest?.supports?.sectionPresentation !== undefined
|
|
280
|
+
&& readSectionPresentation(manifest.supports.sectionPresentation, manifest.supports.sections) === null) {
|
|
281
|
+
errors.push('manifest: sectionPresentation requires supported section types, known controls, unique allowed options and a default among those options');
|
|
282
|
+
}
|
|
283
|
+
for (const [type, controls] of Object.entries(manifest?.supports?.sectionPresentation ?? {})) {
|
|
284
|
+
for (const key of Object.keys(controls ?? {})) {
|
|
285
|
+
const required = SECTION_PRESENTATION_CONTROLS[key]?.requiresSupport;
|
|
286
|
+
if (required && manifest?.supports?.[required] !== true) errors.push(`manifest: ${type} ${key} requires explicit supports.${required}`);
|
|
287
|
+
if (key === 'sectionLayout' && !sectionFieldCapabilities(manifest)[type]) errors.push(`manifest: ${type} sectionLayout requires explicit supports.sectionFields.${type} photograph fields`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
317
290
|
for (const type of Object.keys(manifest?.supports?.sectionHeadingAlignment ?? {})) {
|
|
318
291
|
if (!(manifest?.supports?.sections ?? []).includes(type)) {
|
|
319
292
|
errors.push(`manifest: supports.sectionHeadingAlignment '${type}' must also be declared in supports.sections`);
|
|
@@ -324,17 +297,10 @@ export async function validateArtifact(files) {
|
|
|
324
297
|
}
|
|
325
298
|
const declaredIslands = new Set(manifest?.supports?.islands ?? []);
|
|
326
299
|
const placedIslands = new Set();
|
|
327
|
-
|
|
328
|
-
let
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// V2 collection sections render the supplied bounded envelopes, without independently
|
|
333
|
-
// fetching legacy display islands. The populated fixture's sentinel must appear, and the
|
|
334
|
-
// empty context must render it away (derive or omit, nothing invented, nothing dangling).
|
|
335
|
-
// Sentinels are DERIVED from the canonical fixtures (the first item's display field), never
|
|
336
|
-
// hardcoded, the fixture data is free to become richer without touching a proof, and a proof
|
|
337
|
-
// can never drift from the data it renders.
|
|
300
|
+
let homeHeroMultiShows = null;
|
|
301
|
+
let homeHeroSingleShows = null;
|
|
302
|
+
let homeHeroShowsMultiple = false;
|
|
303
|
+
|
|
338
304
|
const sentinelOf = (type, field) => siteFx.content[type]?.items?.[0]?.[field] ?? null;
|
|
339
305
|
const WIDGET_SECTIONS = {
|
|
340
306
|
events: { island: null, dataKey: 'events', sentinel: sentinelOf('events', 'name') },
|
|
@@ -345,8 +311,6 @@ export async function validateArtifact(files) {
|
|
|
345
311
|
documents: { island: null, dataKey: 'documents', sentinel: sentinelOf('documents', 'title') }
|
|
346
312
|
};
|
|
347
313
|
|
|
348
|
-
// 2b. Compositions (site editor stage 4): each page must be a supported page, each type a
|
|
349
|
-
// supported section the catalogue assigns to that page, listed once, with a role.
|
|
350
314
|
const compositions = manifest?.compositions ?? {};
|
|
351
315
|
for (const [page, entries] of Object.entries(compositions)) {
|
|
352
316
|
if (!(manifest?.supports?.pages ?? []).includes(page)) {
|
|
@@ -369,7 +333,6 @@ export async function validateArtifact(files) {
|
|
|
369
333
|
}
|
|
370
334
|
}
|
|
371
335
|
|
|
372
|
-
// 2-4. Sections: catalogue membership, parse, fixture renders.
|
|
373
336
|
for (const type of manifest?.supports?.sections ?? []) {
|
|
374
337
|
const entry = catalogueByType.get(type);
|
|
375
338
|
if (!entry) {
|
|
@@ -398,6 +361,23 @@ export async function validateArtifact(files) {
|
|
|
398
361
|
...(contextContract.fixtures.sections?.[type] ?? {})
|
|
399
362
|
}), entry, manifest?.supports?.sectionHeadingAlignment?.[type], { inheritedHeading: siteFx.content[type]?.label }));
|
|
400
363
|
}
|
|
364
|
+
errors.push(...await proveSectionFields(content => liquid.render(parsed, {
|
|
365
|
+
section: resolveSectionFixture({ type, content }, siteFx, manifest), site: siteFx,
|
|
366
|
+
...(contextContract.fixtures.sections?.[type] ?? {})
|
|
367
|
+
}), entry, sectionFieldCapabilities(manifest)[type], { fieldMarkers: manifest?.supports?.fieldMarkers === true, css: read('assets/theme.css') }));
|
|
368
|
+
if (Object.hasOwn(manifest?.supports?.sectionPresentation ?? {}, type)
|
|
369
|
+
|| Object.values(SECTION_PRESENTATION_CONTROLS).some(control => read(file).includes(control.attribute))) {
|
|
370
|
+
for (const [control, descriptor] of Object.entries(SECTION_PRESENTATION_CONTROLS)) {
|
|
371
|
+
if (read(file).includes(descriptor.attribute) && !Object.hasOwn(manifest?.supports?.sectionPresentation?.[type] ?? {}, control)) {
|
|
372
|
+
warnings.push(`section '${type}': ${descriptor.attribute} is inert without supports.sectionPresentation.${type}.${control}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
errors.push(...await proveSectionPresentation(content => liquid.render(parsed, {
|
|
376
|
+
section: resolveSectionFixture({ type, content }, siteFx, manifest), site: siteFx,
|
|
377
|
+
...(contextContract.fixtures.sections?.[type] ?? {})
|
|
378
|
+
}), entry, manifest?.supports?.sectionPresentation?.[type], read('assets/theme.css') ?? '',
|
|
379
|
+
{ inheritedHeading: siteFx.content[type]?.label, heroImageLimit: manifest?.supports?.heroImageLimit ?? 1 }));
|
|
380
|
+
}
|
|
401
381
|
if ((Array.isArray(manifest?.supports?.sectionCollectionLinkVisibility) && manifest.supports.sectionCollectionLinkVisibility.includes(type)) || /data-p60-collection-link\b/.test(read(file))) {
|
|
402
382
|
const declared = Array.isArray(manifest?.supports?.sectionCollectionLinkVisibility) && manifest.supports.sectionCollectionLinkVisibility.includes(type);
|
|
403
383
|
const collection = type === 'appealGrid' ? 'causes' : type;
|
|
@@ -407,8 +387,6 @@ export async function validateArtifact(files) {
|
|
|
407
387
|
}, entry, declared));
|
|
408
388
|
if (!declared) warnings.push(`section '${type}': data-p60-collection-link is inert without supports.sectionCollectionLinkVisibility; the editor will not offer visibility`);
|
|
409
389
|
}
|
|
410
|
-
// Widget-section proof (independent of the minimal/sample loop): island placed → the island
|
|
411
|
-
// owns everything; hand-rendered → sentinel appears with data, vanishes without.
|
|
412
390
|
const widget = WIDGET_SECTIONS[type];
|
|
413
391
|
if (widget) {
|
|
414
392
|
const dataFixture = contextContract.fixtures.sections?.[type] ?? {};
|
|
@@ -449,18 +427,12 @@ export async function validateArtifact(files) {
|
|
|
449
427
|
const html = await liquid.render(parsed, {
|
|
450
428
|
section: resolveSectionFixture({ type, content: fixture }, siteFx, manifest),
|
|
451
429
|
site: siteFx,
|
|
452
|
-
// Widget data rides the ordinary fixture renders too, so a hand-rendering section
|
|
453
|
-
// doesn't fail the generic pass for want of its context.
|
|
454
430
|
...(widget ? (contextContract.fixtures.sections?.[type] ?? {}) : {})
|
|
455
431
|
});
|
|
456
432
|
if (type === 'homeHero' && fixtureName === 'sample') {
|
|
457
|
-
// The sample's own first image reference is the probe: `p60fixture:` refs are quote-free
|
|
458
|
-
// and survive HTML escaping, so "does the hero display the photos?" stays a substring check.
|
|
459
433
|
const probe = (fixture.images ?? [])[0]?.imageUrl ?? 'p60fixture:';
|
|
460
434
|
homeHeroMultiShows = html.includes(probe)
|
|
461
435
|
|| splitIslandParts(html).some((p) => p.island === 'hero_carousel');
|
|
462
|
-
// Prove the advertised capacity, not just the two-photo catalogue sample. Real image
|
|
463
|
-
// attributes prevent comments, copied text or repeated first images from opting in.
|
|
464
436
|
const limit = manifest?.supports?.heroImageLimit;
|
|
465
437
|
if (Number.isInteger(limit) && limit > 1 && limit <= 6) {
|
|
466
438
|
const images = Array.from({ length: limit }, (_, index) => ({
|
|
@@ -477,7 +449,6 @@ export async function validateArtifact(files) {
|
|
|
477
449
|
(node.tag === 'img' && node.attrs.src === image.imageUrl)
|
|
478
450
|
|| heroBackgroundShows(node, image.imageUrl)));
|
|
479
451
|
}
|
|
480
|
-
// The single-photo path proven separately: same fixture, first photo only.
|
|
481
452
|
try {
|
|
482
453
|
const single = await liquid.render(parsed, {
|
|
483
454
|
section: { ...fixture, images: (fixture.images ?? []).slice(0, 1) },
|
|
@@ -501,11 +472,6 @@ export async function validateArtifact(files) {
|
|
|
501
472
|
}
|
|
502
473
|
}
|
|
503
474
|
|
|
504
|
-
// 5. FIELD MARKERS: which node shows which field, so the editor can put the caret on the page
|
|
505
|
-
// instead of in a side panel. Source-level, like behaviours: a marker often sits inside a
|
|
506
|
-
// content-dependent branch the fixtures never take, and its presence in the source is the honest
|
|
507
|
-
// proof. A marker is an address inside that section's own content: `title`, or `items.0.label`
|
|
508
|
-
// for a list, where the index is usually a Liquid expression and stands for any position.
|
|
509
475
|
{
|
|
510
476
|
const MARKER = /data-p60-field="([^"]*)"/g;
|
|
511
477
|
const fieldsOf = (type) => new Map((catalogueByType.get(type)?.fields ?? []).map((f) => [f.name, f]));
|
|
@@ -515,7 +481,6 @@ export async function validateArtifact(files) {
|
|
|
515
481
|
const fields = fieldsOf(type);
|
|
516
482
|
for (const [, raw] of source.matchAll(MARKER)) {
|
|
517
483
|
marked.push(type);
|
|
518
|
-
// An index written as Liquid stands for whichever item this is.
|
|
519
484
|
const steps = raw.replace(/\{\{[^}]*\}\}/g, '#').split('.');
|
|
520
485
|
const field = fields.get(steps[0]);
|
|
521
486
|
const named = steps.length === 1
|
|
@@ -543,14 +508,6 @@ export async function validateArtifact(files) {
|
|
|
543
508
|
}
|
|
544
509
|
}
|
|
545
510
|
|
|
546
|
-
// Hero-imagery honesty, checked BEHAVIOURALLY (the worship pattern). The homeHero sample
|
|
547
|
-
// fixture carries photographs whose data URIs embed a quote-free marker that survives HTML
|
|
548
|
-
// escaping, so "does the rendered hero display the tenant's photos?" is a substring check,
|
|
549
|
-
// and with several photos, placing the hero_carousel island IS displaying them (the island
|
|
550
|
-
// renders the slides at runtime). The single-photo path is proven separately: images[0] must
|
|
551
|
-
// appear directly. Declaration and behaviour must agree; the choosers badge photo-led tenants
|
|
552
|
-
// by supports.heroImagery. Legacy imageUrl-only renderers never match (the fixture's photos
|
|
553
|
-
// ride `images`), so they pass undeclared, they just don't earn the badge.
|
|
554
511
|
{
|
|
555
512
|
const declaresHero = manifest?.supports?.heroImagery === true;
|
|
556
513
|
if (declaresHero && manifest?.supports?.heroImageLimit > 1 && !homeHeroShowsMultiple) {
|
|
@@ -571,7 +528,6 @@ export async function validateArtifact(files) {
|
|
|
571
528
|
}
|
|
572
529
|
}
|
|
573
530
|
|
|
574
|
-
// 7. Layout (when declared): parse + render the layout fixture + exactly one content slot.
|
|
575
531
|
if (manifest?.supports?.worship && !manifest?.supports?.layout) {
|
|
576
532
|
errors.push('manifest: supports.worship requires supports.layout, the worship rail is layout chrome');
|
|
577
533
|
}
|
|
@@ -618,11 +574,6 @@ export async function validateArtifact(files) {
|
|
|
618
574
|
errors.push(...highlights.errors);
|
|
619
575
|
warnings.push(...highlights.warnings);
|
|
620
576
|
|
|
621
|
-
// Two-sided worship honesty, checked BEHAVIOURALLY: does the rendered layout actually
|
|
622
|
-
// display the worship fixture's times? Declaration and behaviour must agree, the
|
|
623
|
-
// choosers steer worship-enabled tenants by supports.worship, so a false declaration
|
|
624
|
-
// either hides their times (undeclared but rendered is fine to fix by declaring) or
|
|
625
|
-
// promises a rail that never appears.
|
|
626
577
|
const worshipProbe = contextContract.fixtures.layout.worship?.times?.[0]?.name;
|
|
627
578
|
if (worshipProbe) {
|
|
628
579
|
const rendersWorship = html.includes(worshipProbe);
|
|
@@ -650,7 +601,6 @@ export async function validateArtifact(files) {
|
|
|
650
601
|
warnings.push('layout.liquid present but manifest.supports.layout is not true, it will be ignored');
|
|
651
602
|
}
|
|
652
603
|
|
|
653
|
-
// 8. Page templates (when declared): file exists, parses, renders the page's data fixture.
|
|
654
604
|
for (const pageName of manifest?.supports?.pageTemplates ?? []) {
|
|
655
605
|
const fixture = contextContract.fixtures.pages?.[pageName];
|
|
656
606
|
if (!fixture) {
|
|
@@ -685,7 +635,6 @@ export async function validateArtifact(files) {
|
|
|
685
635
|
}
|
|
686
636
|
}
|
|
687
637
|
|
|
688
|
-
// 5. Island discipline: placed ⊆ declared ⊆ registry (and available).
|
|
689
638
|
for (const name of placedIslands) {
|
|
690
639
|
if (!declaredIslands.has(name)) {
|
|
691
640
|
errors.push(`island '${name}': placed in a section but not declared in manifest.supports.islands`);
|
|
@@ -699,18 +648,13 @@ export async function validateArtifact(files) {
|
|
|
699
648
|
}
|
|
700
649
|
}
|
|
701
650
|
|
|
702
|
-
// 6. Theme.
|
|
703
651
|
if (!has('assets/theme.css') || read('assets/theme.css').trim() === '') {
|
|
704
652
|
errors.push('assets/theme.css missing or empty, a template must ship its look');
|
|
705
653
|
}
|
|
706
654
|
|
|
707
|
-
// 6b. Layout contract (contract/v1/layout.json). Platform pages render through the content SEAM
|
|
708
|
-
// (.container / .full); a template STYLES those to place content, never a parallel content container.
|
|
709
|
-
// The rule is DATA, the seam token, the allowed selectors and the message all come from the contract
|
|
710
|
-
// file; this only implements the check KIND (a non-seam selector sizing its width off the token).
|
|
711
655
|
{
|
|
712
656
|
const css = has('assets/theme.css') ? read('assets/theme.css') : '';
|
|
713
|
-
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
657
|
+
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
714
658
|
const RULE = /([^{}]+)\{([^{}]*)\}/g;
|
|
715
659
|
for (const rule of layoutContract.rules ?? []) {
|
|
716
660
|
if (rule.kind !== 'css-width-off-token') continue;
|
|
@@ -721,7 +665,7 @@ export async function validateArtifact(files) {
|
|
|
721
665
|
let m;
|
|
722
666
|
RULE.lastIndex = 0;
|
|
723
667
|
while ((m = RULE.exec(stripped)) !== null) {
|
|
724
|
-
if (!sizesOffToken.test(m[2])) continue;
|
|
668
|
+
if (!sizesOffToken.test(m[2])) continue;
|
|
725
669
|
for (const sel of m[1].split(',')) {
|
|
726
670
|
const s = sel.trim();
|
|
727
671
|
if (s && !isSeam.test(s)) offenders.add(s);
|
|
@@ -733,9 +677,6 @@ export async function validateArtifact(files) {
|
|
|
733
677
|
}
|
|
734
678
|
}
|
|
735
679
|
|
|
736
|
-
// Open enums, proven survivable (content model v1 discipline 2): a template whose footprint
|
|
737
|
-
// reads site.content.events must survive a registration mode it has never heard of, new modes
|
|
738
|
-
// WILL arrive within the major. No throw, and no undefined/null literal leaking into markup.
|
|
739
680
|
if (contentAnalysis.footprint.includes('content.events')) {
|
|
740
681
|
const futureEvent = {
|
|
741
682
|
...(siteFx.content.events.items[0] ?? {}),
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
// Existing artifacts are validated against their own contract. New uploads have an explicit v2 gate.
|
|
2
1
|
import { validateArtifact as validateV1Artifact } from './validate-v1.mjs';
|
|
3
2
|
import { validateArtifact as validateV2Artifact } from './validate-v2.mjs';
|
|
4
3
|
export { validateV1Artifact, validateV2Artifact };
|
|
5
|
-
// Historical consumers use these exact v1 exports; v2 consumers select the versioned module.
|
|
6
4
|
export { contractDialect, sectionCatalogue, islandRegistry, contextContract, behaviourCatalogue, BEHAVIOUR_PRIMARY_ATTR } from './validate-v1.mjs';
|
|
7
5
|
|
|
8
6
|
function manifestOf(files) {
|
package/starter/assets/theme.css
CHANGED
|
@@ -236,3 +236,58 @@ body { font-family: ' IBM Plex Sans Arabic', var(--p60s-siteFont, 'Inter'), ui-s
|
|
|
236
236
|
cursor: pointer;
|
|
237
237
|
}
|
|
238
238
|
.lq-hero-slides .p60-carousel-dot--active { background: rgba(255, 255, 255, 0.95); }
|
|
239
|
+
|
|
240
|
+
/* Bounded presentation keeps the existing responsive heading families and outer padding. */
|
|
241
|
+
h1[data-p60-heading-scale="compact"] { font-size: clamp(2.16rem, 4.5vw, 3.24rem); }
|
|
242
|
+
h1[data-p60-heading-scale="standard"] { font-size: clamp(2.4rem, 5vw, 3.6rem); }
|
|
243
|
+
h1[data-p60-heading-scale="large"] { font-size: clamp(2.64rem, 5.5vw, 3.96rem); }
|
|
244
|
+
h2[data-p60-heading-scale="compact"] { font-size: clamp(1.53rem, 2.7vw, 2.16rem); }
|
|
245
|
+
h2[data-p60-heading-scale="standard"] { font-size: clamp(1.7rem, 3vw, 2.4rem); }
|
|
246
|
+
h2[data-p60-heading-scale="large"] { font-size: clamp(1.87rem, 3.3vw, 2.64rem); }
|
|
247
|
+
|
|
248
|
+
.lq-hero[data-p60-section-spacing="compact"] { padding-block: 3.6rem 2rem; }
|
|
249
|
+
.lq-hero[data-p60-section-spacing="standard"] { padding-block: 4.5rem 2.5rem; }
|
|
250
|
+
.lq-hero[data-p60-section-spacing="spacious"] { padding-block: 5.4rem 3rem; }
|
|
251
|
+
.lq-values[data-p60-section-spacing="compact"] { padding-block: 2.8rem; }
|
|
252
|
+
.lq-values[data-p60-section-spacing="standard"] { padding-block: 3.5rem; }
|
|
253
|
+
.lq-values[data-p60-section-spacing="spacious"] { padding-block: 4.2rem; }
|
|
254
|
+
.lq-cta[data-p60-section-spacing="compact"] { padding-block: 2.8rem 3.6rem; }
|
|
255
|
+
.lq-cta[data-p60-section-spacing="standard"] { padding-block: 3.5rem 4.5rem; }
|
|
256
|
+
.lq-cta[data-p60-section-spacing="spacious"] { padding-block: 4.2rem 5.4rem; }
|
|
257
|
+
.lq-homehero[data-p60-section-spacing="compact"] { padding-block: 3.6rem; }
|
|
258
|
+
.lq-homehero[data-p60-section-spacing="standard"] { padding-block: 4.5rem; }
|
|
259
|
+
.lq-homehero[data-p60-section-spacing="spacious"] { padding-block: 5.4rem; }
|
|
260
|
+
/* Whole-photo framing has no top padding; the form branch has its own authored bottom. */
|
|
261
|
+
.lq-homehero--vista[data-p60-section-spacing="compact"] { padding-block-start: 0; }
|
|
262
|
+
.lq-homehero--vista[data-p60-section-spacing="standard"] { padding-block-start: 0; }
|
|
263
|
+
.lq-homehero--vista[data-p60-section-spacing="spacious"] { padding-block-start: 0; }
|
|
264
|
+
.lq-homehero--vista-form[data-p60-section-spacing="compact"] { padding-block-end: 2.08rem; }
|
|
265
|
+
.lq-homehero--vista-form[data-p60-section-spacing="standard"] { padding-block-end: 2.6rem; }
|
|
266
|
+
.lq-homehero--vista-form[data-p60-section-spacing="spacious"] { padding-block-end: 3.12rem; }
|
|
267
|
+
|
|
268
|
+
/* Author-bounded Intro photo layouts. No-photo defaults remain unchanged. */
|
|
269
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="frame"] { display:grid; grid-template-columns:minmax(0,1fr); align-items:center; gap:2rem; }
|
|
270
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="media"],.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] { min-inline-size:0; }
|
|
271
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] { overflow-wrap:anywhere; }
|
|
272
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="media"] { container-type:inline-size; container-name:p60-intro-photo; }
|
|
273
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="media"] img { display:block; inline-size:100%; block-size:auto; }
|
|
274
|
+
.lq-hero img[data-p60-field="imageUrl"] { object-fit:contain; }
|
|
275
|
+
.lq-hero img[data-p60-photo-framing="whole"] { aspect-ratio:auto; object-fit:contain; }
|
|
276
|
+
.lq-hero img[data-p60-photo-framing="fill"] { aspect-ratio:16 / 9; object-fit:cover; }
|
|
277
|
+
@container p60-intro-photo (max-width:48rem) {
|
|
278
|
+
.lq-hero img[data-p60-photo-framing="fill"] { aspect-ratio:1 / 1; }
|
|
279
|
+
}
|
|
280
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] { inline-size:100%; container-type:inline-size; }
|
|
281
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] h1 { font-size:clamp(2.2rem,8.5cqi,3.6rem); }
|
|
282
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] h1[data-p60-heading-scale="compact"] { font-size:clamp(1.98rem,7.65cqi,3.24rem); }
|
|
283
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] h1[data-p60-heading-scale="standard"] { font-size:clamp(2.2rem,8.5cqi,3.6rem); }
|
|
284
|
+
.lq-hero[data-p60-layout-has-media] [data-p60-layout-role="content"] h1[data-p60-heading-scale="large"] { font-size:clamp(2.464rem,9.52cqi,4.032rem); }
|
|
285
|
+
@media (min-width:1024px) {
|
|
286
|
+
.lq-hero[data-p60-section-layout="image-start"][data-p60-layout-has-media] [data-p60-layout-role="frame"] { grid-template-columns:minmax(0,1fr) minmax(0,1fr); }
|
|
287
|
+
.lq-hero[data-p60-section-layout="image-start"][data-p60-layout-has-media] [data-p60-layout-role="media"] { grid-column:1; grid-row:1; }
|
|
288
|
+
.lq-hero[data-p60-section-layout="image-start"][data-p60-layout-has-media] [data-p60-layout-role="content"] { grid-column:2; grid-row:1; }
|
|
289
|
+
.lq-hero[data-p60-section-layout="image-end"][data-p60-layout-has-media] [data-p60-layout-role="frame"] { grid-template-columns:minmax(0,1fr) minmax(0,1fr); }
|
|
290
|
+
.lq-hero[data-p60-section-layout="image-end"][data-p60-layout-has-media] [data-p60-layout-role="media"] { grid-column:2; grid-row:1; }
|
|
291
|
+
.lq-hero[data-p60-section-layout="image-end"][data-p60-layout-has-media] [data-p60-layout-role="content"] { grid-column:1; grid-row:1; }
|
|
292
|
+
}
|
|
293
|
+
/* End Intro photo layouts. */
|
package/starter/manifest.json
CHANGED
|
@@ -7,6 +7,26 @@
|
|
|
7
7
|
"supports": {
|
|
8
8
|
"navigationHighlights": false,
|
|
9
9
|
"fieldMarkers": true,
|
|
10
|
+
"sectionFields": { "hero": ["imageUrl", "imageAlt", "photoFraming"] },
|
|
11
|
+
"sectionPresentation": {
|
|
12
|
+
"homeHero": {
|
|
13
|
+
"headingScale": { "options": ["compact", "standard", "large"], "default": "standard" },
|
|
14
|
+
"sectionSpacing": { "options": ["compact", "standard", "spacious"], "default": "standard" }
|
|
15
|
+
},
|
|
16
|
+
"hero": {
|
|
17
|
+
"sectionLayout": { "options": ["image-start", "image-end", "stacked"], "default": "stacked" },
|
|
18
|
+
"headingScale": { "options": ["compact", "standard", "large"], "default": "standard" },
|
|
19
|
+
"sectionSpacing": { "options": ["compact", "standard", "spacious"], "default": "standard" }
|
|
20
|
+
},
|
|
21
|
+
"values": {
|
|
22
|
+
"headingScale": { "options": ["compact", "standard", "large"], "default": "standard" },
|
|
23
|
+
"sectionSpacing": { "options": ["compact", "standard", "spacious"], "default": "standard" }
|
|
24
|
+
},
|
|
25
|
+
"cta": {
|
|
26
|
+
"headingScale": { "options": ["compact", "standard", "large"], "default": "standard" },
|
|
27
|
+
"sectionSpacing": { "options": ["compact", "standard", "spacious"], "default": "standard" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
10
30
|
"pages": [
|
|
11
31
|
"home",
|
|
12
32
|
"about"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<section class="lq-cta">
|
|
1
|
+
<section class="lq-cta" data-p60-section-spacing="{{ section.presentation.sectionSpacing }}">
|
|
2
2
|
<div class="container lq-cta-grid">
|
|
3
3
|
<div>
|
|
4
|
-
<h2 data-p60-field="heading">{{ section.heading }}</h2>
|
|
4
|
+
<h2 data-p60-field="heading" data-p60-heading-scale="{{ section.presentation.headingScale }}">{{ section.heading }}</h2>
|
|
5
5
|
{% if section.text %}<p data-p60-field="text">{{ section.text }}</p>{% endif %}
|
|
6
6
|
<a class="button button-primary" href="{{ section.buttonHref }}">{{ section.buttonLabel }}</a>
|
|
7
7
|
</div>
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
wrap the value in a span when there is punctuation or other copy beside it (people.liquid shows
|
|
6
6
|
one). Marking is optional and additive: an unmarked template is edited in the panel as before.
|
|
7
7
|
{% endcomment %}
|
|
8
|
-
<section class="lq-hero">
|
|
9
|
-
<div class="container">
|
|
8
|
+
<section class="lq-hero" data-p60-section-spacing="{{ section.presentation.sectionSpacing }}" data-p60-section-layout="{{ section.presentation.sectionLayout }}"{% if section.imageUrl != blank %} data-p60-layout-has-media{% endif %}>
|
|
9
|
+
<div class="container" data-p60-layout-role="frame">
|
|
10
|
+
{% if section.imageUrl != blank %}<div data-p60-layout-role="media"><img data-p60-field="imageUrl" data-p60-photo-framing="{{ section.photoFraming }}" src="{{ section.imageUrl }}" alt="{{ section.imageAlt }}" loading="lazy" decoding="async"></div><div data-p60-layout-role="content">{% endif %}
|
|
10
11
|
{% if section.eyebrow %}<p class="lq-kicker" data-p60-field="eyebrow">{{ section.eyebrow }}</p>{% endif %}
|
|
11
|
-
<h1 data-p60-field="title">{{ section.title }}</h1>
|
|
12
|
+
<h1 data-p60-field="title" data-p60-heading-scale="{{ section.presentation.headingScale }}">{{ section.title }}</h1>
|
|
12
13
|
<div class="lq-hero-body">{{ section.bodyHtml | raw }}</div>
|
|
14
|
+
{% if section.imageUrl != blank %}</div>{% endif %}
|
|
13
15
|
</div>
|
|
14
16
|
</section>
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
{% assign heroFraming = section.photoFraming | default: 'fill' %}
|
|
37
37
|
{% if heroCount > 0 and heroFraming == 'whole' %}
|
|
38
38
|
{% if section.actionStyle == 'button' %}
|
|
39
|
-
<section class="lq-homehero lq-homehero--vista{% if heroCount > 1 %} lq-homehero--carousel{% endif %}">
|
|
39
|
+
<section class="lq-homehero lq-homehero--vista{% if heroCount > 1 %} lq-homehero--carousel{% endif %}" data-p60-section-spacing="{{ section.presentation.sectionSpacing }}">
|
|
40
40
|
<div class="lq-vista">
|
|
41
41
|
<img class="lq-vista-sizer" src="{{ section.images.first.imageUrl }}" alt="{{ section.images.first.alt }}" style="object-position: {{ heroFocus }}" fetchpriority="high" decoding="async">
|
|
42
42
|
{% if heroCount > 1 %}{% island 'hero_carousel' %}{% else %}<div class="lq-vista-scrim" aria-hidden="true"></div>{% endif %}
|
|
43
43
|
<div class="lq-vista-copy">
|
|
44
44
|
<div class="container">
|
|
45
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 %}
|
|
46
|
+
{% if section.title != nil and section.title != '' %}<h1 data-p60-field="title" data-p60-heading-scale="{{ section.presentation.headingScale }}">{{ section.title }}</h1>{% endif %}
|
|
47
47
|
{% if section.lead %}<p class="lq-homehero-lead" data-p60-field="lead">{{ section.lead }}</p>{% endif %}
|
|
48
48
|
<div class="lq-homehero-actions">
|
|
49
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 %}
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
{% else %}
|
|
57
57
|
{% comment %} Form mode: the grid pulls up over the band, copy on the photo, the widget riding
|
|
58
58
|
the seam onto the gradient below (never a stacked strip). {% endcomment %}
|
|
59
|
-
<section class="lq-homehero lq-homehero--vista lq-homehero--vista-form{% if heroCount > 1 %} lq-homehero--carousel{% endif %}">
|
|
59
|
+
<section class="lq-homehero lq-homehero--vista lq-homehero--vista-form{% if heroCount > 1 %} lq-homehero--carousel{% endif %}" data-p60-section-spacing="{{ section.presentation.sectionSpacing }}">
|
|
60
60
|
<div class="lq-vista">
|
|
61
61
|
<img class="lq-vista-sizer" src="{{ section.images.first.imageUrl }}" alt="{{ section.images.first.alt }}" style="object-position: {{ heroFocus }}" fetchpriority="high" decoding="async">
|
|
62
62
|
{% if heroCount > 1 %}{% island 'hero_carousel' %}{% else %}<div class="lq-vista-scrim" aria-hidden="true"></div>{% endif %}
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
<div class="container lq-homehero-inner lq-vista-overlap">
|
|
65
65
|
<div>
|
|
66
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 %}
|
|
67
|
+
{% if section.title != nil and section.title != '' %}<h1 data-p60-field="title" data-p60-heading-scale="{{ section.presentation.headingScale }}">{{ section.title }}</h1>{% endif %}
|
|
68
68
|
{% if section.lead %}<p class="lq-homehero-lead" data-p60-field="lead">{{ section.lead }}</p>{% endif %}
|
|
69
69
|
<div class="lq-homehero-actions">
|
|
70
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 %}
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
</section>
|
|
80
80
|
{% endif %}
|
|
81
81
|
{% else %}
|
|
82
|
-
<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 %}>
|
|
82
|
+
<section class="lq-homehero{% if heroPhoto %} lq-homehero--photo{% endif %}{% if heroCount > 1 %} lq-homehero--carousel{% endif %}" data-p60-section-spacing="{{ section.presentation.sectionSpacing }}"{% if heroPhoto %} style="background-image: url('{{ heroPhoto }}'); background-position: {{ heroFocus }}"{% endif %}>
|
|
83
83
|
{% comment %}
|
|
84
84
|
Several photos → the BEHAVIOUR carousel (data-p60-carousel, engine-wired, the template ships
|
|
85
85
|
no JavaScript): complete static markup, one data-p60-slide per photo, auto-advance opt-in.
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
<div class="container lq-homehero-inner">
|
|
100
100
|
<div>
|
|
101
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 %}
|
|
102
|
+
{% if section.title != nil and section.title != '' %}<h1 data-p60-field="title" data-p60-heading-scale="{{ section.presentation.headingScale }}">{{ section.title }}</h1>{% endif %}
|
|
103
103
|
{% if section.lead %}<p class="lq-homehero-lead" data-p60-field="lead">{{ section.lead }}</p>{% endif %}
|
|
104
104
|
<div class="lq-homehero-actions">
|
|
105
105
|
{% if section.actionStyle == 'button' %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<section class="lq-values">
|
|
1
|
+
<section class="lq-values" data-p60-section-spacing="{{ section.presentation.sectionSpacing }}">
|
|
2
2
|
<div class="container">
|
|
3
|
-
{% if section.heading != nil and section.heading != '' %}<h2 data-p60-field="heading">{{ section.heading }}</h2>{% endif %}
|
|
3
|
+
{% if section.heading != nil and section.heading != '' %}<h2 data-p60-field="heading" data-p60-heading-scale="{{ section.presentation.headingScale }}">{{ 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,
|