@canopy-iiif/app 0.8.2 → 0.8.3
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 +1 -1
- package/lib/build/build.js +8 -0
- package/lib/build/dev.js +49 -18
- package/lib/build/iiif.js +124 -43
- package/lib/build/mdx.js +2 -2
- package/lib/build/pages.js +8 -8
- package/lib/build/runtimes.js +6 -6
- package/lib/build/search.js +7 -10
- package/lib/build/verify.js +9 -9
- package/lib/components/featured.js +17 -3
- package/lib/search/search-app.jsx +2 -2
- package/lib/search/{command-runtime.js → search-form-runtime.js} +9 -9
- package/lib/search/search.js +2 -2
- package/package.json +1 -1
- package/ui/dist/index.mjs +72 -58
- package/ui/dist/index.mjs.map +3 -3
- package/ui/dist/server.mjs +54 -40
- package/ui/dist/server.mjs.map +3 -3
- package/ui/styles/base/_common.scss +19 -6
- package/ui/styles/base/_heading.scss +17 -0
- package/ui/styles/base/index.scss +2 -1
- package/ui/styles/components/header/_header.scss +13 -0
- package/ui/styles/components/header/_logo.scss +20 -0
- package/ui/styles/components/header/_navbar.scss +15 -0
- package/ui/styles/components/header/index.scss +3 -0
- package/ui/styles/components/index.scss +4 -4
- package/ui/styles/components/search/_filters.scss +265 -0
- package/ui/styles/components/search/_form.scss +171 -0
- package/ui/styles/components/search/_results.scss +121 -0
- package/ui/styles/components/search/index.scss +3 -0
- package/ui/styles/index.css +480 -77
- package/ui/styles/variables.scss +15 -5
- package/ui/styles/components/_command.scss +0 -164
- package/ui/styles/components/_header.scss +0 -0
package/lib/AGENTS.md
CHANGED
|
@@ -56,7 +56,7 @@ Logbook Template
|
|
|
56
56
|
Logbook
|
|
57
57
|
-------
|
|
58
58
|
- 2025-09-26 / chatgpt: Hardened runtime bundlers to throw when esbuild or source compilation fails and required `content/works/_layout.mdx`; build now aborts instead of silently writing placeholder assets.
|
|
59
|
-
- 2025-09-26 / chatgpt: Replaced the legacy command runtime stub with an esbuild-bundled runtime (`search/
|
|
59
|
+
- 2025-09-26 / chatgpt: Replaced the legacy command runtime stub with an esbuild-bundled runtime (`search/search-form-runtime.js`); `prepareSearchFormRuntime()` now builds `site/scripts/canopy-search-form.js` and fails if esbuild is missing.
|
|
60
60
|
- 2025-09-27 / chatgpt: Documented Tailwind token flow in `app/styles/tailwind.config.js`, compiled UI Sass variables during config load, and exposed `stylesheetHref`/`Stylesheet` helpers via `@canopy-iiif/app/head` so `_app.mdx` can reference the generated CSS directly.
|
|
61
61
|
- 2025-09-27 / chatgpt: Expanded search indexing to harvest MDX pages (respecting frontmatter/layout types), injected BASE_PATH hydration data into search.html, and reworked `mdx.extractTitle()` so generated records surface real headings instead of `Untitled`.
|
|
62
62
|
|
package/lib/build/build.js
CHANGED
|
@@ -62,6 +62,14 @@ async function build(options = {}) {
|
|
|
62
62
|
if (!skipIiif) {
|
|
63
63
|
const results = await iiif.buildIiifCollectionPages(CONFIG);
|
|
64
64
|
iiifRecords = results?.iiifRecords;
|
|
65
|
+
iiifRecordsCache = Array.isArray(iiifRecords) ? iiifRecords : [];
|
|
66
|
+
} else {
|
|
67
|
+
iiifRecords = Array.isArray(iiifRecordsCache) ? iiifRecordsCache : [];
|
|
68
|
+
logLine(
|
|
69
|
+
`• Reusing cached IIIF search records (${iiifRecords.length})`,
|
|
70
|
+
"blue",
|
|
71
|
+
{ dim: true }
|
|
72
|
+
);
|
|
65
73
|
}
|
|
66
74
|
// Ensure any configured featured manifests are cached (and thumbnails computed)
|
|
67
75
|
// so SSR components like <Hero /> can resolve items even if they are not part of
|
package/lib/build/dev.js
CHANGED
|
@@ -969,17 +969,16 @@ async function dev() {
|
|
|
969
969
|
child = startTailwindWatcher();
|
|
970
970
|
|
|
971
971
|
const uiPlugin = path.join(
|
|
972
|
-
|
|
973
|
-
"../ui",
|
|
972
|
+
APP_UI_DIR,
|
|
974
973
|
"tailwind-canopy-iiif-plugin.js"
|
|
975
974
|
);
|
|
976
975
|
const uiPreset = path.join(
|
|
977
|
-
|
|
978
|
-
"../ui",
|
|
976
|
+
APP_UI_DIR,
|
|
979
977
|
"tailwind-canopy-iiif-preset.js"
|
|
980
978
|
);
|
|
981
|
-
const uiStylesDir = path.join(
|
|
982
|
-
const
|
|
979
|
+
const uiStylesDir = path.join(APP_UI_DIR, "styles");
|
|
980
|
+
const uiStylesCss = path.join(uiStylesDir, "index.css");
|
|
981
|
+
const pluginFiles = [uiPlugin, uiPreset].filter((p) => {
|
|
983
982
|
try {
|
|
984
983
|
return fs.existsSync(p);
|
|
985
984
|
} catch (_) {
|
|
@@ -987,25 +986,54 @@ async function dev() {
|
|
|
987
986
|
}
|
|
988
987
|
});
|
|
989
988
|
let restartTimer = null;
|
|
990
|
-
|
|
989
|
+
let uiCssWatcherAttached = false;
|
|
990
|
+
const scheduleTailwindRestart = (message, compileLabel) => {
|
|
991
991
|
clearTimeout(restartTimer);
|
|
992
992
|
restartTimer = setTimeout(() => {
|
|
993
|
-
console.log(
|
|
994
|
-
"[tailwind] detected UI plugin/preset change — restarting Tailwind"
|
|
995
|
-
);
|
|
993
|
+
if (message) console.log(message);
|
|
996
994
|
try {
|
|
997
995
|
if (child && !child.killed) child.kill();
|
|
998
996
|
} catch (_) {}
|
|
999
|
-
safeCompile(
|
|
997
|
+
safeCompile(compileLabel);
|
|
1000
998
|
child = startTailwindWatcher();
|
|
1001
999
|
try { onBuildStart(); } catch (_) {}
|
|
1002
|
-
},
|
|
1000
|
+
}, 120);
|
|
1003
1001
|
};
|
|
1004
|
-
for (const f of
|
|
1002
|
+
for (const f of pluginFiles) {
|
|
1005
1003
|
try {
|
|
1006
|
-
fs.watch(f, { persistent: false },
|
|
1004
|
+
fs.watch(f, { persistent: false }, () => {
|
|
1005
|
+
scheduleTailwindRestart(
|
|
1006
|
+
"[tailwind] detected UI plugin/preset change — restarting Tailwind",
|
|
1007
|
+
"[tailwind] compile after plugin change failed"
|
|
1008
|
+
);
|
|
1009
|
+
});
|
|
1007
1010
|
} catch (_) {}
|
|
1008
1011
|
}
|
|
1012
|
+
const attachCssWatcher = () => {
|
|
1013
|
+
if (uiCssWatcherAttached) {
|
|
1014
|
+
if (fs.existsSync(uiStylesCss)) return;
|
|
1015
|
+
uiCssWatcherAttached = false;
|
|
1016
|
+
}
|
|
1017
|
+
if (!fs.existsSync(uiStylesCss)) return;
|
|
1018
|
+
const handler = () =>
|
|
1019
|
+
scheduleTailwindRestart(
|
|
1020
|
+
"[tailwind] detected @canopy-iiif/app/ui styles change — restarting Tailwind",
|
|
1021
|
+
"[tailwind] compile after UI styles change failed"
|
|
1022
|
+
);
|
|
1023
|
+
try {
|
|
1024
|
+
const watcher = fs.watch(uiStylesCss, { persistent: false }, handler);
|
|
1025
|
+
uiCssWatcherAttached = true;
|
|
1026
|
+
watcher.on("close", () => {
|
|
1027
|
+
uiCssWatcherAttached = false;
|
|
1028
|
+
});
|
|
1029
|
+
} catch (_) {
|
|
1030
|
+
try {
|
|
1031
|
+
fs.watchFile(uiStylesCss, { interval: 250 }, handler);
|
|
1032
|
+
uiCssWatcherAttached = true;
|
|
1033
|
+
} catch (_) {}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
attachCssWatcher();
|
|
1009
1037
|
if (fs.existsSync(uiStylesDir)) {
|
|
1010
1038
|
try {
|
|
1011
1039
|
fs.watch(
|
|
@@ -1013,7 +1041,7 @@ async function dev() {
|
|
|
1013
1041
|
{ persistent: false, recursive: true },
|
|
1014
1042
|
(evt, fn) => {
|
|
1015
1043
|
try {
|
|
1016
|
-
if (fn && /\.s[ac]ss$/i.test(String(fn)))
|
|
1044
|
+
if (fn && /\.s[ac]ss$/i.test(String(fn))) attachCssWatcher();
|
|
1017
1045
|
} catch (_) {}
|
|
1018
1046
|
}
|
|
1019
1047
|
);
|
|
@@ -1027,8 +1055,9 @@ async function dev() {
|
|
|
1027
1055
|
{ persistent: false },
|
|
1028
1056
|
(evt, fn) => {
|
|
1029
1057
|
try {
|
|
1030
|
-
if (fn && /\.s[ac]ss$/i.test(String(fn)))
|
|
1058
|
+
if (fn && /\.s[ac]ss$/i.test(String(fn))) attachCssWatcher();
|
|
1031
1059
|
} catch (_) {}
|
|
1060
|
+
scan(dir);
|
|
1032
1061
|
}
|
|
1033
1062
|
);
|
|
1034
1063
|
watchers.set(dir, w);
|
|
@@ -1055,8 +1084,10 @@ async function dev() {
|
|
|
1055
1084
|
if (fs.existsSync(configPath)) {
|
|
1056
1085
|
try {
|
|
1057
1086
|
fs.watch(configPath, { persistent: false }, () => {
|
|
1058
|
-
|
|
1059
|
-
|
|
1087
|
+
scheduleTailwindRestart(
|
|
1088
|
+
"[tailwind] tailwind.config change — restarting Tailwind",
|
|
1089
|
+
"[tailwind] compile after config change failed"
|
|
1090
|
+
);
|
|
1060
1091
|
});
|
|
1061
1092
|
} catch (_) {}
|
|
1062
1093
|
}
|
package/lib/build/iiif.js
CHANGED
|
@@ -32,6 +32,34 @@ const IIIF_CACHE_INDEX_MANIFESTS = path.join(
|
|
|
32
32
|
"manifest-index.json"
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
const DEFAULT_THUMBNAIL_SIZE = 400;
|
|
36
|
+
const DEFAULT_CHUNK_SIZE = 20;
|
|
37
|
+
const DEFAULT_FETCH_CONCURRENCY = 5;
|
|
38
|
+
|
|
39
|
+
function resolvePositiveInteger(value, fallback) {
|
|
40
|
+
const num = Number(value);
|
|
41
|
+
if (Number.isFinite(num) && num > 0) return Math.max(1, Math.floor(num));
|
|
42
|
+
return Math.max(1, Math.floor(fallback));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveBoolean(value) {
|
|
46
|
+
if (typeof value === "boolean") return value;
|
|
47
|
+
if (value === undefined || value === null) return false;
|
|
48
|
+
const normalized = String(value).trim().toLowerCase();
|
|
49
|
+
if (!normalized) return false;
|
|
50
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveThumbnailPreferences() {
|
|
54
|
+
return {
|
|
55
|
+
size: resolvePositiveInteger(
|
|
56
|
+
process.env.CANOPY_THUMBNAIL_SIZE,
|
|
57
|
+
DEFAULT_THUMBNAIL_SIZE
|
|
58
|
+
),
|
|
59
|
+
unsafe: resolveBoolean(process.env.CANOPY_THUMBNAILS_UNSAFE),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
function firstLabelString(label) {
|
|
36
64
|
if (!label) return "Untitled";
|
|
37
65
|
if (typeof label === "string") return label;
|
|
@@ -373,10 +401,9 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
373
401
|
const CONFIG = cfg || (await loadConfig());
|
|
374
402
|
const featured = Array.isArray(CONFIG && CONFIG.featured) ? CONFIG.featured : [];
|
|
375
403
|
if (!featured.length) return;
|
|
376
|
-
const { getThumbnail } = require("../iiif/thumbnail");
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const unsafeThumbs = !!(CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && (CONFIG.iiif.thumbnails.unsafe === true || CONFIG.iiif.thumbnails.unsafe === 'true'));
|
|
404
|
+
const { getThumbnail, getRepresentativeImage } = require("../iiif/thumbnail");
|
|
405
|
+
const { size: thumbSize, unsafe: unsafeThumbs } = resolveThumbnailPreferences();
|
|
406
|
+
const HERO_THUMBNAIL_SIZE = 1200;
|
|
380
407
|
for (const rawId of featured) {
|
|
381
408
|
const id = normalizeIiifId(String(rawId || ''));
|
|
382
409
|
if (!id) continue;
|
|
@@ -392,18 +419,88 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
392
419
|
// Ensure thumbnail fields exist in index for this manifest (if computable)
|
|
393
420
|
try {
|
|
394
421
|
const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
422
|
+
const idx = await loadManifestIndex();
|
|
423
|
+
if (!Array.isArray(idx.byId)) continue;
|
|
424
|
+
const entry = idx.byId.find(
|
|
425
|
+
(e) =>
|
|
426
|
+
e &&
|
|
427
|
+
e.type === 'Manifest' &&
|
|
428
|
+
normalizeIiifId(String(e.id)) === normalizeIiifId(String(manifest.id))
|
|
429
|
+
);
|
|
430
|
+
if (!entry) continue;
|
|
431
|
+
|
|
432
|
+
let touched = false;
|
|
395
433
|
if (t && t.url) {
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
434
|
+
const nextUrl = String(t.url);
|
|
435
|
+
if (entry.thumbnail !== nextUrl) {
|
|
436
|
+
entry.thumbnail = nextUrl;
|
|
437
|
+
touched = true;
|
|
438
|
+
}
|
|
439
|
+
if (typeof t.width === 'number') {
|
|
440
|
+
if (entry.thumbnailWidth !== t.width) touched = true;
|
|
441
|
+
entry.thumbnailWidth = t.width;
|
|
442
|
+
}
|
|
443
|
+
if (typeof t.height === 'number') {
|
|
444
|
+
if (entry.thumbnailHeight !== t.height) touched = true;
|
|
445
|
+
entry.thumbnailHeight = t.height;
|
|
405
446
|
}
|
|
406
447
|
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const heroSource = (() => {
|
|
451
|
+
if (manifest && manifest.thumbnail) {
|
|
452
|
+
const clone = { ...manifest };
|
|
453
|
+
try {
|
|
454
|
+
delete clone.thumbnail;
|
|
455
|
+
} catch (_) {
|
|
456
|
+
clone.thumbnail = undefined;
|
|
457
|
+
}
|
|
458
|
+
return clone;
|
|
459
|
+
}
|
|
460
|
+
return manifest;
|
|
461
|
+
})();
|
|
462
|
+
const heroRep = await getRepresentativeImage(
|
|
463
|
+
heroSource || manifest,
|
|
464
|
+
HERO_THUMBNAIL_SIZE,
|
|
465
|
+
true
|
|
466
|
+
);
|
|
467
|
+
if (heroRep && heroRep.id) {
|
|
468
|
+
const nextHero = String(heroRep.id);
|
|
469
|
+
if (entry.heroThumbnail !== nextHero) {
|
|
470
|
+
entry.heroThumbnail = nextHero;
|
|
471
|
+
touched = true;
|
|
472
|
+
}
|
|
473
|
+
if (typeof heroRep.width === 'number') {
|
|
474
|
+
if (entry.heroThumbnailWidth !== heroRep.width) touched = true;
|
|
475
|
+
entry.heroThumbnailWidth = heroRep.width;
|
|
476
|
+
} else if (entry.heroThumbnailWidth !== undefined) {
|
|
477
|
+
delete entry.heroThumbnailWidth;
|
|
478
|
+
touched = true;
|
|
479
|
+
}
|
|
480
|
+
if (typeof heroRep.height === 'number') {
|
|
481
|
+
if (entry.heroThumbnailHeight !== heroRep.height) touched = true;
|
|
482
|
+
entry.heroThumbnailHeight = heroRep.height;
|
|
483
|
+
} else if (entry.heroThumbnailHeight !== undefined) {
|
|
484
|
+
delete entry.heroThumbnailHeight;
|
|
485
|
+
touched = true;
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
if (entry.heroThumbnail !== undefined) {
|
|
489
|
+
delete entry.heroThumbnail;
|
|
490
|
+
touched = true;
|
|
491
|
+
}
|
|
492
|
+
if (entry.heroThumbnailWidth !== undefined) {
|
|
493
|
+
delete entry.heroThumbnailWidth;
|
|
494
|
+
touched = true;
|
|
495
|
+
}
|
|
496
|
+
if (entry.heroThumbnailHeight !== undefined) {
|
|
497
|
+
delete entry.heroThumbnailHeight;
|
|
498
|
+
touched = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (_) {}
|
|
502
|
+
|
|
503
|
+
if (touched) await saveManifestIndex(idx);
|
|
407
504
|
} catch (_) {}
|
|
408
505
|
}
|
|
409
506
|
} catch (err) {
|
|
@@ -613,11 +710,9 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
613
710
|
if (!tasks.length) return { searchRecords: [] };
|
|
614
711
|
|
|
615
712
|
// Split into chunks and process with limited concurrency
|
|
616
|
-
const chunkSize =
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
process.env.CANOPY_CHUNK_SIZE || (cfg.iiif && cfg.iiif.chunkSize) || 20
|
|
620
|
-
)
|
|
713
|
+
const chunkSize = resolvePositiveInteger(
|
|
714
|
+
process.env.CANOPY_CHUNK_SIZE,
|
|
715
|
+
DEFAULT_CHUNK_SIZE
|
|
621
716
|
);
|
|
622
717
|
const chunks = Math.ceil(tasks.length / chunkSize);
|
|
623
718
|
// Summary before processing chunks
|
|
@@ -630,18 +725,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
630
725
|
);
|
|
631
726
|
} catch (_) {}
|
|
632
727
|
const iiifRecords = [];
|
|
633
|
-
const unsafeThumbs =
|
|
634
|
-
|
|
635
|
-
cfg.iiif &&
|
|
636
|
-
cfg.iiif.thumbnails &&
|
|
637
|
-
cfg.iiif.thumbnails.unsafe === true
|
|
638
|
-
);
|
|
639
|
-
const thumbSize =
|
|
640
|
-
(cfg &&
|
|
641
|
-
cfg.iiif &&
|
|
642
|
-
cfg.iiif.thumbnails &&
|
|
643
|
-
cfg.iiif.thumbnails.preferredSize) ||
|
|
644
|
-
1200;
|
|
728
|
+
const { size: thumbSize, unsafe: unsafeThumbs } =
|
|
729
|
+
resolveThumbnailPreferences();
|
|
645
730
|
|
|
646
731
|
// Compile the works layout component once per run
|
|
647
732
|
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
@@ -664,13 +749,9 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
664
749
|
const chunk = tasks.slice(ci * chunkSize, (ci + 1) * chunkSize);
|
|
665
750
|
logLine(`• Chunk ${ci + 1}/${chunks}`, "blue", { dim: true });
|
|
666
751
|
|
|
667
|
-
const concurrency =
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
process.env.CANOPY_FETCH_CONCURRENCY ||
|
|
671
|
-
(cfg.iiif && cfg.iiif.concurrency) ||
|
|
672
|
-
6
|
|
673
|
-
)
|
|
752
|
+
const concurrency = resolvePositiveInteger(
|
|
753
|
+
process.env.CANOPY_FETCH_CONCURRENCY,
|
|
754
|
+
DEFAULT_FETCH_CONCURRENCY
|
|
674
755
|
);
|
|
675
756
|
let next = 0;
|
|
676
757
|
const logs = new Array(chunk.length);
|
|
@@ -831,12 +912,12 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
831
912
|
const needsHydrateViewer = body.includes("data-canopy-viewer");
|
|
832
913
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
833
914
|
const needsHero = body.includes("data-canopy-hero");
|
|
834
|
-
const
|
|
915
|
+
const needsSearchForm = body.includes("data-canopy-search-form");
|
|
835
916
|
const needsHydrate =
|
|
836
917
|
body.includes("data-canopy-hydrate") ||
|
|
837
918
|
needsHydrateViewer ||
|
|
838
919
|
needsRelated ||
|
|
839
|
-
|
|
920
|
+
needsSearchForm;
|
|
840
921
|
|
|
841
922
|
const viewerRel = needsHydrateViewer
|
|
842
923
|
? path
|
|
@@ -874,11 +955,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
874
955
|
.split(path.sep)
|
|
875
956
|
.join("/")
|
|
876
957
|
: null;
|
|
877
|
-
const
|
|
958
|
+
const searchFormRel = needsSearchForm
|
|
878
959
|
? path
|
|
879
960
|
.relative(
|
|
880
961
|
path.dirname(outPath),
|
|
881
|
-
path.join(OUT_DIR, "scripts", "canopy-
|
|
962
|
+
path.join(OUT_DIR, "scripts", "canopy-search-form.js")
|
|
882
963
|
)
|
|
883
964
|
.split(path.sep)
|
|
884
965
|
.join("/")
|
|
@@ -919,8 +1000,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
919
1000
|
extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
920
1001
|
if (sliderRel && jsRel !== sliderRel)
|
|
921
1002
|
extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
922
|
-
if (
|
|
923
|
-
extraScripts.push(`<script defer src="${
|
|
1003
|
+
if (searchFormRel && jsRel !== searchFormRel)
|
|
1004
|
+
extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
|
|
924
1005
|
if (extraScripts.length)
|
|
925
1006
|
headExtra = extraScripts.join("") + headExtra;
|
|
926
1007
|
try {
|
package/lib/build/mdx.js
CHANGED
|
@@ -112,7 +112,7 @@ async function loadUiComponents() {
|
|
|
112
112
|
}
|
|
113
113
|
let comp = (mod && typeof mod === 'object') ? mod : {};
|
|
114
114
|
// Hard-require core exports; do not inject fallbacks
|
|
115
|
-
const required = ['SearchPanel', '
|
|
115
|
+
const required = ['SearchPanel', 'SearchFormModal', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Hero', 'FeaturedHero'];
|
|
116
116
|
const missing = required.filter((k) => !comp || !comp[k]);
|
|
117
117
|
if (missing.length) {
|
|
118
118
|
throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
|
|
@@ -123,7 +123,7 @@ async function loadUiComponents() {
|
|
|
123
123
|
mtime: currentMtime,
|
|
124
124
|
hasServerExport: !!mod,
|
|
125
125
|
hasWorkspace: typeof comp !== 'undefined',
|
|
126
|
-
|
|
126
|
+
SearchFormModal: !!comp.SearchFormModal,
|
|
127
127
|
Viewer: !!comp.Viewer,
|
|
128
128
|
Slider: !!comp.Slider,
|
|
129
129
|
}); } catch(_){}
|
package/lib/build/pages.js
CHANGED
|
@@ -47,7 +47,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
47
47
|
const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, extraProps);
|
|
48
48
|
const needsHydrateViewer = body.includes('data-canopy-viewer');
|
|
49
49
|
const needsHydrateSlider = body.includes('data-canopy-slider');
|
|
50
|
-
const
|
|
50
|
+
const needsSearchForm = true; // search form runtime is global
|
|
51
51
|
const needsFacets = body.includes('data-canopy-related-items');
|
|
52
52
|
const viewerRel = needsHydrateViewer
|
|
53
53
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-viewer.js')).split(path.sep).join('/')
|
|
@@ -58,12 +58,12 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
58
58
|
const facetsRel = needsFacets
|
|
59
59
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
|
|
60
60
|
: null;
|
|
61
|
-
let
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
let rel = path.relative(path.dirname(outPath),
|
|
65
|
-
try { const st = fs.statSync(
|
|
66
|
-
|
|
61
|
+
let searchFormRel = null;
|
|
62
|
+
if (needsSearchForm) {
|
|
63
|
+
const runtimeAbs = path.join(OUT_DIR, 'scripts', 'canopy-search-form.js');
|
|
64
|
+
let rel = path.relative(path.dirname(outPath), runtimeAbs).split(path.sep).join('/');
|
|
65
|
+
try { const st = fs.statSync(runtimeAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
|
|
66
|
+
searchFormRel = rel;
|
|
67
67
|
}
|
|
68
68
|
let jsRel = null;
|
|
69
69
|
if (needsFacets && sliderRel) jsRel = sliderRel;
|
|
@@ -90,7 +90,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
90
90
|
if (facetsRel && jsRel !== facetsRel) extraScripts.push(`<script defer src="${facetsRel}"></script>`);
|
|
91
91
|
if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
92
92
|
if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
93
|
-
if (
|
|
93
|
+
if (searchFormRel && jsRel !== searchFormRel) extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
|
|
94
94
|
if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
|
|
95
95
|
const html = htmlShell({ title, body, cssHref: null, scriptHref: jsRel, headExtra: vendorTag + headExtra });
|
|
96
96
|
const { applyBaseToHtml } = require('../common');
|
package/lib/build/runtimes.js
CHANGED
|
@@ -12,7 +12,7 @@ async function prepareAllRuntimes() {
|
|
|
12
12
|
} catch (_) {}
|
|
13
13
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
14
14
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
15
|
-
await
|
|
15
|
+
await prepareSearchFormRuntime();
|
|
16
16
|
try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -24,14 +24,14 @@ async function resolveEsbuild() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async function
|
|
27
|
+
async function prepareSearchFormRuntime() {
|
|
28
28
|
const esbuild = await resolveEsbuild();
|
|
29
|
-
if (!esbuild) throw new Error('
|
|
29
|
+
if (!esbuild) throw new Error('Search form runtime bundling requires esbuild. Install dependencies before building.');
|
|
30
30
|
ensureDirSync(OUT_DIR);
|
|
31
31
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
32
32
|
ensureDirSync(scriptsDir);
|
|
33
|
-
const entry = path.join(__dirname, '..', 'search', '
|
|
34
|
-
const outFile = path.join(scriptsDir, 'canopy-
|
|
33
|
+
const entry = path.join(__dirname, '..', 'search', 'search-form-runtime.js');
|
|
34
|
+
const outFile = path.join(scriptsDir, 'canopy-search-form.js');
|
|
35
35
|
await esbuild.build({
|
|
36
36
|
entryPoints: [entry],
|
|
37
37
|
outfile: outFile,
|
|
@@ -68,4 +68,4 @@ async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
module.exports = { prepareAllRuntimes,
|
|
71
|
+
module.exports = { prepareAllRuntimes, prepareSearchFormRuntime, prepareSearchRuntime };
|
package/lib/build/search.js
CHANGED
|
@@ -188,23 +188,20 @@ async function collectMdxPageRecords() {
|
|
|
188
188
|
let include = !underSearch;
|
|
189
189
|
let resolvedType = null;
|
|
190
190
|
const pageFm = fm && fm.data ? fm.data : null;
|
|
191
|
-
if (pageFm)
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
else include = false;
|
|
196
|
-
} else {
|
|
197
|
-
include = false; // frontmatter present w/o type => exclude per policy
|
|
198
|
-
}
|
|
191
|
+
if (pageFm && pageFm.search === false) include = false;
|
|
192
|
+
if (include && pageFm && Object.prototype.hasOwnProperty.call(pageFm, 'type')) {
|
|
193
|
+
if (pageFm.type) resolvedType = String(pageFm.type);
|
|
194
|
+
else include = false;
|
|
199
195
|
}
|
|
200
196
|
if (include && !resolvedType) {
|
|
201
197
|
const layoutMeta = await pagesHelpers.getNearestDirLayoutMeta(p);
|
|
202
198
|
if (layoutMeta && layoutMeta.type) resolvedType = String(layoutMeta.type);
|
|
203
199
|
}
|
|
204
200
|
if (include && !resolvedType) {
|
|
205
|
-
|
|
201
|
+
resolvedType = 'page';
|
|
206
202
|
}
|
|
207
|
-
|
|
203
|
+
const trimmedType = resolvedType && String(resolvedType).trim();
|
|
204
|
+
pages.push({ title, href, searchInclude: include && !!trimmedType, searchType: trimmedType || undefined });
|
|
208
205
|
}
|
|
209
206
|
}
|
|
210
207
|
}
|
package/lib/build/verify.js
CHANGED
|
@@ -21,10 +21,10 @@ function verifyHomepageElements(outDir) {
|
|
|
21
21
|
const idx = path.join(outDir, 'index.html');
|
|
22
22
|
const html = readFileSafe(idx);
|
|
23
23
|
const okHero = /class=\"[^\"]*canopy-hero/.test(html) || /<div[^>]+canopy-hero/.test(html);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
return { okHero,
|
|
24
|
+
const okSearchForm = /data-canopy-search-form=/.test(html);
|
|
25
|
+
const okSearchFormTrigger = /data-canopy-search-form-trigger/.test(html);
|
|
26
|
+
const okSearchFormScriptRef = /<script[^>]+canopy-search-form\.js/.test(html);
|
|
27
|
+
return { okHero, okSearchForm, okSearchFormTrigger, okSearchFormScriptRef, htmlPath: idx };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function verifyBuildOutput(options = {}) {
|
|
@@ -34,7 +34,7 @@ function verifyBuildOutput(options = {}) {
|
|
|
34
34
|
const okAny = total > 0;
|
|
35
35
|
const indexPath = path.join(outDir, 'index.html');
|
|
36
36
|
const hasIndex = fs.existsSync(indexPath) && fs.statSync(indexPath).size > 0;
|
|
37
|
-
const { okHero,
|
|
37
|
+
const { okHero, okSearchForm, okSearchFormTrigger, okSearchFormScriptRef } = verifyHomepageElements(outDir);
|
|
38
38
|
|
|
39
39
|
const ck = (label, ok, extra) => {
|
|
40
40
|
const status = ok ? '✓' : '✗';
|
|
@@ -44,12 +44,12 @@ function verifyBuildOutput(options = {}) {
|
|
|
44
44
|
ck('HTML pages exist', okAny, okAny ? `(${total})` : '');
|
|
45
45
|
ck('homepage exists', hasIndex, hasIndex ? `(${indexPath})` : '');
|
|
46
46
|
ck('homepage: Hero present', okHero);
|
|
47
|
-
ck('homepage:
|
|
48
|
-
ck('homepage:
|
|
49
|
-
ck('homepage:
|
|
47
|
+
ck('homepage: Search form present', okSearchForm);
|
|
48
|
+
ck('homepage: Search form trigger present', okSearchFormTrigger);
|
|
49
|
+
ck('homepage: Search form script referenced', okSearchFormScriptRef);
|
|
50
50
|
|
|
51
51
|
// Do not fail build on missing SSR trigger; the client runtime injects a default.
|
|
52
|
-
const ok = okAny && hasIndex && okHero &&
|
|
52
|
+
const ok = okAny && hasIndex && okHero && okSearchForm && okSearchFormScriptRef;
|
|
53
53
|
if (!ok) {
|
|
54
54
|
const err = new Error('Build verification failed');
|
|
55
55
|
err.outDir = outDir;
|
|
@@ -105,9 +105,23 @@ function readFeaturedFromCacheSync() {
|
|
|
105
105
|
href: rootRelativeHref(path.join('works', slug + '.html').split(path.sep).join('/')),
|
|
106
106
|
type: 'work',
|
|
107
107
|
};
|
|
108
|
-
if (entry && entry.
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
if (entry && entry.heroThumbnail) {
|
|
109
|
+
rec.thumbnail = String(entry.heroThumbnail);
|
|
110
|
+
if (typeof entry.heroThumbnailWidth === 'number') {
|
|
111
|
+
rec.thumbnailWidth = entry.heroThumbnailWidth;
|
|
112
|
+
} else if (typeof entry.thumbnailWidth === 'number') {
|
|
113
|
+
rec.thumbnailWidth = entry.thumbnailWidth;
|
|
114
|
+
}
|
|
115
|
+
if (typeof entry.heroThumbnailHeight === 'number') {
|
|
116
|
+
rec.thumbnailHeight = entry.heroThumbnailHeight;
|
|
117
|
+
} else if (typeof entry.thumbnailHeight === 'number') {
|
|
118
|
+
rec.thumbnailHeight = entry.thumbnailHeight;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);
|
|
122
|
+
if (entry && typeof entry.thumbnailWidth === 'number') rec.thumbnailWidth = entry.thumbnailWidth;
|
|
123
|
+
if (entry && typeof entry.thumbnailHeight === 'number') rec.thumbnailHeight = entry.thumbnailHeight;
|
|
124
|
+
}
|
|
111
125
|
if (!rec.thumbnail) {
|
|
112
126
|
try {
|
|
113
127
|
const t = m && m.thumbnail;
|
|
@@ -681,7 +681,7 @@ function parseProps(el) {
|
|
|
681
681
|
function bindSearchInputToStore() {
|
|
682
682
|
if (!store || typeof document === "undefined") return;
|
|
683
683
|
try {
|
|
684
|
-
const input = document.querySelector("[data-canopy-
|
|
684
|
+
const input = document.querySelector("[data-canopy-search-form-input]");
|
|
685
685
|
if (!input || input.dataset.canopySearchSync === "1") return;
|
|
686
686
|
input.dataset.canopySearchSync = "1";
|
|
687
687
|
|
|
@@ -764,7 +764,7 @@ if (typeof document !== "undefined") {
|
|
|
764
764
|
const q =
|
|
765
765
|
ev && ev.detail && typeof ev.detail.query === "string"
|
|
766
766
|
? ev.detail.query
|
|
767
|
-
: document.querySelector("[data-canopy-
|
|
767
|
+
: document.querySelector("[data-canopy-search-form-input]")?.value ||
|
|
768
768
|
"";
|
|
769
769
|
if (typeof q === "string") store.setQuery(q);
|
|
770
770
|
} catch (_) {}
|
|
@@ -217,7 +217,7 @@ function bindKeyboardNavigation({ input, list, panel }) {
|
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
async function
|
|
220
|
+
async function attachSearchForm(host) {
|
|
221
221
|
const config = parseProps(host) || {};
|
|
222
222
|
const maxResults = Number(config.maxResults || 8) || 8;
|
|
223
223
|
const groupOrder = Array.isArray(config.groupOrder) ? config.groupOrder : ['work', 'page'];
|
|
@@ -225,14 +225,14 @@ async function attachCommand(host) {
|
|
|
225
225
|
const onSearchPage = isOnSearchPage();
|
|
226
226
|
|
|
227
227
|
const panel = (() => {
|
|
228
|
-
try { return host.querySelector('[data-canopy-
|
|
228
|
+
try { return host.querySelector('[data-canopy-search-form-panel]'); } catch (_) { return null; }
|
|
229
229
|
})();
|
|
230
230
|
if (!panel) return;
|
|
231
231
|
|
|
232
232
|
if (!onSearchPage) {
|
|
233
233
|
try {
|
|
234
234
|
const wrapper = host.querySelector('.relative');
|
|
235
|
-
if (wrapper) wrapper.setAttribute('data-canopy-
|
|
235
|
+
if (wrapper) wrapper.setAttribute('data-canopy-search-form-auto', '1');
|
|
236
236
|
} catch (_) {}
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -244,7 +244,7 @@ async function attachCommand(host) {
|
|
|
244
244
|
if (!list) return;
|
|
245
245
|
|
|
246
246
|
const input = (() => {
|
|
247
|
-
try { return host.querySelector('[data-canopy-
|
|
247
|
+
try { return host.querySelector('[data-canopy-search-form-input]'); } catch (_) { return null; }
|
|
248
248
|
})();
|
|
249
249
|
if (!input) return;
|
|
250
250
|
|
|
@@ -345,9 +345,9 @@ async function attachCommand(host) {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
host.addEventListener('click', (event) => {
|
|
348
|
-
const trigger = event.target && event.target.closest && event.target.closest('[data-canopy-
|
|
348
|
+
const trigger = event.target && event.target.closest && event.target.closest('[data-canopy-search-form-trigger]');
|
|
349
349
|
if (!trigger) return;
|
|
350
|
-
const mode = (trigger.dataset && trigger.dataset.
|
|
350
|
+
const mode = (trigger.dataset && trigger.dataset.canopySearchFormTrigger) || '';
|
|
351
351
|
if (mode === 'submit' || mode === 'form') return;
|
|
352
352
|
event.preventDefault();
|
|
353
353
|
openPanel();
|
|
@@ -359,12 +359,12 @@ async function attachCommand(host) {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
ready(() => {
|
|
362
|
-
const hosts = Array.from(document.querySelectorAll('[data-canopy-
|
|
362
|
+
const hosts = Array.from(document.querySelectorAll('[data-canopy-search-form]'));
|
|
363
363
|
if (!hosts.length) return;
|
|
364
364
|
hosts.forEach((host) => {
|
|
365
|
-
|
|
365
|
+
attachSearchForm(host).catch((err) => {
|
|
366
366
|
try {
|
|
367
|
-
console.warn('[canopy][
|
|
367
|
+
console.warn('[canopy][search-form] failed to initialise', err && (err.message || err));
|
|
368
368
|
} catch (_) {}
|
|
369
369
|
});
|
|
370
370
|
});
|