@canopy-iiif/app 0.8.6 → 0.9.1
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/lib/AGENTS.md +5 -0
- package/lib/build/build.js +2 -13
- package/lib/build/dev.js +9 -2
- package/lib/build/iiif.js +6 -7
- package/lib/build/mdx.js +145 -58
- package/lib/build/pages.js +13 -1
- package/lib/build/runtimes.js +1 -5
- package/lib/components/hero-slider-runtime.js +78 -0
- package/lib/orchestrator.js +0 -30
- package/package.json +1 -1
- package/types/orchestrator.d.ts +0 -1
- package/ui/dist/server.mjs +572 -132
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/components/_buttons.scss +50 -0
- package/ui/styles/components/_interstitial-hero.scss +411 -0
- package/ui/styles/components/_sub-navigation.scss +0 -1
- package/ui/styles/components/index.scss +2 -1
- package/ui/styles/index.css +364 -23
- package/ui/theme.js +41 -5
- package/lib/build/verify.js +0 -60
- package/ui/styles/components/_hero.scss +0 -21
package/lib/AGENTS.md
CHANGED
|
@@ -34,6 +34,11 @@ Current Focus
|
|
|
34
34
|
- Runtime bundling: review `search/search.js` for esbuild config drift (externals, shims); list required adjustments if UI adds new hydrated components.
|
|
35
35
|
- Cache hygiene: document when `.cache/mdx` vs `.cache/iiif` are pruned and add TODOs if cleanup relies on manual intervention.
|
|
36
36
|
|
|
37
|
+
### Interstitial hero support
|
|
38
|
+
- Featured manifests come from `canopy.yml → featured`; `lib/components/featured.js` normalizes IDs, resolves slugs from `.cache/iiif/index.json`, and surfaces thumbnails (`heroThumbnail*` when the IIIF build computed representative images).
|
|
39
|
+
- `build/iiif.js` now writes `heroThumbnail`, `heroThumbnailWidth`, and `heroThumbnailHeight` alongside standard thumbnails so the UI hero can display consistent crops.
|
|
40
|
+
- `build/mdx.js` → `ensureHeroRuntime()` bundles `packages/app/lib/components/hero-slider-runtime.js` to `site/scripts/canopy-hero-slider.js`; keep Swiper external and mirror any esbuild option changes in the UI workspace docs.
|
|
41
|
+
|
|
37
42
|
Risks & Watchpoints
|
|
38
43
|
-------------------
|
|
39
44
|
- Long reruns: if `dev` mode clears `.cache/iiif` too aggressively it slows iteration; capture triggers before modifying cleanup behavior.
|
package/lib/build/build.js
CHANGED
|
@@ -19,7 +19,6 @@ const {
|
|
|
19
19
|
const { ensureStyles } = require("./styles");
|
|
20
20
|
const { copyAssets } = require("./assets");
|
|
21
21
|
const { logLine } = require("./log");
|
|
22
|
-
const { verifyBuildOutput } = require("./verify");
|
|
23
22
|
const navigation = require("../components/navigation");
|
|
24
23
|
|
|
25
24
|
// hold records between builds if skipping IIIF
|
|
@@ -74,7 +73,7 @@ async function build(options = {}) {
|
|
|
74
73
|
);
|
|
75
74
|
}
|
|
76
75
|
// Ensure any configured featured manifests are cached (and thumbnails computed)
|
|
77
|
-
// so SSR
|
|
76
|
+
// so SSR interstitials can resolve items even if they are not part of
|
|
78
77
|
// the traversed collection or when IIIF build is skipped during incremental rebuilds.
|
|
79
78
|
try { await iiif.ensureFeaturedInCache(CONFIG); } catch (_) {}
|
|
80
79
|
|
|
@@ -87,7 +86,7 @@ async function build(options = {}) {
|
|
|
87
86
|
bright: true,
|
|
88
87
|
underscore: true,
|
|
89
88
|
});
|
|
90
|
-
//
|
|
89
|
+
// Interstitials read directly from the local IIIF cache; no API file needed
|
|
91
90
|
pageRecords = await searchBuild.collectMdxPageRecords();
|
|
92
91
|
await pages.buildContentTree(CONTENT_DIR, pageRecords);
|
|
93
92
|
logLine("✓ MDX pages built", "green");
|
|
@@ -135,16 +134,6 @@ async function build(options = {}) {
|
|
|
135
134
|
});
|
|
136
135
|
await copyAssets();
|
|
137
136
|
|
|
138
|
-
/**
|
|
139
|
-
* Final verification (checklist)
|
|
140
|
-
*/
|
|
141
|
-
try {
|
|
142
|
-
verifyBuildOutput({ outDir: OUT_DIR });
|
|
143
|
-
} catch (e) {
|
|
144
|
-
logLine("✗ Build verification failed", "red", { bright: true });
|
|
145
|
-
logLine(String(e && e.message ? e.message : e), "red");
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
137
|
}
|
|
149
138
|
|
|
150
139
|
module.exports = { build };
|
package/lib/build/dev.js
CHANGED
|
@@ -1050,6 +1050,13 @@ async function dev() {
|
|
|
1050
1050
|
}
|
|
1051
1051
|
};
|
|
1052
1052
|
attachCssWatcher();
|
|
1053
|
+
const handleUiSassChange = () => {
|
|
1054
|
+
attachCssWatcher();
|
|
1055
|
+
scheduleTailwindRestart(
|
|
1056
|
+
"[tailwind] detected @canopy-iiif/app/ui Sass change — restarting Tailwind",
|
|
1057
|
+
"[tailwind] compile after UI Sass change failed"
|
|
1058
|
+
);
|
|
1059
|
+
};
|
|
1053
1060
|
if (fs.existsSync(uiStylesDir)) {
|
|
1054
1061
|
try {
|
|
1055
1062
|
fs.watch(
|
|
@@ -1057,7 +1064,7 @@ async function dev() {
|
|
|
1057
1064
|
{ persistent: false, recursive: true },
|
|
1058
1065
|
(evt, fn) => {
|
|
1059
1066
|
try {
|
|
1060
|
-
if (fn && /\.s[ac]ss$/i.test(String(fn)))
|
|
1067
|
+
if (fn && /\.s[ac]ss$/i.test(String(fn))) handleUiSassChange();
|
|
1061
1068
|
} catch (_) {}
|
|
1062
1069
|
}
|
|
1063
1070
|
);
|
|
@@ -1071,7 +1078,7 @@ async function dev() {
|
|
|
1071
1078
|
{ persistent: false },
|
|
1072
1079
|
(evt, fn) => {
|
|
1073
1080
|
try {
|
|
1074
|
-
if (fn && /\.s[ac]ss$/i.test(String(fn)))
|
|
1081
|
+
if (fn && /\.s[ac]ss$/i.test(String(fn))) handleUiSassChange();
|
|
1075
1082
|
} catch (_) {}
|
|
1076
1083
|
scan(dir);
|
|
1077
1084
|
}
|
package/lib/build/iiif.js
CHANGED
|
@@ -578,7 +578,7 @@ async function saveCachedManifest(manifest, id, parentId) {
|
|
|
578
578
|
}
|
|
579
579
|
|
|
580
580
|
// Ensure any configured featured manifests are present in the local cache
|
|
581
|
-
// (and have thumbnails computed) so
|
|
581
|
+
// (and have thumbnails computed) so interstitial heroes can read them.
|
|
582
582
|
async function ensureFeaturedInCache(cfg) {
|
|
583
583
|
try {
|
|
584
584
|
const CONFIG = cfg || (await loadConfig());
|
|
@@ -1147,7 +1147,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1147
1147
|
const needsHydrateViewer =
|
|
1148
1148
|
body.includes("data-canopy-viewer") || body.includes("data-canopy-scroll");
|
|
1149
1149
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
1150
|
-
const
|
|
1150
|
+
const needsHeroSlider = body.includes("data-canopy-hero-slider");
|
|
1151
1151
|
const needsSearchForm = body.includes("data-canopy-search-form");
|
|
1152
1152
|
const needsHydrate =
|
|
1153
1153
|
body.includes("data-canopy-hydrate") ||
|
|
@@ -1173,11 +1173,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1173
1173
|
.split(path.sep)
|
|
1174
1174
|
.join("/")
|
|
1175
1175
|
: null;
|
|
1176
|
-
const heroRel =
|
|
1176
|
+
const heroRel = needsHeroSlider
|
|
1177
1177
|
? path
|
|
1178
1178
|
.relative(
|
|
1179
1179
|
path.dirname(outPath),
|
|
1180
|
-
path.join(OUT_DIR, "scripts", "canopy-hero.js")
|
|
1180
|
+
path.join(OUT_DIR, "scripts", "canopy-hero-slider.js")
|
|
1181
1181
|
)
|
|
1182
1182
|
.split(path.sep)
|
|
1183
1183
|
.join("/")
|
|
@@ -1202,15 +1202,14 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1202
1202
|
: null;
|
|
1203
1203
|
|
|
1204
1204
|
let jsRel = null;
|
|
1205
|
-
if (
|
|
1205
|
+
if (needsHeroSlider && heroRel) jsRel = heroRel;
|
|
1206
1206
|
else if (needsRelated && sliderRel) jsRel = sliderRel;
|
|
1207
1207
|
else if (viewerRel) jsRel = viewerRel;
|
|
1208
1208
|
|
|
1209
1209
|
let headExtra = head;
|
|
1210
1210
|
const needsReact = !!(
|
|
1211
1211
|
needsHydrateViewer ||
|
|
1212
|
-
needsRelated
|
|
1213
|
-
needsHero
|
|
1212
|
+
needsRelated
|
|
1214
1213
|
);
|
|
1215
1214
|
let vendorTag = "";
|
|
1216
1215
|
if (needsReact) {
|
package/lib/build/mdx.js
CHANGED
|
@@ -113,7 +113,7 @@ async function loadUiComponents() {
|
|
|
113
113
|
}
|
|
114
114
|
let comp = (mod && typeof mod === 'object') ? mod : {};
|
|
115
115
|
// Hard-require core exports; do not inject fallbacks
|
|
116
|
-
const required = ['SearchPanel', 'SearchFormModal', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', '
|
|
116
|
+
const required = ['SearchPanel', 'SearchFormModal', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Interstitials'];
|
|
117
117
|
const missing = required.filter((k) => !comp || !comp[k]);
|
|
118
118
|
if (missing.length) {
|
|
119
119
|
throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
|
|
@@ -139,6 +139,50 @@ async function loadUiComponents() {
|
|
|
139
139
|
return UI_COMPONENTS;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
function slugifyHeading(text) {
|
|
143
|
+
try {
|
|
144
|
+
return String(text || '')
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.trim()
|
|
147
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
148
|
+
.replace(/\s+/g, '-');
|
|
149
|
+
} catch (_) {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function extractHeadings(mdxSource) {
|
|
155
|
+
const { content } = parseFrontmatter(String(mdxSource || ''));
|
|
156
|
+
const cleaned = content.replace(/```[\s\S]*?```/g, '');
|
|
157
|
+
const headingRegex = /^ {0,3}(#{1,6})\s+(.+?)\s*$/gm;
|
|
158
|
+
const seen = new Map();
|
|
159
|
+
const headings = [];
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = headingRegex.exec(cleaned))) {
|
|
162
|
+
const hashes = match[1] || '';
|
|
163
|
+
const depth = hashes.length;
|
|
164
|
+
let raw = match[2] || '';
|
|
165
|
+
let explicitId = null;
|
|
166
|
+
const idMatch = raw.match(/\s*\{#([A-Za-z0-9_-]+)\}\s*$/);
|
|
167
|
+
if (idMatch) {
|
|
168
|
+
explicitId = idMatch[1];
|
|
169
|
+
raw = raw.slice(0, raw.length - idMatch[0].length);
|
|
170
|
+
}
|
|
171
|
+
const title = raw.replace(/\<[^>]*\>/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').trim();
|
|
172
|
+
if (!title) continue;
|
|
173
|
+
const baseId = explicitId || slugifyHeading(title) || `section-${headings.length + 1}`;
|
|
174
|
+
const count = seen.get(baseId) || 0;
|
|
175
|
+
seen.set(baseId, count + 1);
|
|
176
|
+
const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
|
|
177
|
+
headings.push({
|
|
178
|
+
id,
|
|
179
|
+
title,
|
|
180
|
+
depth,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return headings;
|
|
184
|
+
}
|
|
185
|
+
|
|
142
186
|
function extractTitle(mdxSource) {
|
|
143
187
|
const { data, content } = parseFrontmatter(String(mdxSource || ""));
|
|
144
188
|
if (data && typeof data.title === "string" && data.title.trim()) {
|
|
@@ -271,6 +315,84 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
|
|
|
271
315
|
const mod = await import(pathToFileURL(tmpFile).href + bust);
|
|
272
316
|
const MDXContent = mod.default || mod.MDXContent || mod;
|
|
273
317
|
const components = await loadUiComponents();
|
|
318
|
+
const rawHeadings = Array.isArray(extraProps && extraProps.page && extraProps.page.headings)
|
|
319
|
+
? extraProps.page.headings
|
|
320
|
+
.map((heading) => (heading ? { ...heading } : heading))
|
|
321
|
+
.filter(Boolean)
|
|
322
|
+
: [];
|
|
323
|
+
const headingQueue = rawHeadings.slice();
|
|
324
|
+
const headingIdCounts = new Map();
|
|
325
|
+
headingQueue.forEach((heading) => {
|
|
326
|
+
if (heading && heading.id) {
|
|
327
|
+
const key = String(heading.id);
|
|
328
|
+
headingIdCounts.set(key, (headingIdCounts.get(key) || 0) + 1);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
function reserveHeadingId(base) {
|
|
333
|
+
const fallback = base || 'section';
|
|
334
|
+
let candidate = fallback;
|
|
335
|
+
let attempt = 1;
|
|
336
|
+
while (headingIdCounts.has(candidate)) {
|
|
337
|
+
attempt += 1;
|
|
338
|
+
candidate = `${fallback}-${attempt}`;
|
|
339
|
+
}
|
|
340
|
+
headingIdCounts.set(candidate, 1);
|
|
341
|
+
return candidate;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function extractTextFromChildren(children) {
|
|
345
|
+
if (children == null) return '';
|
|
346
|
+
if (typeof children === 'string' || typeof children === 'number') return String(children);
|
|
347
|
+
if (Array.isArray(children)) return children.map((child) => extractTextFromChildren(child)).join('');
|
|
348
|
+
if (React.isValidElement(children)) return extractTextFromChildren(children.props && children.props.children);
|
|
349
|
+
return '';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function takeHeading(level, children) {
|
|
353
|
+
if (!headingQueue.length) return null;
|
|
354
|
+
const idx = headingQueue.findIndex((item) => {
|
|
355
|
+
if (!item || typeof item !== 'object') return false;
|
|
356
|
+
const depth = typeof item.depth === 'number' ? item.depth : item.level;
|
|
357
|
+
return depth === level;
|
|
358
|
+
});
|
|
359
|
+
if (idx === -1) return null;
|
|
360
|
+
const [heading] = headingQueue.splice(idx, 1);
|
|
361
|
+
if (!heading.id) {
|
|
362
|
+
const text = heading.title || extractTextFromChildren(children);
|
|
363
|
+
const baseId = slugifyHeading(text);
|
|
364
|
+
heading.id = reserveHeadingId(baseId);
|
|
365
|
+
}
|
|
366
|
+
if (!heading.title) {
|
|
367
|
+
heading.title = extractTextFromChildren(children);
|
|
368
|
+
}
|
|
369
|
+
return heading;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function createHeadingComponent(level) {
|
|
373
|
+
const tag = `h${level}`;
|
|
374
|
+
const Base = components && components[tag] ? components[tag] : tag;
|
|
375
|
+
return function HeadingComponent(props) {
|
|
376
|
+
const heading = takeHeading(level, props && props.children);
|
|
377
|
+
const id = props && props.id ? props.id : heading && heading.id;
|
|
378
|
+
const finalProps = id ? { ...props, id } : props;
|
|
379
|
+
return React.createElement(Base, finalProps, props && props.children);
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const levelsPresent = Array.from(
|
|
384
|
+
new Set(
|
|
385
|
+
headingQueue
|
|
386
|
+
.map((heading) => (heading ? heading.depth || heading.level : null))
|
|
387
|
+
.filter((level) => typeof level === 'number' && level >= 1 && level <= 6)
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
const headingComponents = levelsPresent.length
|
|
391
|
+
? levelsPresent.reduce((acc, level) => {
|
|
392
|
+
acc[`h${level}`] = createHeadingComponent(level);
|
|
393
|
+
return acc;
|
|
394
|
+
}, {})
|
|
395
|
+
: {};
|
|
274
396
|
const MDXProvider = await getMdxProvider();
|
|
275
397
|
// Base path support for anchors
|
|
276
398
|
const Anchor = function A(props) {
|
|
@@ -294,7 +416,7 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
|
|
|
294
416
|
? React.createElement(PageContext.Provider, { value: contextValue }, withLayout)
|
|
295
417
|
: withLayout;
|
|
296
418
|
const withApp = React.createElement(app.App, null, withContext);
|
|
297
|
-
const compMap = { ...components, a: Anchor };
|
|
419
|
+
const compMap = { ...components, ...headingComponents, a: Anchor };
|
|
298
420
|
const page = MDXProvider
|
|
299
421
|
? React.createElement(MDXProvider, { components: compMap }, withApp)
|
|
300
422
|
: withApp;
|
|
@@ -772,67 +894,21 @@ async function ensureReactGlobals() {
|
|
|
772
894
|
});
|
|
773
895
|
}
|
|
774
896
|
|
|
775
|
-
// Bundle a small runtime to hydrate <Hero /> placeholders from featured items
|
|
776
897
|
async function ensureHeroRuntime() {
|
|
777
898
|
let esbuild = null;
|
|
778
|
-
try {
|
|
779
|
-
|
|
780
|
-
|
|
899
|
+
try {
|
|
900
|
+
esbuild = require("../ui/node_modules/esbuild");
|
|
901
|
+
} catch (_) {
|
|
902
|
+
try { esbuild = require("esbuild"); } catch (_) {}
|
|
903
|
+
}
|
|
904
|
+
if (!esbuild) throw new Error('Hero slider runtime bundling requires esbuild. Install dependencies before building.');
|
|
781
905
|
ensureDirSync(OUT_DIR);
|
|
782
906
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
783
907
|
ensureDirSync(scriptsDir);
|
|
784
|
-
const outFile = path.join(scriptsDir, 'canopy-hero.js');
|
|
785
|
-
const
|
|
786
|
-
import { Hero } from '@canopy-iiif/app/ui';
|
|
787
|
-
function ready(fn){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }
|
|
788
|
-
function parseProps(el){ try{ const s=el.querySelector('script[type="application/json"]'); if(s) return JSON.parse(s.textContent||'{}'); }catch(_){ } return {}; }
|
|
789
|
-
function rootBase(){ try { var bp=(window&&window.CANOPY_BASE_PATH)?String(window.CANOPY_BASE_PATH):''; return bp && bp.endsWith('/') ? bp.slice(0,-1) : bp; } catch(_){ return ''; } }
|
|
790
|
-
async function getApiVersion(){ try{ const u=rootBase() + '/api/index.json'; const res=await fetch(u).catch(()=>null); const j=res&&res.ok?await res.json().catch(()=>null):null; return (j && j.version) || ''; }catch(_){ return ''; } }
|
|
791
|
-
async function loadFeatured(){ try { const v = await getApiVersion(); const q = v ? ('?v='+encodeURIComponent(v)) : ''; const res = await fetch(rootBase() + '/api/featured.json' + q).catch(()=>null); const j = res && res.ok ? await res.json().catch(()=>[]) : []; return Array.isArray(j) ? j : (j && j.items) || []; } catch(_){ return []; } }
|
|
792
|
-
function mount(el, rec){ try{ const React=(window&&window.React)||null; const RDC=(window&&window.ReactDOMClient)||null; const createRoot = RDC && RDC.createRoot; if(!React||!createRoot) return; const props = parseProps(el) || {}; const height = props.height || 360; const node = React.createElement(Hero, { height, item: rec }); const root=createRoot(el); root.render(node); } catch(_){} }
|
|
793
|
-
ready(async function(){ const hosts = Array.from(document.querySelectorAll('[data-canopy-hero]')); if(!hosts.length) return; const featured = await loadFeatured(); if(!featured.length) return; hosts.forEach((el, i) => { try { const p = parseProps(el) || {}; let idx = 0; if (p && typeof p.index === 'number') idx = Math.max(0, Math.min(featured.length-1, Math.floor(p.index))); else if (p && (p.random===true || p.random==='true')) idx = Math.floor(Math.random() * featured.length); const rec = featured[idx] || featured[0]; if (rec) mount(el, rec); } catch(_){} }); });
|
|
794
|
-
`;
|
|
795
|
-
const reactShim = `
|
|
796
|
-
const React = (typeof window !== 'undefined' && window.React) || {};
|
|
797
|
-
export default React;
|
|
798
|
-
export const Children = React.Children;
|
|
799
|
-
export const Component = React.Component;
|
|
800
|
-
export const Fragment = React.Fragment;
|
|
801
|
-
export const createElement = React.createElement;
|
|
802
|
-
export const cloneElement = React.cloneElement;
|
|
803
|
-
export const createContext = React.createContext;
|
|
804
|
-
export const forwardRef = React.forwardRef;
|
|
805
|
-
export const memo = React.memo;
|
|
806
|
-
export const startTransition = React.startTransition;
|
|
807
|
-
export const isValidElement = React.isValidElement;
|
|
808
|
-
export const useEffect = React.useEffect;
|
|
809
|
-
export const useLayoutEffect = React.useLayoutEffect;
|
|
810
|
-
export const useMemo = React.useMemo;
|
|
811
|
-
export const useState = React.useState;
|
|
812
|
-
export const useRef = React.useRef;
|
|
813
|
-
export const useCallback = React.useCallback;
|
|
814
|
-
export const useContext = React.useContext;
|
|
815
|
-
export const useReducer = React.useReducer;
|
|
816
|
-
export const useId = React.useId;
|
|
817
|
-
`;
|
|
818
|
-
const rdomClientShim = `
|
|
819
|
-
const RDC = (typeof window !== 'undefined' && window.ReactDOMClient) || {};
|
|
820
|
-
export default RDC;
|
|
821
|
-
export const createRoot = RDC.createRoot;
|
|
822
|
-
export const hydrateRoot = RDC.hydrateRoot;
|
|
823
|
-
`;
|
|
824
|
-
const plugin = {
|
|
825
|
-
name: 'canopy-react-shims-hero',
|
|
826
|
-
setup(build) {
|
|
827
|
-
const ns = 'canopy-shim';
|
|
828
|
-
build.onResolve({ filter: /^react$/ }, () => ({ path: 'react', namespace: ns }));
|
|
829
|
-
build.onResolve({ filter: /^react-dom\/client$/ }, () => ({ path: 'react-dom-client', namespace: ns }));
|
|
830
|
-
build.onLoad({ filter: /^react$/, namespace: ns }, () => ({ contents: reactShim, loader: 'js' }));
|
|
831
|
-
build.onLoad({ filter: /^react-dom-client$/, namespace: ns }, () => ({ contents: rdomClientShim, loader: 'js' }));
|
|
832
|
-
}
|
|
833
|
-
};
|
|
908
|
+
const outFile = path.join(scriptsDir, 'canopy-hero-slider.js');
|
|
909
|
+
const entryFile = path.join(__dirname, '..', 'components', 'hero-slider-runtime.js');
|
|
834
910
|
await esbuild.build({
|
|
835
|
-
|
|
911
|
+
entryPoints: [entryFile],
|
|
836
912
|
outfile: outFile,
|
|
837
913
|
platform: 'browser',
|
|
838
914
|
format: 'iife',
|
|
@@ -841,12 +917,23 @@ async function ensureHeroRuntime() {
|
|
|
841
917
|
target: ['es2018'],
|
|
842
918
|
logLevel: 'silent',
|
|
843
919
|
minify: true,
|
|
844
|
-
plugins: [plugin],
|
|
845
920
|
});
|
|
921
|
+
try {
|
|
922
|
+
const { logLine } = require('./log');
|
|
923
|
+
let size = 0;
|
|
924
|
+
try {
|
|
925
|
+
const st = fs.statSync(outFile);
|
|
926
|
+
size = (st && st.size) || 0;
|
|
927
|
+
} catch (_) {}
|
|
928
|
+
const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : '';
|
|
929
|
+
const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
|
|
930
|
+
logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
|
|
931
|
+
} catch (_) {}
|
|
846
932
|
}
|
|
847
933
|
|
|
848
934
|
module.exports = {
|
|
849
935
|
extractTitle,
|
|
936
|
+
extractHeadings,
|
|
850
937
|
isReservedFile,
|
|
851
938
|
parseFrontmatter,
|
|
852
939
|
compileMdxFile,
|
package/lib/build/pages.js
CHANGED
|
@@ -50,6 +50,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
50
50
|
const pageInfo = navigation.getPageInfo(normalizedRel);
|
|
51
51
|
const navData = navigation.buildNavigationForFile(normalizedRel);
|
|
52
52
|
const mergedProps = { ...(extraProps || {}) };
|
|
53
|
+
const headings = mdx.extractHeadings(source);
|
|
53
54
|
if (pageInfo) {
|
|
54
55
|
mergedProps.page = mergedProps.page
|
|
55
56
|
? { ...pageInfo, ...mergedProps.page }
|
|
@@ -58,10 +59,16 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
58
59
|
if (navData && !mergedProps.navigation) {
|
|
59
60
|
mergedProps.navigation = navData;
|
|
60
61
|
}
|
|
62
|
+
if (headings && headings.length) {
|
|
63
|
+
mergedProps.page = mergedProps.page
|
|
64
|
+
? { ...mergedProps.page, headings }
|
|
65
|
+
: { headings };
|
|
66
|
+
}
|
|
61
67
|
const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, mergedProps);
|
|
62
68
|
const needsHydrateViewer =
|
|
63
69
|
body.includes('data-canopy-viewer') || body.includes('data-canopy-scroll');
|
|
64
70
|
const needsHydrateSlider = body.includes('data-canopy-slider');
|
|
71
|
+
const needsHeroSlider = body.includes('data-canopy-hero-slider');
|
|
65
72
|
const needsSearchForm = true; // search form runtime is global
|
|
66
73
|
const needsFacets = body.includes('data-canopy-related-items');
|
|
67
74
|
const viewerRel = needsHydrateViewer
|
|
@@ -70,6 +77,9 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
70
77
|
const sliderRel = (needsHydrateSlider || needsFacets)
|
|
71
78
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-slider.js')).split(path.sep).join('/')
|
|
72
79
|
: null;
|
|
80
|
+
const heroRel = needsHeroSlider
|
|
81
|
+
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-hero-slider.js')).split(path.sep).join('/')
|
|
82
|
+
: null;
|
|
73
83
|
const facetsRel = needsFacets
|
|
74
84
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
|
|
75
85
|
: null;
|
|
@@ -81,7 +91,8 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
81
91
|
searchFormRel = rel;
|
|
82
92
|
}
|
|
83
93
|
let jsRel = null;
|
|
84
|
-
if (
|
|
94
|
+
if (needsHeroSlider && heroRel) jsRel = heroRel;
|
|
95
|
+
else if (needsFacets && sliderRel) jsRel = sliderRel;
|
|
85
96
|
else if (viewerRel) jsRel = viewerRel;
|
|
86
97
|
else if (sliderRel) jsRel = sliderRel;
|
|
87
98
|
else if (facetsRel) jsRel = facetsRel;
|
|
@@ -102,6 +113,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
102
113
|
} catch (_) {}
|
|
103
114
|
let headExtra = head;
|
|
104
115
|
const extraScripts = [];
|
|
116
|
+
if (heroRel && jsRel !== heroRel) extraScripts.push(`<script defer src="${heroRel}"></script>`);
|
|
105
117
|
if (facetsRel && jsRel !== facetsRel) extraScripts.push(`<script defer src="${facetsRel}"></script>`);
|
|
106
118
|
if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
107
119
|
if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
package/lib/build/runtimes.js
CHANGED
|
@@ -5,11 +5,7 @@ async function prepareAllRuntimes() {
|
|
|
5
5
|
const mdx = require('./mdx');
|
|
6
6
|
try { await mdx.ensureClientRuntime(); } catch (_) {}
|
|
7
7
|
try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
|
|
8
|
-
try {
|
|
9
|
-
if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
|
|
10
|
-
if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
|
|
11
|
-
}
|
|
12
|
-
} catch (_) {}
|
|
8
|
+
try { if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime(); } catch (_) {}
|
|
13
9
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
14
10
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
15
11
|
await prepareSearchFormRuntime();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Swiper from 'swiper';
|
|
2
|
+
import { Navigation, Pagination, Autoplay } from 'swiper/modules';
|
|
3
|
+
import 'swiper/css';
|
|
4
|
+
import 'swiper/css/navigation';
|
|
5
|
+
import 'swiper/css/pagination';
|
|
6
|
+
|
|
7
|
+
function ready(fn) {
|
|
8
|
+
if (typeof document === 'undefined') return;
|
|
9
|
+
if (document.readyState === 'loading') {
|
|
10
|
+
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
11
|
+
} else {
|
|
12
|
+
fn();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function initSlider(host) {
|
|
17
|
+
if (!host || host.__canopyHeroBound) return;
|
|
18
|
+
const slider = host.querySelector('.canopy-interstitial__slider');
|
|
19
|
+
if (!slider) return;
|
|
20
|
+
const prev = host.querySelector('.canopy-interstitial__nav-btn--prev');
|
|
21
|
+
const next = host.querySelector('.canopy-interstitial__nav-btn--next');
|
|
22
|
+
const pagination = host.querySelector('.canopy-interstitial__pagination');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const swiperInstance = new Swiper(slider, {
|
|
26
|
+
modules: [Navigation, Pagination, Autoplay],
|
|
27
|
+
loop: true,
|
|
28
|
+
slidesPerView: 1,
|
|
29
|
+
navigation: {
|
|
30
|
+
prevEl: prev || undefined,
|
|
31
|
+
nextEl: next || undefined,
|
|
32
|
+
},
|
|
33
|
+
pagination: {
|
|
34
|
+
el: pagination || undefined,
|
|
35
|
+
clickable: true,
|
|
36
|
+
},
|
|
37
|
+
autoplay: {
|
|
38
|
+
delay: 6000,
|
|
39
|
+
disableOnInteraction: false,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
host.__canopyHeroBound = true;
|
|
43
|
+
host.__canopyHeroSwiper = swiperInstance;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
try {
|
|
46
|
+
console.warn('[canopy][hero] failed to initialise slider', error);
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function observeHosts() {
|
|
52
|
+
try {
|
|
53
|
+
const observer = new MutationObserver((mutations) => {
|
|
54
|
+
mutations.forEach((mutation) => {
|
|
55
|
+
mutation.addedNodes &&
|
|
56
|
+
mutation.addedNodes.forEach((node) => {
|
|
57
|
+
if (!(node instanceof Element)) return;
|
|
58
|
+
if (node.matches && node.matches('[data-canopy-hero-slider]')) initSlider(node);
|
|
59
|
+
const inner = node.querySelectorAll
|
|
60
|
+
? node.querySelectorAll('[data-canopy-hero-slider]')
|
|
61
|
+
: [];
|
|
62
|
+
inner && inner.forEach && inner.forEach((el) => initSlider(el));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
observer.observe(document.documentElement || document.body, {
|
|
67
|
+
childList: true,
|
|
68
|
+
subtree: true,
|
|
69
|
+
});
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ready(() => {
|
|
74
|
+
if (typeof document === 'undefined') return;
|
|
75
|
+
const hosts = document.querySelectorAll('[data-canopy-hero-slider]');
|
|
76
|
+
hosts.forEach((host) => initSlider(host));
|
|
77
|
+
observeHosts();
|
|
78
|
+
});
|
package/lib/orchestrator.js
CHANGED
|
@@ -128,26 +128,6 @@ function attachSignalHandlers() {
|
|
|
128
128
|
process.on('exit', clean);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
function verifyBuildOutput(outDir = 'site') {
|
|
132
|
-
const root = path.resolve(outDir);
|
|
133
|
-
function walk(dir) {
|
|
134
|
-
let count = 0;
|
|
135
|
-
if (!fs.existsSync(dir)) return 0;
|
|
136
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
const p = path.join(dir, entry.name);
|
|
139
|
-
if (entry.isDirectory()) count += walk(p);
|
|
140
|
-
else if (entry.isFile() && p.toLowerCase().endsWith('.html')) count += 1;
|
|
141
|
-
}
|
|
142
|
-
return count;
|
|
143
|
-
}
|
|
144
|
-
const pages = walk(root);
|
|
145
|
-
if (!pages) {
|
|
146
|
-
throw new Error('CI check failed: no HTML pages generated in "site/".');
|
|
147
|
-
}
|
|
148
|
-
log(`CI check: found ${pages} HTML page(s) in ${root}.`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
131
|
async function orchestrate(options = {}) {
|
|
152
132
|
const argv = options.argv || process.argv.slice(2);
|
|
153
133
|
const env = options.env || process.env;
|
|
@@ -156,12 +136,6 @@ async function orchestrate(options = {}) {
|
|
|
156
136
|
const mode = getMode(argv, env);
|
|
157
137
|
log(`Mode: ${mode}`);
|
|
158
138
|
|
|
159
|
-
const cli = new Set(argv);
|
|
160
|
-
if (cli.has('--verify')) {
|
|
161
|
-
verifyBuildOutput(env.CANOPY_OUT_DIR || 'site');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
139
|
await prepareUi(mode, env);
|
|
166
140
|
|
|
167
141
|
const api = loadLibraryApi();
|
|
@@ -176,9 +150,6 @@ async function orchestrate(options = {}) {
|
|
|
176
150
|
await api.build();
|
|
177
151
|
}
|
|
178
152
|
log('Build complete');
|
|
179
|
-
if (env.CANOPY_VERIFY === '1' || env.CANOPY_VERIFY === 'true') {
|
|
180
|
-
verifyBuildOutput(env.CANOPY_OUT_DIR || 'site');
|
|
181
|
-
}
|
|
182
153
|
}
|
|
183
154
|
} finally {
|
|
184
155
|
if (uiWatcherChild && !uiWatcherChild.killed) {
|
|
@@ -189,7 +160,6 @@ async function orchestrate(options = {}) {
|
|
|
189
160
|
|
|
190
161
|
module.exports = {
|
|
191
162
|
orchestrate,
|
|
192
|
-
verifyBuildOutput,
|
|
193
163
|
_internals: {
|
|
194
164
|
getMode,
|
|
195
165
|
prepareUi,
|
package/package.json
CHANGED
package/types/orchestrator.d.ts
CHANGED