@port60/template-kit 1.2.1 → 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 +38 -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/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 +559 -7
- 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/presentation-proof.mjs +0 -2
- package/src/vendor/validator/preview-v1.mjs +3 -103
- package/src/vendor/validator/preview-v2.mjs +11 -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,9 +1,3 @@
|
|
|
1
|
-
// LiquidJS DoS budgets. ONE definition for the production engine, the validator and the studio
|
|
2
|
-
// preview, so "validated ⇒ renders in production" includes the resource ceiling. Third-party
|
|
3
|
-
// templates run in the shared SSR process (developer program T1.3): without these, an untrusted
|
|
4
|
-
// `{% for %}`/`{% capture %}` burns CPU/memory for every tenant on the box. The numbers are far
|
|
5
|
-
// above anything a legitimate template needs (the largest internal artifact parses ~80KB and
|
|
6
|
-
// renders in single-digit ms) and far below what hurts the process.
|
|
7
1
|
export const LIQUID_BUDGETS = {
|
|
8
2
|
/** Max characters parsed per parse() call (dialect docs: a typical PC handles 1e8). */
|
|
9
3
|
parseLimit: 1e6,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { LIQUID_BUDGETS } from './budgets.mjs';
|
|
2
|
+
|
|
3
|
+
const ATTRIBUTE = 'data-p60-colour-role';
|
|
4
|
+
const ROLES = new Map([
|
|
5
|
+
['h2', 'text'], ['h3', 'text'], ['h4', 'text'],
|
|
6
|
+
['p', 'muted'], ['ul', 'muted'], ['ol', 'muted'], ['li', 'muted'],
|
|
7
|
+
['a', 'link'], ['blockquote', 'surface'], ['hr', 'border']
|
|
8
|
+
]);
|
|
9
|
+
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
10
|
+
const OPAQUE = new Set(['script', 'style', 'textarea', 'template', 'noscript', 'iframe', 'svg', 'math', 'astro-island']);
|
|
11
|
+
|
|
12
|
+
/** Adds paint ownership to already-sanitised rich text, never sanitises or marks it safe.
|
|
13
|
+
* Opening tags retain their bytes apart from one inserted attribute. The bounded linear scan
|
|
14
|
+
* avoids DOM allocation/serialization, preserving URLs, entities, whitespace and text-align.
|
|
15
|
+
* Inline emphasis inherits its paragraph/heading/link; images and opaque subtrees are untouched.
|
|
16
|
+
*/
|
|
17
|
+
export function colourRoles(value) {
|
|
18
|
+
if (typeof value !== 'string' || !value) return '';
|
|
19
|
+
if (value.length > LIQUID_BUDGETS.parseLimit) throw new Error('colour_roles rich text exceeds the template parse budget');
|
|
20
|
+
const chunks = [], opaque = [];
|
|
21
|
+
let cursor = 0, copied = 0;
|
|
22
|
+
while (cursor < value.length) {
|
|
23
|
+
const start = value.indexOf('<', cursor);
|
|
24
|
+
if (start < 0) break;
|
|
25
|
+
if (value.startsWith('<!--', start)) {
|
|
26
|
+
const end = value.indexOf('-->', start + 4);
|
|
27
|
+
cursor = end < 0 ? value.length : end + 3;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
let end = start + 1, quote = null;
|
|
31
|
+
for (; end < value.length; end++) {
|
|
32
|
+
const character = value[end];
|
|
33
|
+
if (quote) { if (character === quote) quote = null; }
|
|
34
|
+
else if (character === '"' || character === "'") quote = character;
|
|
35
|
+
else if (character === '>') break;
|
|
36
|
+
}
|
|
37
|
+
if (end === value.length) break;
|
|
38
|
+
cursor = end + 1;
|
|
39
|
+
const token = value.slice(start, cursor);
|
|
40
|
+
const tag = /^<(\/)?([a-z][\w:-]*)(?=[\s/>])/i.exec(token);
|
|
41
|
+
if (!tag) continue;
|
|
42
|
+
const name = tag[2].toLowerCase();
|
|
43
|
+
if (tag[1]) {
|
|
44
|
+
if (opaque.at(-1) === name) opaque.pop();
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const attributes = [...token.slice(tag[0].length, -1).matchAll(/([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)];
|
|
48
|
+
const names = attributes.map(attribute => attribute[1].toLowerCase());
|
|
49
|
+
const closed = VOID.has(name) || /\/\s*>$/.test(token);
|
|
50
|
+
if (opaque.length || OPAQUE.has(name) || names.includes('data-p60-island')) {
|
|
51
|
+
if (!closed) opaque.push(name);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const role = ROLES.get(name);
|
|
55
|
+
if (!role) continue;
|
|
56
|
+
const existing = attributes.filter(attribute => attribute[1].toLowerCase() === ATTRIBUTE);
|
|
57
|
+
if (existing.length) {
|
|
58
|
+
if (existing.length !== 1 || (existing[0][2] ?? existing[0][3] ?? existing[0][4]) !== role) {
|
|
59
|
+
throw new Error('colour_roles cannot replace an existing conflicting colour role');
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const insertion = start + tag[0].length;
|
|
64
|
+
chunks.push(value.slice(copied, insertion), ` ${ATTRIBUTE}="${role}"`);
|
|
65
|
+
copied = insertion;
|
|
66
|
+
}
|
|
67
|
+
return chunks.length ? chunks.join('') + value.slice(copied) : value;
|
|
68
|
+
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// CONTENT FOOTPRINTS (content model v1, docs/template-content-model.md): which site.* paths a
|
|
2
|
-
// template touches, decidable at publish because the dialect is closed. Lives in the ENGINE so
|
|
3
|
-
// the production loader computes the same footprint the validator stamps, the render path
|
|
4
|
-
// fetches precisely what a template reads, and the two can never disagree.
|
|
5
1
|
import contentModel from '../contract/v1/content-model.json' with { type: 'json' };
|
|
6
2
|
import contentModelV2 from '../contract/v2/content-model.json' with { type: 'json' };
|
|
7
3
|
import siteSchemaV2 from '../contract/v2/site.schema.json' with { type: 'json' };
|
|
@@ -9,12 +5,6 @@ import siteSchemaV2 from '../contract/v2/site.schema.json' with { type: 'json' }
|
|
|
9
5
|
export { contentModel };
|
|
10
6
|
export { contentModelV2 };
|
|
11
7
|
|
|
12
|
-
// ── Extraction ─────────────────────────────────────────────────────────
|
|
13
|
-
// The dialect is closed, so the site paths a template reads are decidable from its sources: every
|
|
14
|
-
// reference is a literal `site.…` chain (dynamic indexing is refused below, and aliasing the tree
|
|
15
|
-
// itself is refused so a chain can never hide behind a variable). The footprint is collection-
|
|
16
|
-
// granular, `content.events`, because item fields ride the collection fetch.
|
|
17
|
-
|
|
18
8
|
const CHAIN = /\bsite((?:\.[A-Za-z_][A-Za-z0-9_-]*|\[\s*'[^']*'\s*\]|\[\s*"[^"]*"\s*\])+)/g;
|
|
19
9
|
const DYNAMIC_INDEX = /\bsite(?:\.[A-Za-z_][A-Za-z0-9_-]*|\[\s*(?:'[^']*'|"[^"]*")\s*\])*\[\s*(?!\s*['"])[^\]]/;
|
|
20
10
|
const BARE_SITE = /\bsite\s*(?:\}\}|\|)|(?:\bassign\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*|\bfor\s+[A-Za-z_][A-Za-z0-9_]*\s+in\s+)site\s*(?:%\}|\.\s*%\})/;
|
|
@@ -140,7 +130,6 @@ export function extractContentFootprint(files, options = {}) {
|
|
|
140
130
|
touched.add(head);
|
|
141
131
|
} else errors.push(`${path}: site.${head} is not part of the v2 model.`);
|
|
142
132
|
}
|
|
143
|
-
// Local aliases of nav/collection objects are valid. Retired members are not.
|
|
144
133
|
const retired = [
|
|
145
134
|
[/\b(?:site\.)?nav\.(?:items|derived)\b/, 'nav.header and nav.footer replace nav.items/nav.derived'],
|
|
146
135
|
[/\b(?:site\.)?actions\.(?:primary|secondary)\b/, 'actions.header, actions.hero and actions.widget replace action aliases'],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const COLOR = /^(?:|#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8}))$/;
|
|
2
|
+
|
|
3
|
+
/** Resolve presentation only. Invalid stored choices fall back without changing tenant data;
|
|
4
|
+
* an explicit blank colour or declared blank option still means no emitted override. */
|
|
5
|
+
export function resolveDesignSettingValue(knob, stored, knownFamily) {
|
|
6
|
+
const valid = value => {
|
|
7
|
+
if (knob.kind === 'toggle') return typeof value === 'boolean' || value === 'true' || value === 'false';
|
|
8
|
+
if (typeof value !== 'string') return false;
|
|
9
|
+
if (knob.kind === 'color') return COLOR.test(value);
|
|
10
|
+
if (knob.kind === 'select') return Array.isArray(knob.options) && knob.options.includes(value);
|
|
11
|
+
if (knob.kind === 'font') return knownFamily(value) !== null;
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
const candidate = stored ?? knob.default;
|
|
15
|
+
const resolved = valid(candidate) ? candidate : valid(knob.default) ? knob.default : null;
|
|
16
|
+
return resolved === null ? null : String(resolved);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The live host and both template preview dialects stamp exactly the same resolved settings. */
|
|
20
|
+
export function resolveDesignSettings(schema, overrides, { knownFamily, fontStackFor }) {
|
|
21
|
+
const bodyAttrs = {};
|
|
22
|
+
const colorVars = [];
|
|
23
|
+
const fontSlots = [];
|
|
24
|
+
for (const knob of schema ?? []) {
|
|
25
|
+
const value = resolveDesignSettingValue(knob, Object.hasOwn(overrides, knob.key) ? overrides[knob.key] : undefined, knownFamily);
|
|
26
|
+
if (value === null || value === '') continue;
|
|
27
|
+
if (knob.kind === 'color') colorVars.push(`--p60s-${knob.key}: ${value};`);
|
|
28
|
+
else if (knob.kind === 'font') {
|
|
29
|
+
fontSlots.push({ family: value, weights: [...(knob.weights ?? [])] });
|
|
30
|
+
colorVars.push(`--p60s-${knob.key}: ${fontStackFor(value)};`);
|
|
31
|
+
} else bodyAttrs[`data-p60s-${knob.key}`] = value;
|
|
32
|
+
}
|
|
33
|
+
return { bodyAttrs, colorVars, fontSlots };
|
|
34
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
// Applies the Port60 dialect (contract/v1/dialect.json) to a LiquidJS instance. Plain JS on purpose:
|
|
2
|
-
// this exact module is imported by BOTH the rendering engine (Vite-bundled into charity-site) and the
|
|
3
|
-
// Node conformance validator (scripts/validate-template.mjs), one enforcement implementation, so the
|
|
4
|
-
// validator can never disagree with production rendering. The dialect object is passed IN (parsed
|
|
5
|
-
// JSON); this module does no file loading.
|
|
6
|
-
|
|
7
1
|
/** Marker protocol for island placement: the island tag emits these; the renderer splits on them. */
|
|
8
2
|
import { templateMessage, templateDate } from './locale.mjs';
|
|
3
|
+
import { colourRoles } from './colour-roles.mjs';
|
|
9
4
|
|
|
10
5
|
export const ISLAND_MARK_START = '\u0000P60_ISLAND:';
|
|
11
6
|
export const ISLAND_MARK_END = '\u0000';
|
|
@@ -31,10 +26,10 @@ export function configureDialect(liquid, dialect, islandNames) {
|
|
|
31
26
|
}
|
|
32
27
|
});
|
|
33
28
|
}
|
|
34
|
-
// Filters LiquidJS ships beyond our whitelist (e.g. its non-standard json/inspect debug filters).
|
|
35
29
|
const allowed = new Set(dialect.filters);
|
|
36
30
|
if (allowed.has('t')) liquid.registerFilter('t', templateMessage);
|
|
37
31
|
if (allowed.has('local_date')) liquid.registerFilter('local_date', templateDate);
|
|
32
|
+
if (allowed.has('colour_roles')) liquid.registerFilter('colour_roles', colourRoles);
|
|
38
33
|
for (const name of ['json', 'inspect', 'to_integer', 'normalize_whitespace', 'find', 'find_exp', 'group_by', 'group_by_exp', 'where_exp', 'sum']) {
|
|
39
34
|
if (!allowed.has(name)) {
|
|
40
35
|
liquid.registerFilter(name, () => {
|
|
@@ -49,13 +44,11 @@ export function configureDialect(liquid, dialect, islandNames) {
|
|
|
49
44
|
async render(ctx) {
|
|
50
45
|
const name = String(await this.liquid.evalValue(this.args, ctx));
|
|
51
46
|
if (!islandNames.has(name)) {
|
|
52
|
-
return '';
|
|
47
|
+
return '';
|
|
53
48
|
}
|
|
54
49
|
return `${ISLAND_MARK_START}${name}${ISLAND_MARK_END}`;
|
|
55
50
|
}
|
|
56
51
|
});
|
|
57
|
-
// {% content %}, the layout's page slot (contract v1, roadmap 8.2). Registered everywhere; a
|
|
58
|
-
// section that emits it is rejected by the validator and stripped by the section renderer.
|
|
59
52
|
liquid.registerTag('content', {
|
|
60
53
|
parse() {},
|
|
61
54
|
render() {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Template interface copy only. Never translate tenant-authored content or source record labels.
|
|
2
|
-
// Plain JS so the host and kit use the exact same implementation, without network or tenant state.
|
|
3
1
|
const arabic = Object.freeze({
|
|
4
2
|
'Explore': 'تصفّح', 'Menu': 'القائمة', 'Open menu': 'فتح القائمة', 'Close menu': 'إغلاق القائمة',
|
|
5
3
|
'Skip to content': 'انتقل إلى المحتوى', 'Skip to main content': 'انتقل إلى المحتوى الرئيسي',
|
|
@@ -54,11 +52,8 @@ export function templateMessage(message, locale) {
|
|
|
54
52
|
return language(locale) === 'ar' && Object.hasOwn(arabic, message) ? arabic[message] : message;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
// Exactly six formatter instances at most. No per-tenant or arbitrary-pattern cache growth.
|
|
58
55
|
const formatters = new Map();
|
|
59
56
|
export function templateDate(value, locale, style = 'date') {
|
|
60
|
-
// A timestamp needs an explicit offset. Date.parse on a zone-less timestamp would depend
|
|
61
|
-
// on the kit author's machine rather than the host's UTC rendering contract.
|
|
62
57
|
if (typeof value !== 'string' || value.length > 64 || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2}))?$/.test(value)) return '';
|
|
63
58
|
const timestamp = Date.parse(value);
|
|
64
59
|
if (!Number.isFinite(timestamp) || !['date', 'datetime'].includes(style)) return '';
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// The CONTRACT MAJORS registry (roadmap 8.6, multi-major engine support): the engine resolves a
|
|
2
|
-
// template's declared `format` to the contract set that renders it, so `port60-liquid@1` keeps
|
|
3
|
-
// rendering after `@2` ships. Today one major exists; when `@2` arrives it gets its own entry
|
|
4
|
-
// (own dialect/catalogues, own configured Liquid instance) and @1 artifacts keep resolving here
|
|
5
|
-
// untouched, with the published deprecation window governing how long.
|
|
6
1
|
import dialect from '../contract/v1/dialect.json' with { type: 'json' };
|
|
7
2
|
import sections from '../contract/v1/sections.json' with { type: 'json' };
|
|
8
3
|
import islands from '../contract/v1/islands.json' with { type: 'json' };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Exact-artifact opt-ins. Preferences survive template switches in the stored document;
|
|
2
|
-
// only supported values reach the render context or the editor's controls.
|
|
3
1
|
export const NAVIGATION_MODES = Object.freeze(['simple', 'mega']);
|
|
4
2
|
export const COLLECTION_LINK_SECTIONS = Object.freeze([
|
|
5
3
|
'events', 'articles', 'courses', 'campaigns', 'services', 'documents', 'appealGrid'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const INTRO_PHOTO_FIELDS = Object.freeze(['imageUrl', 'imageAlt']);
|
|
2
|
+
export const INTRO_OPTIONAL_FIELDS = Object.freeze([...INTRO_PHOTO_FIELDS, 'photoFraming']);
|
|
3
|
+
export const INTRO_PHOTO_FRAMING = Object.freeze(['fill', 'whole']);
|
|
4
|
+
export const INTRO_PHOTO_FRAMING_ATTRIBUTE = 'data-p60-photo-framing';
|
|
5
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
|
|
7
|
+
/** Optional field support is explicit per section and independent from layout variants. */
|
|
8
|
+
export function readSectionFields(value, sections) {
|
|
9
|
+
if (!object(value) || !Array.isArray(sections)) return null;
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [type, fields] of Object.entries(value)) {
|
|
12
|
+
if (type !== 'hero' || !sections.includes(type) || !Array.isArray(fields)
|
|
13
|
+
|| fields.length < INTRO_PHOTO_FIELDS.length || fields.length > INTRO_OPTIONAL_FIELDS.length || new Set(fields).size !== fields.length
|
|
14
|
+
|| INTRO_PHOTO_FIELDS.some(field => !fields.includes(field)) || fields.some(field => !INTRO_OPTIONAL_FIELDS.includes(field))) return null;
|
|
15
|
+
result[type] = [...fields];
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sectionFieldCapabilities(manifest) {
|
|
21
|
+
return manifest?.format === 'port60-liquid@2'
|
|
22
|
+
? readSectionFields(manifest?.supports?.sectionFields, manifest?.supports?.sections) ?? {} : {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Conservative public projection of the server's bounded image URL policy, not a fetch operation. */
|
|
26
|
+
export function safeSectionImageUrl(value, { fixture = false } = {}) {
|
|
27
|
+
if (typeof value !== 'string' || !value || value.length > 2048 || /[\s\u0000-\u0020\u007f-\u009f\\]/.test(value)) return undefined;
|
|
28
|
+
if (fixture && /^p60(?:fixture|preview):[^\s\\]+$/.test(value)) return value;
|
|
29
|
+
if (value.startsWith('/') && !value.startsWith('//')) return value;
|
|
30
|
+
if (!/^https?:\/\/[^/]/i.test(value)) return undefined;
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(value);
|
|
33
|
+
return ['https:', 'http:'].includes(url.protocol) && url.hostname && !url.username && !url.password ? value : undefined;
|
|
34
|
+
} catch { return undefined; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Retain tenant-owned stored fields, projecting only safe fields accepted by this artifact. */
|
|
38
|
+
export function withSectionFields(content, type, manifest, options) {
|
|
39
|
+
if (type !== 'hero' || manifest?.format !== 'port60-liquid@2') return content;
|
|
40
|
+
const projected = { ...content };
|
|
41
|
+
for (const field of INTRO_OPTIONAL_FIELDS) delete projected[field];
|
|
42
|
+
const fields = sectionFieldCapabilities(manifest).hero;
|
|
43
|
+
if (!fields) return projected;
|
|
44
|
+
const imageUrl = safeSectionImageUrl(content?.imageUrl, options);
|
|
45
|
+
if (imageUrl) {
|
|
46
|
+
projected.imageUrl = imageUrl;
|
|
47
|
+
projected.imageAlt = typeof content?.imageAlt === 'string' ? content.imageAlt : '';
|
|
48
|
+
if (fields.includes('photoFraming') && INTRO_PHOTO_FRAMING.includes(content?.photoFraming)) projected.photoFraming = content.photoFraming;
|
|
49
|
+
}
|
|
50
|
+
return projected;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Patch only this section's existing photo, leaving its source, content and neighbours intact. */
|
|
54
|
+
export function applySectionPhotoFraming(wrapper, fields, value) {
|
|
55
|
+
if (!wrapper || wrapper.getAttribute('data-p60-editor-type') !== 'hero'
|
|
56
|
+
|| !readSectionFields(fields, ['hero'])?.hero?.includes('photoFraming')
|
|
57
|
+
|| (value !== null && !INTRO_PHOTO_FRAMING.includes(value))) return false;
|
|
58
|
+
const hooks = [...wrapper.querySelectorAll(`[${INTRO_PHOTO_FRAMING_ATTRIBUTE}]`)]
|
|
59
|
+
.filter(node => node.closest('[data-p60-editor-section]') === wrapper);
|
|
60
|
+
if (hooks.length !== 1 || hooks[0].tagName !== 'IMG' || hooks[0].getAttribute('data-p60-field') !== 'imageUrl'
|
|
61
|
+
|| hooks[0].closest('astro-island, [data-p60-island]') || !safeSectionImageUrl(hooks[0].getAttribute('src'))) return false;
|
|
62
|
+
hooks[0].setAttribute(INTRO_PHOTO_FRAMING_ATTRIBUTE, value ?? '');
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Presentation choices are opt-in on the exact immutable artifact. Never mutate saved content:
|
|
2
|
-
// switching to an unsupported template leaves the original choice available for a later switch.
|
|
3
1
|
export const HEADING_ALIGNMENTS = Object.freeze(['start', 'center', 'end']);
|
|
4
2
|
|
|
5
3
|
export function headingAlignmentCapabilities(manifest) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import contract from '../contract/v2/presentation.json' with { type: 'json' };
|
|
2
|
+
import { sectionFieldCapabilities } from './section-fields.mjs';
|
|
3
|
+
|
|
4
|
+
export const PRESENTATION_CONTRACT = contract;
|
|
5
|
+
export const HEADING_SCALES = Object.freeze([...contract.controls.headingScale.options]);
|
|
6
|
+
export const SECTION_SPACINGS = Object.freeze([...contract.controls.sectionSpacing.options]);
|
|
7
|
+
export const IMAGE_OVERLAYS = Object.freeze([...contract.controls.imageOverlay.options]);
|
|
8
|
+
export const COLOUR_TREATMENTS = Object.freeze([...contract.controls.colourTreatment.options]);
|
|
9
|
+
export const SECTION_LAYOUTS = Object.freeze([...contract.controls.sectionLayout.options]);
|
|
10
|
+
export const SECTION_PRESENTATION_CONTROLS = Object.freeze(Object.fromEntries(
|
|
11
|
+
Object.entries(contract.controls).filter(([, descriptor]) => descriptor.address[0] === 'presentation')
|
|
12
|
+
));
|
|
13
|
+
export const SECTION_ROOT_TAGS = Object.freeze(['section', 'article', 'div', 'main', 'aside', 'header', 'footer']);
|
|
14
|
+
const INTERACTIVE_MEDIA_TAGS = new Set(['a', 'button', 'form', 'input', 'select', 'textarea', 'details', 'summary', 'video', 'audio', 'canvas', 'iframe', 'object', 'embed']);
|
|
15
|
+
export function sectionLayoutInteractiveMedia(tag, attrs) {
|
|
16
|
+
return INTERACTIVE_MEDIA_TAGS.has(tag) || Object.hasOwn(attrs, 'tabindex') || Object.hasOwn(attrs, 'contenteditable')
|
|
17
|
+
|| /(?:^|\s)(?:button|link|checkbox|radio|switch|slider|spinbutton|textbox|combobox|listbox|menuitem)(?:\s|$)/.test(attrs.role ?? '');
|
|
18
|
+
}
|
|
19
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
|
|
21
|
+
/** A malformed declaration fails closed as one unit; no control is guessed from rendered content. */
|
|
22
|
+
export function readSectionPresentation(value, sections) {
|
|
23
|
+
if (!object(value) || !Array.isArray(sections)) return null;
|
|
24
|
+
const entries = Object.entries(value);
|
|
25
|
+
if (entries.length > contract.sectionTypes.length) return null;
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const [type, controls] of entries) {
|
|
28
|
+
if (!contract.sectionTypes.includes(type) || !sections.includes(type) || !object(controls)
|
|
29
|
+
|| !Object.keys(controls).length) return null;
|
|
30
|
+
result[type] = {};
|
|
31
|
+
for (const [key, choice] of Object.entries(controls)) {
|
|
32
|
+
if (!Object.hasOwn(SECTION_PRESENTATION_CONTROLS, key)) return null;
|
|
33
|
+
const descriptor = SECTION_PRESENTATION_CONTROLS[key];
|
|
34
|
+
if (descriptor.sectionTypes && !descriptor.sectionTypes.includes(type)) return null;
|
|
35
|
+
if (!object(choice) || Object.keys(choice).length !== 2 || !Object.hasOwn(choice, 'options')
|
|
36
|
+
|| !Object.hasOwn(choice, 'default') || !Array.isArray(choice.options)
|
|
37
|
+
|| choice.options.length < descriptor.minimumOptions || choice.options.length > descriptor.options.length
|
|
38
|
+
|| new Set(choice.options).size !== choice.options.length
|
|
39
|
+
|| choice.options.some(option => !descriptor.options.includes(option)) || !choice.options.includes(choice.default)) return null;
|
|
40
|
+
result[type][key] = { options: [...choice.options], default: choice.default };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sectionPresentationCapabilities(manifest) {
|
|
47
|
+
if (manifest?.format !== 'port60-liquid@2') return {};
|
|
48
|
+
const controls = readSectionPresentation(manifest?.supports?.sectionPresentation, manifest?.supports?.sections) ?? {};
|
|
49
|
+
for (const [type, section] of Object.entries(controls)) for (const key of Object.keys(section)) {
|
|
50
|
+
const required = SECTION_PRESENTATION_CONTROLS[key].requiresSupport;
|
|
51
|
+
if (required && manifest?.supports?.[required] !== true) return {};
|
|
52
|
+
if (key === 'sectionLayout' && !sectionFieldCapabilities(manifest)[type]) delete section[key];
|
|
53
|
+
}
|
|
54
|
+
for (const [type, section] of Object.entries(controls)) if (!Object.keys(section).length) delete controls[type];
|
|
55
|
+
return controls;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Project accepted explicit choices only. This never mutates or deletes saved preferences. */
|
|
59
|
+
export function withSectionPresentation(content, type, manifest) {
|
|
60
|
+
const projected = { ...content };
|
|
61
|
+
delete projected.presentation;
|
|
62
|
+
const declared = sectionPresentationCapabilities(manifest)[type] ?? {};
|
|
63
|
+
const saved = object(content?.presentation) ? content.presentation : {};
|
|
64
|
+
const accepted = Object.fromEntries(Object.entries(declared)
|
|
65
|
+
.filter(([key, choice]) => Object.hasOwn(saved, key) && choice.options.includes(saved[key]))
|
|
66
|
+
.map(([key]) => [key, saved[key]]));
|
|
67
|
+
if (Object.keys(accepted).length) projected.presentation = accepted;
|
|
68
|
+
return projected;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Shape validation is shared by kit fixtures; exact support is a separate capability check. */
|
|
72
|
+
export function presentationValueErrors(value, type) {
|
|
73
|
+
if (!object(value)) return ['must be an object of presentation overrides'];
|
|
74
|
+
return Object.entries(value).flatMap(([key, saved]) => !Object.hasOwn(SECTION_PRESENTATION_CONTROLS, key)
|
|
75
|
+
? [`unknown presentation control '${key}'`]
|
|
76
|
+
: type && SECTION_PRESENTATION_CONTROLS[key].sectionTypes && !SECTION_PRESENTATION_CONTROLS[key].sectionTypes.includes(type)
|
|
77
|
+
? [`${key} is not available for section '${type}'`]
|
|
78
|
+
: SECTION_PRESENTATION_CONTROLS[key].options.includes(saved) ? []
|
|
79
|
+
: [`${key} must be ${SECTION_PRESENTATION_CONTROLS[key].options.join(', ')}; omit it to inherit`]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Immediate, bounded canvas patch. The final template render remains authoritative. */
|
|
83
|
+
export function applySectionPresentation(wrapper, capabilities, control, value) {
|
|
84
|
+
if (!wrapper || typeof control !== 'string' || !Object.hasOwn(SECTION_PRESENTATION_CONTROLS, control)) return false;
|
|
85
|
+
const type = wrapper.getAttribute('data-p60-editor-type');
|
|
86
|
+
const canonicalType = contract.legacySectionAliases[type] ?? type;
|
|
87
|
+
const accepted = readSectionPresentation(capabilities, contract.sectionTypes);
|
|
88
|
+
const declaration = accepted?.[canonicalType]?.[control];
|
|
89
|
+
if (!declaration || (value !== null && !declaration.options.includes(value))) return false;
|
|
90
|
+
const descriptor = SECTION_PRESENTATION_CONTROLS[control];
|
|
91
|
+
const hooks = [...wrapper.querySelectorAll(`[${descriptor.attribute}]`)]
|
|
92
|
+
.filter(node => node.closest('[data-p60-editor-section]') === wrapper);
|
|
93
|
+
const targets = descriptor.target === 'section-heading'
|
|
94
|
+
? hooks.filter(node => /^H[1-6]$/.test(node.tagName) && !node.closest('astro-island'))
|
|
95
|
+
: hooks;
|
|
96
|
+
// Validate the entire root patch before touching the DOM. Never widen a malformed hook.
|
|
97
|
+
if (['section-root', 'section-colour-treatment', 'section-layout'].includes(descriptor.target) && (targets.length !== 1 || wrapper.children.length !== 1
|
|
98
|
+
|| targets[0] !== wrapper.firstElementChild || !SECTION_ROOT_TAGS.includes(targets[0].tagName.toLowerCase())
|
|
99
|
+
|| targets[0].closest('astro-island')
|
|
100
|
+
|| [...wrapper.childNodes].some(node => node.nodeType === 3 && node.textContent.trim()))) return false;
|
|
101
|
+
if (descriptor.target === 'section-colour-treatment' && targets[0].querySelector('astro-island, [data-p60-island]')) return false;
|
|
102
|
+
if (descriptor.target === 'section-layout' && !validLayoutPatchRoot(targets[0])) return false;
|
|
103
|
+
if (descriptor.target === 'section-image-treatment' && (targets.length !== 1
|
|
104
|
+
|| targets[0] !== wrapper.firstElementChild || !SECTION_ROOT_TAGS.includes(targets[0].tagName.toLowerCase())
|
|
105
|
+
|| targets[0].closest('astro-island') || !targets[0].querySelector('[data-p60-image-overlay-media]'))) return false;
|
|
106
|
+
if (!targets.length) return false;
|
|
107
|
+
for (const target of targets) target.setAttribute(descriptor.attribute, value === null ? '' : value);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validLayoutPatchRoot(root) {
|
|
112
|
+
if (root.querySelector('astro-island, [data-p60-island]')) return false;
|
|
113
|
+
const roles = [root, ...root.querySelectorAll('[data-p60-layout-role]')].filter(node => node.hasAttribute('data-p60-layout-role'));
|
|
114
|
+
if (roles.some(node => !['frame', 'media', 'content'].includes(node.getAttribute('data-p60-layout-role')))) return false;
|
|
115
|
+
const role = name => roles.filter(node => node.getAttribute('data-p60-layout-role') === name);
|
|
116
|
+
const frame = role('frame'), media = role('media'), content = role('content');
|
|
117
|
+
if (root.hasAttribute('data-p60-layout-has-media') !== (media.length > 0)) return false;
|
|
118
|
+
if (!media.length) return frame.length <= 1 && content.length <= 1;
|
|
119
|
+
const images = [media[0], ...media[0].querySelectorAll('img')].filter(node => node.tagName === 'IMG');
|
|
120
|
+
return frame.length === 1 && media.length === 1 && content.length === 1
|
|
121
|
+
&& media[0].parentElement === frame[0] && content[0].parentElement === frame[0]
|
|
122
|
+
&& frame[0].children.length === 2
|
|
123
|
+
&& ![...frame[0].childNodes].some(node => node.nodeType === 3 && node.textContent.trim())
|
|
124
|
+
&& ![media[0], ...media[0].querySelectorAll('*')].some(node => sectionLayoutInteractiveMedia(node.tagName.toLowerCase(), Object.fromEntries([...node.attributes].map(attr => [attr.name, attr.value]))))
|
|
125
|
+
&& !media[0].textContent.trim()
|
|
126
|
+
&& images.some(node => node.getAttribute('src')?.trim());
|
|
127
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/build-behaviors-runtime.mjs, do not edit by hand.
|
|
2
|
-
"use strict";var p60Behaviors=(()=>{var N=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var F=(i,d)=>{for(var e in d)N(i,e,{get:d[e],enumerable:!0})},O=(i,d,e,n)=>{if(d&&typeof d=="object"||typeof d=="function")for(let t of B(d))!R.call(i,t)&&t!==e&&N(i,t,{get:()=>d[t],enumerable:!(n=C(d,t))||n.enumerable});return i};var _=i=>O(N({},"__esModule",{value:!0}),i);var se={};F(se,{initBehaviors:()=>ie});var M=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;function v(i,d){let e=`data-p60-bound-${d}`;return i.hasAttribute(e)?!0:(i.setAttribute(e,""),!1)}var q=.15;function H(i,d){if(i.length===0)return;if(M()||!("IntersectionObserver"in window)){i.forEach(d);return}let e=new IntersectionObserver(n=>{for(let t of n){if(!t.isIntersecting)continue;(t.boundingClientRect.height>=window.innerHeight||t.intersectionRatio>=q-.001)&&(e.unobserve(t.target),d(t.target))}},{rootMargin:"0px 0px -10% 0px",threshold:[0,q]});i.forEach(n=>e.observe(n))}function D(i){let d=Array.from(i.querySelectorAll("[data-p60-reveal-group]")).filter(n=>!v(n,"reveal-group"));for(let n of d)Array.from(n.children).forEach((t,r)=>{t.style.setProperty("--p60-reveal-index",String(r)),t.classList.add("p60-reveal-child")});H(d,n=>n.classList.add("is-revealed"));let e=Array.from(i.querySelectorAll("[data-p60-reveal]")).filter(n=>!v(n,"reveal"));H(e,n=>n.classList.add("is-revealed"))}var $=/\d[\d, ]*(?:\.\d+)?/;function V(i){let d=Array.from(i.querySelectorAll("[data-p60-count]")).filter(e=>!v(e,"count"));H(d,e=>{e.classList.add("is-revealed");let n=e.textContent??"",t=$.exec(n);if(!t)return;let r=t[0].replace(/[, ]+$/,""),l=n.slice(0,t.index),o=n.slice(t.index+r.length),s=/[, ]/.test(r),c=r.includes(".")?r.length-r.indexOf(".")-1:0,a=Number.parseFloat(r.replace(/[, ]/g,"")),u=Number.parseFloat(e.getAttribute("data-p60-count")??""),m=Number.isFinite(u)?u:a;if(!Number.isFinite(m)||M())return;let f=Math.min(Number.parseInt(e.getAttribute("data-p60-count-duration")??"1200",10)||1200,4e3),p=new Intl.NumberFormat(document.documentElement.lang||void 0,{useGrouping:s,minimumFractionDigits:c,maximumFractionDigits:c}),b=performance.now(),E=y=>{let A=Math.min((y-b)/f,1),g=1-Math.pow(1-A,3);e.textContent=A<1?l+p.format(m*g)+o:n,A<1&&requestAnimationFrame(E)};requestAnimationFrame(E)})}function Y(i){let d=Array.from(i.querySelectorAll("[data-p60-progress]")).filter(e=>!v(e,"progress"));if(!M()){for(let e of d){let n=e.style.width||getComputedStyle(e).width;e.style.transition="none",e.style.width="0px",e.offsetWidth,e.style.transition="width 1.1s cubic-bezier(0.22, 1, 0.36, 1)",e.dataset.p60ProgressTarget=n}H(d,e=>{e.classList.add("is-revealed"),e.style.width=e.dataset.p60ProgressTarget??""})}}function z(i){let d=Array.from(i.querySelectorAll("[data-p60-countdown]")).filter(e=>!v(e,"countdown"));for(let e of d){let n=Date.parse(e.getAttribute("data-p60-until")??"");if(!Number.isFinite(n))continue;let t=e.getAttribute("data-p60-countdown")||"dhm",r=t==="dhms",l=()=>{let s=n-Date.now();if(s<=0){e.classList.add("is-elapsed");let b=e.getAttribute("data-p60-elapsed-text");b&&(e.textContent=b),window.clearInterval(o);return}let c=Math.floor(s/1e3),a=Math.floor(c/86400),u=Math.floor(c%86400/3600),m=Math.floor(c%3600/60),f=c%60,p=[];t.includes("d")&&a>0&&p.push(`${a}d`),p.push(`${u}h`,`${m}m`),r&&p.push(`${f}s`),e.textContent=p.join(" ")},o=window.setInterval(l,r?1e3:15e3);l()}}function K(i){let d=Array.from(i.querySelectorAll("[data-p60-accordion]")).filter(e=>!v(e,"accordion"));for(let e of d){let n=e.getAttribute("data-p60-accordion")!=="multi",t=Array.from(e.querySelectorAll("details"));for(let r of t){r.addEventListener("toggle",()=>{if(r.classList.toggle("is-open",r.open),r.open&&n)for(let s of t)s!==r&&s.open&&(s.open=!1)}),r.open&&r.classList.add("is-open");let o=r.querySelector("summary")?.nextElementSibling;o&&!M()&&r.addEventListener("toggle",()=>{if(!r.open)return;let s=o.scrollHeight;o.style.overflow="hidden",o.animate([{height:"0px",opacity:"0"},{height:`${s}px`,opacity:"1"}],{duration:260,easing:"ease-out"}).addEventListener("finish",()=>{o.style.overflow=""})})}}}function U(i){let d=Array.from(i.querySelectorAll("[data-p60-carousel]")).filter(e=>!v(e,"carousel"));for(let e of d){let n=Array.from(e.querySelectorAll("[data-p60-slide]"));if(n.length<2)continue;let t=Math.max(0,n.findIndex(g=>g.classList.contains("is-active"))),r=[],l=document.createElement("nav");l.className="p60-carousel-dots",l.setAttribute("aria-label","Slides"),n.forEach((g,L)=>{let h=document.createElement("button");h.type="button",h.className="p60-carousel-dot",h.setAttribute("aria-label",`Go to slide ${L+1}`),h.addEventListener("click",()=>o(L,!0)),r.push(h),l.appendChild(h)}),e.appendChild(l),e.setAttribute("aria-roledescription","carousel"),e.hasAttribute("tabindex")||e.setAttribute("tabindex","0");let o=(g,L=!1)=>{t=(g+n.length)%n.length,n.forEach((h,x)=>{h.classList.toggle("is-active",x===t),x===t?(h.removeAttribute("aria-hidden"),h.removeAttribute("inert")):(h.setAttribute("aria-hidden","true"),h.setAttribute("inert",""))}),r.forEach((h,x)=>{h.classList.toggle("p60-carousel-dot--active",x===t),h.setAttribute("aria-current",x===t?"true":"false")}),e.style.setProperty("--p60-carousel-index",String(t)),L&&E()};e.querySelectorAll("[data-p60-carousel-prev]").forEach(g=>g.addEventListener("click",()=>o(t-1,!0))),e.querySelectorAll("[data-p60-carousel-next]").forEach(g=>g.addEventListener("click",()=>o(t+1,!0))),e.addEventListener("keydown",g=>{g.key==="ArrowLeft"&&o(t-1,!0),g.key==="ArrowRight"&&o(t+1,!0)});let s=0,c=0;e.addEventListener("pointerdown",g=>{s=g.clientX,c=g.clientY}),e.addEventListener("pointerup",g=>{let L=g.clientX-s,h=g.clientY-c;Math.abs(L)>40&&Math.abs(L)>Math.abs(h)&&o(t+(L<0?1:-1),!0)});let a=!1,u=!1,m,f=e.getAttribute("data-p60-carousel")==="auto"&&!M(),p=Math.max(Number.parseInt(e.getAttribute("data-p60-interval")??"6000",10)||6e3,3e3),b=()=>{m!==void 0&&window.clearInterval(m),m=void 0},E=()=>{b(),!(!f||u||M())&&(m=window.setInterval(()=>{!a&&!document.hidden&&o(t+1)},p))},y=null,A=g=>{u=g,e.classList.toggle("is-paused",u),y&&(y.setAttribute("aria-label",u?"Start slide rotation":"Stop slide rotation"),y.classList.toggle("p60-carousel-pause--stopped",u)),E()};f&&(y=document.createElement("button"),y.type="button",y.className="p60-carousel-pause",y.setAttribute("aria-label","Stop slide rotation"),y.addEventListener("click",()=>A(!u)),l.insertBefore(y,l.firstChild)),e.addEventListener("mouseenter",()=>{a=!0}),e.addEventListener("mouseleave",()=>{a=!1}),e.addEventListener("focusin",g=>{g.target!==y&&m!==void 0&&A(!0)}),window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener?.("change",g=>{g.matches&&A(!0)}),o(t),E()}}function X(i){let d=Array.from(i.querySelectorAll("[data-p60-sticky-header]")).filter(e=>!v(e,"sticky-header"));for(let e of d){let n=e.getAttribute("data-p60-sticky-header")==="auto-hide",t=window.scrollY,r=!1,l=()=>{let o=window.scrollY;e.classList.toggle("is-condensed",o>24),n&&(o>160&&o>t?e.classList.add("is-hidden"):e.classList.remove("is-hidden")),t=o,r=!1};window.addEventListener("scroll",()=>{r||(r=!0,requestAnimationFrame(l))},{passive:!0}),l()}}function W(i){let d=Array.from(i.querySelectorAll("[data-p60-sticky-cta]")).filter(e=>!v(e,"sticky-cta"));for(let e of d){let n=Number.parseInt(e.getAttribute("data-p60-sticky-cta")??"480",10)||480,t=!1,r=()=>{e.classList.toggle("is-stuck",window.scrollY>n),t=!1};window.addEventListener("scroll",()=>{t||(t=!0,requestAnimationFrame(r))},{passive:!0}),r()}}function j(i){let d=Array.from(i.querySelectorAll("[data-p60-lightbox]")).filter(e=>!v(e,"lightbox"));for(let e of d){let n=Array.from(e.querySelectorAll("a[data-p60-lightbox-item]"));if(n.length===0)continue;let t=null,r=0,l=null,o=()=>{t?.remove(),t=null,e.classList.remove("is-open"),document.removeEventListener("keydown",c),l?.focus()},s=u=>{r=(u+n.length)%n.length;let m=n[r];if(!t)return;let f=t.querySelector(".p60-lightbox-img"),p=t.querySelector(".p60-lightbox-caption");f&&(f.src=m.href);let b=m.getAttribute("data-p60-caption")??m.querySelector("img")?.alt??"";p&&(p.textContent=b),f&&(f.alt=b)},c=u=>{if(u.key==="Escape"&&o(),u.key==="ArrowLeft"&&s(r-1),u.key==="ArrowRight"&&s(r+1),u.key==="Tab"&&t){let m=Array.from(t.querySelectorAll("button")),f=m.indexOf(document.activeElement),p=u.shiftKey?f<=0?m.length-1:f-1:f>=m.length-1?0:f+1;m[p]?.focus(),u.preventDefault()}},a=(u,m)=>{l=m,t=document.createElement("div"),t.className="p60-lightbox",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),M()&&(t.style.transition="none"),t.innerHTML='<figure><img class="p60-lightbox-img" alt=""><figcaption class="p60-lightbox-caption"></figcaption></figure>';let f=(b,E,y)=>{let A=document.createElement("button");return A.type="button",A.className=b,A.setAttribute("aria-label",E),A.addEventListener("click",y),t.appendChild(A),A};n.length>1&&(f("p60-lightbox-prev","Previous image",()=>s(r-1)),f("p60-lightbox-next","Next image",()=>s(r+1)));let p=f("p60-lightbox-close","Close",o);t.addEventListener("click",b=>{b.target===t&&o()}),document.body.appendChild(t),e.classList.add("is-open"),document.addEventListener("keydown",c),s(u),p.focus()};n.forEach((u,m)=>u.addEventListener("click",f=>{f.preventDefault(),a(m,u)}))}}function G(i){let d=Array.from(i.querySelectorAll("[data-p60-tabs]")).filter(e=>!v(e,"tabs"));for(let e of d){let n=Array.from(e.querySelectorAll("[data-p60-tab]")),t=Array.from(e.querySelectorAll("[data-p60-panel]"));if(n.length===0||t.length===0)continue;let r=o=>t.find(s=>s.getAttribute("data-p60-panel")===o.getAttribute("data-p60-tab"));n[0].closest("[data-p60-tabs] > *")?.setAttribute("role","tablist");let l=(o,s=!1)=>{for(let c of n){let a=c===o;c.classList.toggle("is-active",a),c.setAttribute("aria-selected",a?"true":"false"),c.setAttribute("tabindex",a?"0":"-1")}for(let c of t){let a=c===r(o);c.classList.toggle("is-active",a),c.hidden=!a}s&&o.focus()};n.forEach((o,s)=>{o.setAttribute("role","tab"),o.addEventListener("click",c=>{c.preventDefault(),l(o)}),o.addEventListener("keydown",c=>{let a=c.key;a==="ArrowLeft"&&l(n[(s-1+n.length)%n.length],!0),a==="ArrowRight"&&l(n[(s+1)%n.length],!0),a==="Home"&&l(n[0],!0),a==="End"&&l(n[n.length-1],!0)})}),t.forEach(o=>o.setAttribute("role","tabpanel")),l(n.find(o=>o.classList.contains("is-active"))??n[0])}}var J=60,Q=280,k=()=>window.matchMedia("(hover: hover)").matches,Z='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',T=new Set,P=!1;function I(i){i.openTimer!==null&&(window.clearTimeout(i.openTimer),i.openTimer=null),i.closeTimer!==null&&(window.clearTimeout(i.closeTimer),i.closeTimer=null)}function w(i,d=!1){I(i);for(let e of Array.from(T))e!==i&&i.item.contains(e.item)&&w(e);i.item.classList.remove("is-open"),i.toggle.setAttribute("aria-expanded","false"),i.openedBy=null,T.delete(i),d&&i.toggle.focus()}function S(i,d){I(i);for(let e of Array.from(T))e!==i&&e.nav===i.nav&&!e.item.contains(i.item)&&w(e);i.item.classList.add("is-open"),i.toggle.setAttribute("aria-expanded","true"),i.openedBy=d,T.add(i)}function ee(i){for(let e of Array.from(i.querySelectorAll("[data-p60-nav]"))){let n=Array.from(e.querySelectorAll("[data-p60-nav-item]")).filter(t=>t.closest("[data-p60-nav]")===e&&!v(t,"nav-item"));for(let t of n){let r=a=>a.closest("[data-p60-nav-item]")===t,l=Array.from(t.querySelectorAll("[data-p60-nav-toggle]")).find(r)??null,o=Array.from(t.querySelectorAll("[data-p60-nav-menu]")).find(r)??null;if(!l||!o)continue;let s={nav:e,item:t,toggle:l,menu:o,openedBy:null,lastPointer:"",openTimer:null,closeTimer:null},c=()=>T.has(s);l.setAttribute("aria-expanded","false"),o.id&&!l.hasAttribute("aria-controls")&&l.setAttribute("aria-controls",o.id),t.addEventListener("mouseenter",()=>{k()&&(s.closeTimer!==null&&(window.clearTimeout(s.closeTimer),s.closeTimer=null),!c()&&s.openTimer===null&&(s.openTimer=window.setTimeout(()=>{s.openTimer=null,S(s,"hover")},J)))}),t.addEventListener("mouseleave",()=>{k()&&(s.openTimer!==null&&(window.clearTimeout(s.openTimer),s.openTimer=null),c()&&s.openedBy==="hover"&&s.closeTimer===null&&(s.closeTimer=window.setTimeout(()=>{s.closeTimer=null,w(s)},Q)))}),l.addEventListener("pointerdown",a=>{s.lastPointer=a.pointerType??""}),l.addEventListener("click",a=>{let u=a.detail===0||s.lastPointer==="touch"||s.lastPointer==="pen"||!k();if(!c()){a.preventDefault(),a.stopPropagation(),S(s,"toggle");return}!u&&s.openedBy==="hover"||(a.preventDefault(),a.stopPropagation(),w(s))}),l.addEventListener("keydown",a=>{a.key==="ArrowDown"&&(a.preventDefault(),c()||S(s,"toggle"),o.querySelector(Z)?.focus())}),t.addEventListener("keydown",a=>{a.key==="Escape"&&c()&&(a.stopPropagation(),w(s,!0))}),t.addEventListener("focusin",a=>{a.target instanceof Node&&o.contains(a.target)&&!c()&&S(s,"focus")}),t.addEventListener("focusout",a=>{let u=a.relatedTarget;u instanceof Node&&!t.contains(u)&&c()&&w(s)})}if(!v(e,"nav-fit")){let r=f=>{for(let p=1;p<=3;p++)e.classList.toggle(`is-fit-${p}`,p<=f)},l=f=>{let p=f.getBoundingClientRect();return p.width>0||p.height>0?[f]:Array.from(f.children).flatMap(l)},o=f=>{let p=-1/0;for(let b of f.flatMap(l)){let E=b.getBoundingClientRect();if(p!==-1/0&&E.top>=p-2)return!0;p=Math.max(p,E.bottom)}return!1},s=()=>{if(e.scrollWidth>e.clientWidth+1||o(Array.from(e.children)))return!1;let f=e.parentElement;return!(f&&getComputedStyle(f).flexWrap==="wrap"&&o(Array.from(f.children)))},c=0,a=()=>{if(c=performance.now(),e.offsetParent===null||!getComputedStyle(e).flexDirection.startsWith("row")){r(0);return}r(0);let f=0;for(;!s()&&f<3;)r(++f);c=performance.now()},u=!1,m=()=>{u||(u=!0,requestAnimationFrame(()=>{u=!1,a()}))};window.addEventListener("resize",m,{passive:!0}),document.fonts?.ready.then(m).catch(()=>{}),document.fonts?.addEventListener("loadingdone",m),"ResizeObserver"in window&&new ResizeObserver(()=>{performance.now()-c>120&&m()}).observe(e),a()}}let d=Array.from(i.querySelectorAll("[data-p60-nav-burger]")).filter(e=>!v(e,"nav-burger"));for(let e of d){let n=e.getAttribute("aria-controls")||e.getAttribute("data-p60-nav-burger")||"",t=n?document.getElementById(n):null;if(!t)continue;let r=()=>t.classList.contains("is-open"),l=o=>{if(t.classList.toggle("is-open",o),e.classList.toggle("is-open",o),e.setAttribute("aria-expanded",String(o)),!o)for(let s of Array.from(T))t.contains(s.item)&&w(s)};e.setAttribute("aria-expanded","false"),e.addEventListener("click",o=>{o.stopPropagation(),l(!r())}),t.addEventListener("click",o=>{o.target instanceof Element&&o.target.closest("a")&&l(!1)}),document.addEventListener("keydown",o=>{o.key==="Escape"&&r()&&(l(!1),e.focus())}),document.addEventListener("click",o=>{r()&&o.target instanceof Node&&!t.contains(o.target)&&!e.contains(o.target)&&l(!1)}),window.addEventListener("resize",()=>{r()&&e.offsetParent===null&&l(!1)})}P||(P=!0,document.addEventListener("click",e=>{for(let n of Array.from(T))(!(e.target instanceof Node)||!n.item.contains(e.target))&&w(n)}))}function te(i){let d=Array.from(i.querySelectorAll("[data-p60-reading-progress]")).filter(e=>!v(e,"reading-progress"));for(let e of d){let n=e.getAttribute("data-p60-reading-target"),t=n?document.getElementById(n):null;if(n&&!t)continue;e.setAttribute("role","progressbar"),e.setAttribute("aria-valuemin","0"),e.setAttribute("aria-valuemax","100"),!e.hasAttribute("aria-label")&&!e.hasAttribute("aria-labelledby")&&e.setAttribute("aria-label","Reading progress");let r=!1,l=()=>{let s=window.innerHeight,c=0,a=Math.max(document.documentElement.scrollHeight-s,0);if(t){let m=t.getBoundingClientRect();c=m.top+window.scrollY,a=Math.max(m.height-s,0)}let u=a===0?window.scrollY>=c?1:0:Math.min(Math.max((window.scrollY-c)/a,0),1);e.style.setProperty("--p60-reading-progress",u.toFixed(4)),e.setAttribute("aria-valuenow",String(Math.round(u*100))),e.classList.toggle("is-started",u>0),e.classList.toggle("is-complete",u>=1),r=!1},o=()=>{r||(r=!0,requestAnimationFrame(l))};window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o,{passive:!0}),l()}}function ne(i){let d=Array.from(i.querySelectorAll("[data-p60-show-more]")).filter(e=>!v(e,"show-more"));for(let e of d){let n=Math.max(Number.parseInt(e.getAttribute("data-p60-show-more")??"6",10)||6,1),t=Array.from(e.querySelectorAll("[data-p60-show-more-item]")),r=e.querySelector("[data-p60-show-more-toggle]")??(e.id?document.querySelector(`[data-p60-show-more-toggle][aria-controls="${e.id}"]`):null);if(!r)continue;let l=t.slice(n);if(l.length===0){r.hidden=!0;continue}let o=r.textContent??"",s=r.getAttribute("data-p60-show-more-toggle")??"";e.id&&!r.hasAttribute("aria-controls")&&r.setAttribute("aria-controls",e.id);let c=a=>{for(let u of l)u.hidden=!a;e.classList.toggle("is-expanded",a),r.setAttribute("aria-expanded",a?"true":"false"),s&&(r.textContent=a?s:o)};r.addEventListener("click",()=>c(r.getAttribute("aria-expanded")!=="true")),c(e.classList.contains("is-expanded"))}}function oe(i){let d=Array.from(i.querySelectorAll("[data-p60-scrollspy]")).filter(e=>!v(e,"scrollspy"));for(let e of d){let n=Number.parseInt(e.getAttribute("data-p60-scrollspy")??"96",10)||96,t=[];for(let s of Array.from(e.querySelectorAll('a[href^="#"]'))){let c=decodeURIComponent((s.getAttribute("href")??"").slice(1)),a=c?document.getElementById(c):null;a&&t.push({link:s,target:a})}if(t.length===0)continue;let r=!1,l=()=>{let s=null,c=-1/0;for(let{link:a,target:u}of t){let m=u.getBoundingClientRect().top;m<=n&&m>c&&(c=m,s=a)}for(let{link:a}of t){let u=a===s;a.classList.toggle("is-current",u),u?a.setAttribute("aria-current","location"):a.removeAttribute("aria-current")}r=!1},o=()=>{r||(r=!0,requestAnimationFrame(l))};window.addEventListener("scroll",o,{passive:!0}),window.addEventListener("resize",o,{passive:!0}),window.addEventListener("hashchange",o),l()}}var re=[["reveal",D],["counter",V],["progress",Y],["countdown",z],["accordion",K],["carousel",U],["stickyHeader",X],["stickyCta",W],["lightbox",j],["tabs",G],["readingProgress",te],["showMore",ne],["scrollspy",oe],["nav",ee]];function ie(i=document){document.documentElement.classList.add("p60-js");for(let[d,e]of re)try{e(i)}catch(n){console.warn(`[p60] behaviour "${d}" failed to initialise; the others still run`,n)}}return _(se);})();
|
|
2
|
+
"use strict";var p60Behaviors=(()=>{var R=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var W=(o,c)=>{for(var e in c)R(o,e,{get:c[e],enumerable:!0})},X=(o,c,e,s)=>{if(c&&typeof c=="object"||typeof c=="function")for(let t of K(c))!U.call(o,t)&&t!==e&&R(o,t,{get:()=>c[t],enumerable:!(s=z(c,t))||s.enumerable});return o};var j=o=>X(R({},"__esModule",{value:!0}),o);var Ee={};W(Ee,{disposeBehaviors:()=>G,initBehaviors:()=>be,revealBehaviorTarget:()=>J,setBehaviorEditing:()=>Q});var H=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,I=new WeakMap;function E(o,c){let e=I.get(o);e||I.set(o,e=new Map);let s=e.get(c);return s||e.set(c,s={dispose:[]}),s}function w(o,c){let e=new AbortController;return E(o,c).dispose.push(()=>e.abort()),e.signal}function A(o,c){return[...o instanceof Element&&o.matches(c)?[o]:[],...Array.from(o.querySelectorAll(c))]}function V(o){return A(o,"*")}function G(o,c=new Set){let e=Array.from(c);for(let s of V(o)){if(s.closest("astro-island")||e.some(n=>n===s||n.contains(s)))continue;let t=I.get(s);if(t)for(let[n,d]of t){for(let a of d.dispose)a();t.delete(n),s.removeAttribute(`data-p60-bound-${n}`)}}}function Q(o,c){for(let e of V(o))e.closest("astro-island")||I.get(e)?.forEach(s=>s.editing?.(c))}function J(o){if(o.closest("astro-island"))return;let c=[];for(let e=o;e;e=e.parentElement)c.push(e);for(let e of c.reverse())I.get(e)?.forEach(s=>s.reveal?.(o))}function L(o,c){let e=`data-p60-bound-${c}`;return o.hasAttribute(e)?!0:(o.setAttribute(e,""),E(o,c),!1)}var $=.15;function B(o,c,e){if(o=o.filter(t=>!t.classList.contains("is-revealed")),o.length===0)return;if(H()||!("IntersectionObserver"in window)){o.forEach(c);return}let s=new IntersectionObserver(t=>{for(let n of t){if(!n.isIntersecting)continue;(n.boundingClientRect.height>=window.innerHeight||n.intersectionRatio>=$-.001)&&(s.unobserve(n.target),c(n.target))}},{rootMargin:"0px 0px -10% 0px",threshold:[0,$]});o.forEach(t=>{s.observe(t),E(t,e).dispose.push(()=>s.unobserve(t))})}function Z(o){let c=A(o,"[data-p60-reveal-group]").filter(s=>!L(s,"reveal-group"));for(let s of c)Array.from(s.children).forEach((t,n)=>{t.style.setProperty("--p60-reveal-index",String(n)),t.classList.add("p60-reveal-child")});B(c,s=>s.classList.add("is-revealed"),"reveal-group");let e=A(o,"[data-p60-reveal]").filter(s=>!L(s,"reveal"));B(e,s=>s.classList.add("is-revealed"),"reveal")}var ee=/\d[\d, ]*(?:\.\d+)?/;function te(o){let c=A(o,"[data-p60-count]").filter(e=>!L(e,"count"));B(c,e=>{e.classList.add("is-revealed");let s=e.textContent??"",t=ee.exec(s);if(!t)return;let n=t[0].replace(/[, ]+$/,""),d=s.slice(0,t.index),a=s.slice(t.index+n.length),r=/[, ]/.test(n),i=n.includes(".")?n.length-n.indexOf(".")-1:0,u=Number.parseFloat(n.replace(/[, ]/g,"")),l=Number.parseFloat(e.getAttribute("data-p60-count")??""),f=Number.isFinite(l)?l:u;if(!Number.isFinite(f)||H())return;let p=Math.min(Number.parseInt(e.getAttribute("data-p60-count-duration")??"1200",10)||1200,4e3),m=new Intl.NumberFormat(document.documentElement.lang||void 0,{useGrouping:r,minimumFractionDigits:i,maximumFractionDigits:i}),v=performance.now(),h,M=T=>{let b=Math.min((T-v)/p,1),S=1-Math.pow(1-b,3);e.textContent=b<1?d+m.format(f*S)+a:s,b<1&&(h=requestAnimationFrame(M))};h=requestAnimationFrame(M),E(e,"count").dispose.push(()=>cancelAnimationFrame(h))},"count")}function ne(o){let c=A(o,"[data-p60-progress]").filter(e=>!L(e,"progress")&&!e.classList.contains("is-revealed"));if(!H()){for(let e of c){let s=e.style.width||getComputedStyle(e).width;e.style.transition="none",e.style.width="0px",e.offsetWidth,e.style.transition="width 1.1s cubic-bezier(0.22, 1, 0.36, 1)",e.dataset.p60ProgressTarget=s}B(c,e=>{e.classList.add("is-revealed"),e.style.width=e.dataset.p60ProgressTarget??""},"progress")}}function oe(o){let c=A(o,"[data-p60-countdown]").filter(e=>!L(e,"countdown"));for(let e of c){let s=Date.parse(e.getAttribute("data-p60-until")??"");if(e.classList.remove("is-elapsed"),!Number.isFinite(s))continue;let t=e.getAttribute("data-p60-countdown")||"dhm",n=t==="dhms",d=()=>{let r=s-Date.now();if(r<=0){e.classList.add("is-elapsed");let v=e.getAttribute("data-p60-elapsed-text");v&&(e.textContent=v),window.clearInterval(a);return}let i=Math.floor(r/1e3),u=Math.floor(i/86400),l=Math.floor(i%86400/3600),f=Math.floor(i%3600/60),p=i%60,m=[];t.includes("d")&&u>0&&m.push(`${u}d`),m.push(`${l}h`,`${f}m`),n&&m.push(`${p}s`),e.textContent=m.join(" ")},a=window.setInterval(d,n?1e3:15e3);E(e,"countdown").dispose.push(()=>window.clearInterval(a)),d()}}function ie(o){let c=A(o,"[data-p60-accordion]").filter(e=>!L(e,"accordion"));for(let e of c){let s=w(e,"accordion"),t=new Map;E(e,"accordion").dispose.push(()=>t.forEach((a,r)=>{r.cancel(),a.style.overflow=""}));let n=e.getAttribute("data-p60-accordion")!=="multi",d=Array.from(e.querySelectorAll("details"));for(let a of d){a.addEventListener("toggle",()=>{if(a.classList.toggle("is-open",a.open),a.open&&n)for(let u of d)u!==a&&u.open&&(u.open=!1)},{signal:s}),a.open&&a.classList.add("is-open");let i=a.querySelector("summary")?.nextElementSibling;i&&!H()&&a.addEventListener("toggle",()=>{if(!a.open)return;let u=i.scrollHeight;i.style.overflow="hidden";let l=i.animate([{height:"0px",opacity:"0"},{height:`${u}px`,opacity:"1"}],{duration:260,easing:"ease-out"});t.set(l,i),l.addEventListener("finish",()=>{t.delete(l),i.style.overflow=""})},{signal:s})}E(e,"accordion").reveal=a=>{let r=d.find(i=>i.contains(a));r&&(r.open=!0)}}}function se(o){let c=A(o,"[data-p60-carousel]").filter(e=>!L(e,"carousel"));for(let e of c){let s=w(e,"carousel"),t=Array.from(e.querySelectorAll("[data-p60-slide]")).filter(g=>g.closest("[data-p60-carousel]")===e);if(t.length<2){for(let g of t)g.classList.add("is-active"),g.removeAttribute("aria-hidden"),g.removeAttribute("inert");e.style.removeProperty("--p60-carousel-index");continue}let n=Math.max(0,t.findIndex(g=>g.classList.contains("is-active"))),d=[],a=document.createElement("nav");a.className="p60-carousel-dots",a.setAttribute("aria-label","Slides"),t.forEach((g,x)=>{let y=document.createElement("button");y.type="button",y.className="p60-carousel-dot",y.setAttribute("aria-label",`Go to slide ${x+1}`),y.addEventListener("click",()=>r(x,!0),{signal:s}),d.push(y),a.appendChild(y)}),e.appendChild(a),e.setAttribute("aria-roledescription","carousel"),e.hasAttribute("tabindex")||e.setAttribute("tabindex","0");let r=(g,x=!1)=>{n=(g+t.length)%t.length,t.forEach((y,P)=>{y.classList.toggle("is-active",P===n),P===n?(y.removeAttribute("aria-hidden"),y.removeAttribute("inert")):(y.setAttribute("aria-hidden","true"),y.setAttribute("inert",""))}),d.forEach((y,P)=>{y.classList.toggle("p60-carousel-dot--active",P===n),y.setAttribute("aria-current",P===n?"true":"false")}),e.style.setProperty("--p60-carousel-index",String(n)),x&&T()};e.querySelectorAll("[data-p60-carousel-prev]").forEach(g=>g.addEventListener("click",()=>r(n-1,!0),{signal:s})),e.querySelectorAll("[data-p60-carousel-next]").forEach(g=>g.addEventListener("click",()=>r(n+1,!0),{signal:s})),e.addEventListener("keydown",g=>{g.key==="ArrowLeft"&&r(n-1,!0),g.key==="ArrowRight"&&r(n+1,!0)},{signal:s});let i=0,u=0;e.addEventListener("pointerdown",g=>{i=g.clientX,u=g.clientY},{signal:s}),e.addEventListener("pointerup",g=>{let x=g.clientX-i,y=g.clientY-u;Math.abs(x)>40&&Math.abs(x)>Math.abs(y)&&r(n+(x<0?1:-1),!0)},{signal:s});let l=!1,f=e.classList.contains("is-paused"),p=!!e.closest("[data-p60-editor-behavior-selected]"),m,v=e.getAttribute("data-p60-carousel")==="auto"&&!H(),h=Math.max(Number.parseInt(e.getAttribute("data-p60-interval")??"6000",10)||6e3,3e3),M=()=>{m!==void 0&&window.clearInterval(m),m=void 0},T=()=>{M(),!(!v||f||p||H())&&(m=window.setInterval(()=>{!l&&!document.hidden&&r(n+1)},h))},b=null,S=()=>{b&&(b.disabled=p,b.setAttribute("aria-label",p?"Slide rotation paused while editing":f?"Start slide rotation":"Stop slide rotation"),b.classList.toggle("p60-carousel-pause--stopped",f||p))},q=g=>{f=g,e.classList.toggle("is-paused",f),S(),T()};v&&(b=document.createElement("button"),b.type="button",b.className="p60-carousel-pause",b.setAttribute("aria-label","Stop slide rotation"),b.addEventListener("click",()=>q(!f),{signal:s}),a.insertBefore(b,a.firstChild)),e.addEventListener("mouseenter",()=>{l=!0},{signal:s}),e.addEventListener("mouseleave",()=>{l=!1},{signal:s}),e.addEventListener("focusin",g=>{g.target!==b&&m!==void 0&&q(!0)},{signal:s});let D=window.matchMedia("(prefers-reduced-motion: reduce)"),_=g=>{g.matches&&q(!0)};D.addEventListener?.("change",_),E(e,"carousel").dispose.push(M,()=>a.remove(),()=>D.removeEventListener?.("change",_)),E(e,"carousel").editing=g=>{p=g,S(),T()},E(e,"carousel").reveal=g=>{let x=t.findIndex(y=>y===g||y.contains(g));x>=0&&r(x)},r(n),S(),T()}}function re(o){let c=A(o,"[data-p60-sticky-header]").filter(e=>!L(e,"sticky-header"));for(let e of c){let s=w(e,"sticky-header"),t;E(e,"sticky-header").dispose.push(()=>{t!==void 0&&cancelAnimationFrame(t)});let n=e.getAttribute("data-p60-sticky-header")==="auto-hide",d=window.scrollY,a=!1,r=()=>{let i=window.scrollY;e.classList.toggle("is-condensed",i>24),n&&i>160&&i>d?e.classList.add("is-hidden"):e.classList.remove("is-hidden"),d=i,a=!1};window.addEventListener("scroll",()=>{a||(a=!0,t=requestAnimationFrame(r))},{passive:!0,signal:s}),r()}}function ae(o){let c=A(o,"[data-p60-sticky-cta]").filter(e=>!L(e,"sticky-cta"));for(let e of c){let s=w(e,"sticky-cta"),t;E(e,"sticky-cta").dispose.push(()=>{t!==void 0&&cancelAnimationFrame(t)});let n=Number.parseInt(e.getAttribute("data-p60-sticky-cta")??"480",10)||480,d=!1,a=()=>{e.classList.toggle("is-stuck",window.scrollY>n),d=!1};window.addEventListener("scroll",()=>{d||(d=!0,t=requestAnimationFrame(a))},{passive:!0,signal:s}),a()}}function le(o){let c=A(o,"[data-p60-lightbox]").filter(e=>!L(e,"lightbox"));for(let e of c){let s=w(e,"lightbox"),t=Array.from(e.querySelectorAll("a[data-p60-lightbox-item]"));if(t.length===0)continue;let n=null,d=0,a=null,r=()=>{n?.remove(),n=null,e.classList.remove("is-open"),document.removeEventListener("keydown",u),a?.focus()},i=f=>{d=(f+t.length)%t.length;let p=t[d];if(!n)return;let m=n.querySelector(".p60-lightbox-img"),v=n.querySelector(".p60-lightbox-caption");m&&(m.src=p.href);let h=p.getAttribute("data-p60-caption")??p.querySelector("img")?.alt??"";v&&(v.textContent=h),m&&(m.alt=h)},u=f=>{if(f.key==="Escape"&&r(),f.key==="ArrowLeft"&&i(d-1),f.key==="ArrowRight"&&i(d+1),f.key==="Tab"&&n){let p=Array.from(n.querySelectorAll("button")),m=p.indexOf(document.activeElement),v=f.shiftKey?m<=0?p.length-1:m-1:m>=p.length-1?0:m+1;p[v]?.focus(),f.preventDefault()}},l=(f,p)=>{a=p,n=document.createElement("div"),n.className="p60-lightbox",n.setAttribute("role","dialog"),n.setAttribute("aria-modal","true"),H()&&(n.style.transition="none"),n.innerHTML='<figure><img class="p60-lightbox-img" alt=""><figcaption class="p60-lightbox-caption"></figcaption></figure>';let m=(h,M,T)=>{let b=document.createElement("button");return b.type="button",b.className=h,b.setAttribute("aria-label",M),b.addEventListener("click",T),n.appendChild(b),b};t.length>1&&(m("p60-lightbox-prev","Previous image",()=>i(d-1)),m("p60-lightbox-next","Next image",()=>i(d+1)));let v=m("p60-lightbox-close","Close",r);n.addEventListener("click",h=>{h.target===n&&r()}),document.body.appendChild(n),e.classList.add("is-open"),document.addEventListener("keydown",u),i(f),v.focus()};E(e,"lightbox").dispose.push(()=>{n?.remove(),n=null,e.classList.remove("is-open"),document.removeEventListener("keydown",u)}),t.forEach((f,p)=>f.addEventListener("click",m=>{m.preventDefault(),l(p,f)},{signal:s}))}}function ce(o){let c=A(o,"[data-p60-tabs]").filter(e=>!L(e,"tabs"));for(let e of c){let s=w(e,"tabs"),t=Array.from(e.querySelectorAll("[data-p60-tab]")).filter(r=>r.closest("[data-p60-tabs]")===e),n=Array.from(e.querySelectorAll("[data-p60-panel]")).filter(r=>r.closest("[data-p60-tabs]")===e);if(t.length===0||n.length===0)continue;let d=r=>n.find(i=>i.getAttribute("data-p60-panel")===r.getAttribute("data-p60-tab"));t[0].closest("[data-p60-tabs] > *")?.setAttribute("role","tablist");let a=(r,i=!1)=>{for(let u of t){let l=u===r;u.classList.toggle("is-active",l),u.setAttribute("aria-selected",l?"true":"false"),u.setAttribute("tabindex",l?"0":"-1")}for(let u of n){let l=u===d(r);u.classList.toggle("is-active",l),u.hidden=!l}i&&r.focus()};t.forEach((r,i)=>{r.setAttribute("role","tab"),r.addEventListener("click",u=>{u.preventDefault(),a(r)},{signal:s}),r.addEventListener("keydown",u=>{let l=u.key;l==="ArrowLeft"&&a(t[(i-1+t.length)%t.length],!0),l==="ArrowRight"&&a(t[(i+1)%t.length],!0),l==="Home"&&a(t[0],!0),l==="End"&&a(t[t.length-1],!0)},{signal:s})}),n.forEach(r=>r.setAttribute("role","tabpanel")),E(e,"tabs").reveal=r=>{let i=t.find(u=>u===r||u.contains(r)||d(u)?.contains(r));i&&a(i)},a(t.find(r=>r.classList.contains("is-active"))??t[0])}}var de=60,ue=280,F=()=>window.matchMedia("(hover: hover)").matches,fe='a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',k=new Set,Y=!1;function O(o){o.openTimer!==null&&(window.clearTimeout(o.openTimer),o.openTimer=null),o.closeTimer!==null&&(window.clearTimeout(o.closeTimer),o.closeTimer=null)}function N(o,c=!1){O(o);for(let e of Array.from(k))e!==o&&o.item.contains(e.item)&&N(e);o.item.classList.remove("is-open"),o.toggle.setAttribute("aria-expanded","false"),o.openedBy=null,k.delete(o),c&&o.toggle.focus()}function C(o,c){O(o);for(let e of Array.from(k))e!==o&&e.nav===o.nav&&!e.item.contains(o.item)&&N(e);o.item.classList.add("is-open"),o.toggle.setAttribute("aria-expanded","true"),o.openedBy=c,k.add(o)}function me(o){for(let e of A(o,"[data-p60-nav]")){let s=Array.from(e.querySelectorAll("[data-p60-nav-item]")).filter(t=>t.closest("[data-p60-nav]")===e&&!L(t,"nav-item"));for(let t of s){let n=w(t,"nav-item"),d=l=>l.closest("[data-p60-nav-item]")===t,a=Array.from(t.querySelectorAll("[data-p60-nav-toggle]")).find(d)??null,r=Array.from(t.querySelectorAll("[data-p60-nav-menu]")).find(d)??null;if(!a||!r)continue;let i={nav:e,item:t,toggle:a,menu:r,openedBy:null,lastPointer:"",openTimer:null,closeTimer:null,editing:!!t.closest("[data-p60-editor-behavior-selected]")};E(t,"nav-item").dispose.push(()=>N(i)),E(t,"nav-item").reveal=l=>{(r===l||r.contains(l))&&C(i,"toggle")},E(t,"nav-item").editing=l=>{i.editing=l,l&&O(i)};let u=()=>k.has(i);a.setAttribute("aria-expanded","false"),r.id&&!a.hasAttribute("aria-controls")&&a.setAttribute("aria-controls",r.id),t.addEventListener("mouseenter",()=>{F()&&(i.closeTimer!==null&&(window.clearTimeout(i.closeTimer),i.closeTimer=null),!u()&&i.openTimer===null&&(i.openTimer=window.setTimeout(()=>{i.openTimer=null,C(i,"hover")},de)))},{signal:n}),t.addEventListener("mouseleave",()=>{!F()||i.editing||(i.openTimer!==null&&(window.clearTimeout(i.openTimer),i.openTimer=null),u()&&i.openedBy==="hover"&&i.closeTimer===null&&(i.closeTimer=window.setTimeout(()=>{i.closeTimer=null,N(i)},ue)))},{signal:n}),a.addEventListener("pointerdown",l=>{i.lastPointer=l.pointerType??""},{signal:n}),a.addEventListener("click",l=>{let f=l.detail===0||i.lastPointer==="touch"||i.lastPointer==="pen"||!F();if(!u()){l.preventDefault(),l.stopPropagation(),C(i,"toggle");return}!f&&i.openedBy==="hover"||(l.preventDefault(),l.stopPropagation(),N(i))},{signal:n}),a.addEventListener("keydown",l=>{l.key==="ArrowDown"&&(l.preventDefault(),u()||C(i,"toggle"),r.querySelector(fe)?.focus())},{signal:n}),t.addEventListener("keydown",l=>{l.key==="Escape"&&u()&&(l.stopPropagation(),N(i,!0))},{signal:n}),t.addEventListener("focusin",l=>{l.target instanceof Node&&r.contains(l.target)&&!u()&&C(i,"focus")},{signal:n}),t.addEventListener("focusout",l=>{let f=l.relatedTarget;!i.editing&&f instanceof Node&&!t.contains(f)&&u()&&N(i)},{signal:n})}if(!L(e,"nav-fit")){let t=w(e,"nav-fit"),n=3,d=v=>{for(let h=1;h<=n;h++)e.classList.toggle(`is-fit-${h}`,h<=v)},a=v=>{let h=v.getBoundingClientRect();return h.width>0||h.height>0?[v]:Array.from(v.children).flatMap(a)},r=v=>{let h=-1/0;for(let M of v.flatMap(a)){let T=M.getBoundingClientRect();if(h!==-1/0&&T.top>=h-2)return!0;h=Math.max(h,T.bottom)}return!1},i=()=>{if(e.scrollWidth>e.clientWidth+1||r(Array.from(e.children)))return!1;let v=e.parentElement;return!(v&&getComputedStyle(v).flexWrap==="wrap"&&r(Array.from(v.children)))},u=0,l=()=>{if(u=performance.now(),e.offsetParent===null||!getComputedStyle(e).flexDirection.startsWith("row")){d(0);return}d(0);let v=0;for(;!i()&&v<n;)d(++v);u=performance.now()},f=!1,p,m=()=>{f||t.aborted||(f=!0,p=requestAnimationFrame(()=>{f=!1,l()}))};if(window.addEventListener("resize",m,{passive:!0,signal:t}),document.fonts?.ready.then(m).catch(()=>{}),document.fonts?.addEventListener("loadingdone",m,{signal:t}),E(e,"nav-fit").dispose.push(()=>{p!==void 0&&cancelAnimationFrame(p)}),"ResizeObserver"in window){let v=new ResizeObserver(()=>{performance.now()-u>120&&m()});v.observe(e),E(e,"nav-fit").dispose.push(()=>v.disconnect())}l()}}let c=A(o,"[data-p60-nav-burger]").filter(e=>!L(e,"nav-burger"));for(let e of c){let s=w(e,"nav-burger"),t=e.getAttribute("aria-controls")||e.getAttribute("data-p60-nav-burger")||"",n=t?document.getElementById(t):null;if(!n)continue;let d=()=>n.classList.contains("is-open"),a=r=>{if(n.classList.toggle("is-open",r),e.classList.toggle("is-open",r),e.setAttribute("aria-expanded",String(r)),!r)for(let i of Array.from(k))n.contains(i.item)&&N(i)};E(e,"nav-burger").reveal=()=>a(!0),e.setAttribute("aria-expanded","false"),e.addEventListener("click",r=>{r.stopPropagation(),a(!d())},{signal:s}),n.addEventListener("click",r=>{r.target instanceof Element&&r.target.closest("a")&&a(!1)},{signal:s}),document.addEventListener("keydown",r=>{r.key==="Escape"&&d()&&(a(!1),e.focus())},{signal:s}),document.addEventListener("click",r=>{d()&&r.target instanceof Node&&!n.contains(r.target)&&!e.contains(r.target)&&a(!1)},{signal:s}),window.addEventListener("resize",()=>{d()&&e.offsetParent===null&&a(!1)},{signal:s})}Y||(Y=!0,document.addEventListener("click",e=>{for(let s of Array.from(k))!s.editing&&(!(e.target instanceof Node)||!s.item.contains(e.target))&&N(s)}))}function pe(o){let c=A(o,"[data-p60-reading-progress]").filter(e=>!L(e,"reading-progress"));for(let e of c){let s=w(e,"reading-progress"),t;E(e,"reading-progress").dispose.push(()=>{t!==void 0&&cancelAnimationFrame(t)});let n=e.getAttribute("data-p60-reading-target"),d=n?document.getElementById(n):null;if(n&&!d)continue;e.setAttribute("role","progressbar"),e.setAttribute("aria-valuemin","0"),e.setAttribute("aria-valuemax","100"),!e.hasAttribute("aria-label")&&!e.hasAttribute("aria-labelledby")&&e.setAttribute("aria-label","Reading progress");let a=!1,r=()=>{let u=window.innerHeight,l=0,f=Math.max(document.documentElement.scrollHeight-u,0);if(d){let m=d.getBoundingClientRect();l=m.top+window.scrollY,f=Math.max(m.height-u,0)}let p=f===0?window.scrollY>=l?1:0:Math.min(Math.max((window.scrollY-l)/f,0),1);e.style.setProperty("--p60-reading-progress",p.toFixed(4)),e.setAttribute("aria-valuenow",String(Math.round(p*100))),e.classList.toggle("is-started",p>0),e.classList.toggle("is-complete",p>=1),a=!1},i=()=>{a||(a=!0,t=requestAnimationFrame(r))};window.addEventListener("scroll",i,{passive:!0,signal:s}),window.addEventListener("resize",i,{passive:!0,signal:s}),r()}}function ge(o){let c=A(o,"[data-p60-show-more]").filter(e=>!L(e,"show-more"));for(let e of c){let s=w(e,"show-more"),t=Math.max(Number.parseInt(e.getAttribute("data-p60-show-more")??"6",10)||6,1),n=Array.from(e.querySelectorAll("[data-p60-show-more-item]")),d=e.querySelector("[data-p60-show-more-toggle]")??(e.id?document.querySelector(`[data-p60-show-more-toggle][aria-controls="${e.id}"]`):null);if(!d)continue;let a=n.slice(t);if(a.length===0){d.hidden=!0;continue}let r=d.textContent??"",i=d.getAttribute("data-p60-show-more-toggle")??"";e.id&&!d.hasAttribute("aria-controls")&&d.setAttribute("aria-controls",e.id);let u=l=>{for(let f of a)f.hidden=!l;e.classList.toggle("is-expanded",l),d.setAttribute("aria-expanded",l?"true":"false"),i&&(d.textContent=l?i:r)};d.addEventListener("click",()=>u(d.getAttribute("aria-expanded")!=="true"),{signal:s}),u(e.classList.contains("is-expanded"))}}function ve(o){let c=A(o,"[data-p60-scrollspy]").filter(e=>!L(e,"scrollspy"));for(let e of c){let s=w(e,"scrollspy"),t;E(e,"scrollspy").dispose.push(()=>{t!==void 0&&cancelAnimationFrame(t)});let n=Number.parseInt(e.getAttribute("data-p60-scrollspy")??"96",10)||96,d=[];for(let u of Array.from(e.querySelectorAll('a[href^="#"]'))){let l=decodeURIComponent((u.getAttribute("href")??"").slice(1)),f=l?document.getElementById(l):null;f&&d.push({link:u,target:f})}if(d.length===0)continue;let a=!1,r=()=>{let u=null,l=-1/0;for(let{link:f,target:p}of d){let m=p.getBoundingClientRect().top;m<=n&&m>l&&(l=m,u=f)}for(let{link:f}of d){let p=f===u;f.classList.toggle("is-current",p),p?f.setAttribute("aria-current","location"):f.removeAttribute("aria-current")}a=!1},i=()=>{a||(a=!0,t=requestAnimationFrame(r))};window.addEventListener("scroll",i,{passive:!0,signal:s}),window.addEventListener("resize",i,{passive:!0,signal:s}),window.addEventListener("hashchange",i,{signal:s}),r()}}var he=[["reveal",Z],["counter",te],["progress",ne],["countdown",oe],["accordion",ie],["carousel",se],["stickyHeader",re],["stickyCta",ae],["lightbox",le],["tabs",ce],["readingProgress",pe],["showMore",ge],["scrollspy",ve],["nav",me]];function be(o=document){document.documentElement.classList.add("p60-js");for(let[c,e]of he)try{e(o)}catch(s){console.warn(`[p60] behaviour "${c}" failed to initialise; the others still run`,s)}}return j(Ee);})();
|
|
@@ -8,7 +8,6 @@ export async function proveCollectionLinkVisibility(render, entry, declared) {
|
|
|
8
8
|
const errors = [];
|
|
9
9
|
const href = `/p60-collection-link-${entry.type}`;
|
|
10
10
|
const content = { ...structuredClone(entry.sample ?? {}) };
|
|
11
|
-
// Authored appeal cards intentionally replace the connected causes collection.
|
|
12
11
|
if (entry.type === 'appealGrid') content.items = [];
|
|
13
12
|
delete content.showCollectionLink;
|
|
14
13
|
let baseline;
|