@canopy-iiif/app 0.7.13 → 0.7.16
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 +64 -0
- package/lib/build/dev.js +251 -354
- package/lib/build/iiif.js +20 -43
- package/lib/build/mdx.js +37 -74
- package/lib/build/runtimes.js +40 -85
- package/lib/build/search-index.js +6 -5
- package/lib/build/search.js +4 -3
- package/lib/build/styles.js +9 -15
- package/lib/common.js +20 -0
- package/lib/components/featured.js +2 -1
- package/lib/search/command-runtime.js +370 -0
- package/lib/search/search-app.jsx +46 -2
- package/lib/search/search.js +7 -192
- package/package.json +8 -2
- package/ui/dist/index.mjs +116 -72
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +89 -38
- package/ui/dist/server.mjs.map +4 -4
- package/ui/tailwind-canopy-iiif-plugin.js +7 -12
- package/ui/tailwind-canopy-iiif-preset.js +15 -0
package/lib/build/iiif.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
CONTENT_DIR,
|
|
12
12
|
ensureDirSync,
|
|
13
13
|
htmlShell,
|
|
14
|
+
rootRelativeHref,
|
|
14
15
|
} = require("../common");
|
|
15
16
|
const mdx = require("./mdx");
|
|
16
17
|
const { log, logLine, logResponse } = require("./log");
|
|
@@ -21,7 +22,7 @@ const IIIF_CACHE_COLLECTIONS_DIR = path.join(IIIF_CACHE_DIR, "collections");
|
|
|
21
22
|
const IIIF_CACHE_COLLECTION = path.join(IIIF_CACHE_DIR, "collection.json");
|
|
22
23
|
// Primary global index location
|
|
23
24
|
const IIIF_CACHE_INDEX = path.join(IIIF_CACHE_DIR, "index.json");
|
|
24
|
-
//
|
|
25
|
+
// Additional legacy locations kept for backward compatibility (read + optional write)
|
|
25
26
|
const IIIF_CACHE_INDEX_LEGACY = path.join(
|
|
26
27
|
IIIF_CACHE_DIR,
|
|
27
28
|
"manifest-index.json"
|
|
@@ -149,7 +150,7 @@ async function loadManifestIndex() {
|
|
|
149
150
|
return { byId, collection: idx.collection || null };
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
//
|
|
153
|
+
// Legacy index location retained for backward compatibility
|
|
153
154
|
if (fs.existsSync(IIIF_CACHE_INDEX_LEGACY)) {
|
|
154
155
|
const idx = await readJson(IIIF_CACHE_INDEX_LEGACY);
|
|
155
156
|
if (idx && typeof idx === "object") {
|
|
@@ -166,7 +167,7 @@ async function loadManifestIndex() {
|
|
|
166
167
|
return { byId, collection: idx.collection || null };
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
|
-
//
|
|
170
|
+
// Legacy manifests index retained for backward compatibility
|
|
170
171
|
if (fs.existsSync(IIIF_CACHE_INDEX_MANIFESTS)) {
|
|
171
172
|
const idx = await readJson(IIIF_CACHE_INDEX_MANIFESTS);
|
|
172
173
|
if (idx && typeof idx === "object") {
|
|
@@ -405,8 +406,9 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
405
406
|
}
|
|
406
407
|
} catch (_) {}
|
|
407
408
|
}
|
|
408
|
-
} catch (
|
|
409
|
-
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const message = err && err.message ? err.message : err;
|
|
411
|
+
throw new Error(`[iiif] Failed to populate featured cache: ${message}`);
|
|
410
412
|
}
|
|
411
413
|
}
|
|
412
414
|
|
|
@@ -642,33 +644,20 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
642
644
|
1200;
|
|
643
645
|
|
|
644
646
|
// Compile the works layout component once per run
|
|
647
|
+
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
648
|
+
if (!fs.existsSync(worksLayoutPath)) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
"IIIF build requires content/works/_layout.mdx. Create the layout instead of relying on generated output."
|
|
651
|
+
);
|
|
652
|
+
}
|
|
645
653
|
let WorksLayoutComp = null;
|
|
646
654
|
try {
|
|
647
|
-
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
648
655
|
WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
|
|
649
|
-
} catch (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
"div",
|
|
655
|
-
{ className: "content" },
|
|
656
|
-
React.createElement("h1", null, title || "Untitled"),
|
|
657
|
-
// Render viewer placeholder for hydration
|
|
658
|
-
React.createElement(
|
|
659
|
-
"div",
|
|
660
|
-
{ "data-canopy-viewer": "1" },
|
|
661
|
-
React.createElement("script", {
|
|
662
|
-
type: "application/json",
|
|
663
|
-
dangerouslySetInnerHTML: {
|
|
664
|
-
__html: JSON.stringify({
|
|
665
|
-
iiifContent: manifest && (manifest.id || ""),
|
|
666
|
-
}),
|
|
667
|
-
},
|
|
668
|
-
})
|
|
669
|
-
)
|
|
670
|
-
);
|
|
671
|
-
};
|
|
656
|
+
} catch (err) {
|
|
657
|
+
const message = err && err.message ? err.message : err;
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Failed to compile content/works/_layout.mdx: ${message}`
|
|
660
|
+
);
|
|
672
661
|
}
|
|
673
662
|
|
|
674
663
|
for (let ci = 0; ci < chunks; ci++) {
|
|
@@ -807,20 +796,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
807
796
|
href = withBase(href);
|
|
808
797
|
return React.createElement("a", { href, ...rest }, props.children);
|
|
809
798
|
};
|
|
810
|
-
// Map exported UI components into MDX
|
|
799
|
+
// Map exported UI components into MDX and add anchor helper
|
|
811
800
|
const compMap = { ...components, a: Anchor };
|
|
812
|
-
if (!compMap.SearchPanel && compMap.CommandPalette) {
|
|
813
|
-
compMap.SearchPanel = compMap.CommandPalette;
|
|
814
|
-
}
|
|
815
|
-
if (!components.HelloWorld) {
|
|
816
|
-
components.HelloWorld = components.Fallback
|
|
817
|
-
? (props) =>
|
|
818
|
-
React.createElement(components.Fallback, {
|
|
819
|
-
name: "HelloWorld",
|
|
820
|
-
...props,
|
|
821
|
-
})
|
|
822
|
-
: () => null;
|
|
823
|
-
}
|
|
824
801
|
let MDXProvider = null;
|
|
825
802
|
try {
|
|
826
803
|
const mod = await import("@mdx-js/react");
|
|
@@ -1009,7 +986,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1009
986
|
iiifRecords.push({
|
|
1010
987
|
id: String(manifest.id || id),
|
|
1011
988
|
title,
|
|
1012
|
-
href: href.split(path.sep).join("/"),
|
|
989
|
+
href: rootRelativeHref(href.split(path.sep).join("/")),
|
|
1013
990
|
type: "work",
|
|
1014
991
|
thumbnail: thumbUrl || undefined,
|
|
1015
992
|
thumbnailWidth:
|
package/lib/build/mdx.js
CHANGED
|
@@ -101,7 +101,7 @@ async function loadUiComponents() {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
if (!mod) {
|
|
104
|
-
// Try package subpath as a secondary resolution path
|
|
104
|
+
// Try package subpath as a secondary resolution path to avoid export-map issues
|
|
105
105
|
try {
|
|
106
106
|
mod = await import('@canopy-iiif/app/ui/server');
|
|
107
107
|
} catch (e2) {
|
|
@@ -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', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems'];
|
|
115
|
+
const required = ['SearchPanel', 'CommandPalette', '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(', '));
|
|
@@ -128,41 +128,6 @@ async function loadUiComponents() {
|
|
|
128
128
|
Slider: !!comp.Slider,
|
|
129
129
|
}); } catch(_){}
|
|
130
130
|
}
|
|
131
|
-
// No stub injection beyond this point; UI package must supply these.
|
|
132
|
-
// Ensure a minimal SSR Hero exists
|
|
133
|
-
if (!comp.Hero) {
|
|
134
|
-
comp.Hero = function SimpleHero({ height = 360, item, className = '', style = {}, ...rest }){
|
|
135
|
-
const h = typeof height === 'number' ? `${height}px` : String(height || '').trim() || '360px';
|
|
136
|
-
const base = { position: 'relative', width: '100%', height: h, overflow: 'hidden', backgroundColor: 'var(--color-gray-muted)', ...style };
|
|
137
|
-
const title = (item && item.title) || '';
|
|
138
|
-
const href = (item && item.href) || '#';
|
|
139
|
-
const thumbnail = (item && item.thumbnail) || '';
|
|
140
|
-
return React.createElement('div', { className: ['canopy-hero', className].filter(Boolean).join(' '), style: base, ...rest },
|
|
141
|
-
thumbnail ? React.createElement('img', { src: thumbnail, alt: '', 'aria-hidden': 'true', style: { position:'absolute', inset:0, width:'100%', height:'100%', objectFit:'cover', objectPosition:'center', filter:'none' } }) : null,
|
|
142
|
-
React.createElement('div', { className:'canopy-hero-overlay', style: { position:'absolute', left:0, right:0, bottom:0, padding:'1rem', background:'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 55%, rgba(0,0,0,0.65) 100%)', color:'white' } },
|
|
143
|
-
React.createElement('h3', { style: { margin:0, fontSize:'1.5rem', fontWeight:600, lineHeight:1.2, textShadow:'0 1px 3px rgba(0,0,0,0.6)' } },
|
|
144
|
-
React.createElement('a', { href, style:{ color:'inherit', textDecoration:'none' }, className:'canopy-hero-link' }, title)
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
);
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
// Provide a minimal SSR FeaturedHero fallback if missing
|
|
151
|
-
if (!comp.FeaturedHero) {
|
|
152
|
-
try {
|
|
153
|
-
const helpers = require('../components/featured');
|
|
154
|
-
comp.FeaturedHero = function FeaturedHero(props) {
|
|
155
|
-
try {
|
|
156
|
-
const list = helpers && helpers.readFeaturedFromCacheSync ? helpers.readFeaturedFromCacheSync() : [];
|
|
157
|
-
if (!Array.isArray(list) || list.length === 0) return null;
|
|
158
|
-
const index = (props && typeof props.index === 'number') ? Math.max(0, Math.min(list.length - 1, Math.floor(props.index))) : null;
|
|
159
|
-
const pick = (index != null) ? index : ((props && (props.random === true || props.random === 'true')) ? Math.floor(Math.random() * list.length) : 0);
|
|
160
|
-
const item = list[pick] || list[0];
|
|
161
|
-
return React.createElement(comp.Hero, { ...props, item });
|
|
162
|
-
} catch (_) { return null; }
|
|
163
|
-
};
|
|
164
|
-
} catch (_) { /* ignore */ }
|
|
165
|
-
}
|
|
166
131
|
UI_COMPONENTS = comp;
|
|
167
132
|
UI_COMPONENTS_PATH = currentPath;
|
|
168
133
|
UI_COMPONENTS_MTIME = currentMtime;
|
|
@@ -265,26 +230,9 @@ async function loadAppWrapper() {
|
|
|
265
230
|
ok = false;
|
|
266
231
|
}
|
|
267
232
|
if (!ok) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
// Fallback to pass-through wrapper to avoid blocking builds
|
|
273
|
-
if (!App) {
|
|
274
|
-
App = function PassThrough(props) {
|
|
275
|
-
return React.createElement(React.Fragment, null, props.children);
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
require("./log").log(
|
|
280
|
-
"! Warning: content/_app.mdx did not clearly render {children}; proceeding with best-effort wrapper\n",
|
|
281
|
-
"yellow"
|
|
282
|
-
);
|
|
283
|
-
} catch (_) {
|
|
284
|
-
console.warn(
|
|
285
|
-
"Warning: content/_app.mdx did not clearly render {children}; proceeding."
|
|
286
|
-
);
|
|
287
|
-
}
|
|
233
|
+
throw new Error(
|
|
234
|
+
"content/_app.mdx must render {children}. Update the layout so downstream pages receive their content."
|
|
235
|
+
);
|
|
288
236
|
}
|
|
289
237
|
APP_WRAPPER = { App, Head };
|
|
290
238
|
return APP_WRAPPER;
|
|
@@ -390,7 +338,7 @@ async function ensureClientRuntime() {
|
|
|
390
338
|
esbuild = require("esbuild");
|
|
391
339
|
} catch (_) {}
|
|
392
340
|
}
|
|
393
|
-
if (!esbuild)
|
|
341
|
+
if (!esbuild) throw new Error('Viewer runtime bundling requires esbuild. Install dependencies before building.');
|
|
394
342
|
ensureDirSync(OUT_DIR);
|
|
395
343
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
396
344
|
ensureDirSync(scriptsDir);
|
|
@@ -509,6 +457,9 @@ async function ensureClientRuntime() {
|
|
|
509
457
|
async function ensureFacetsRuntime() {
|
|
510
458
|
let esbuild = null;
|
|
511
459
|
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
460
|
+
if (!esbuild) {
|
|
461
|
+
throw new Error('RelatedItems runtime bundling requires esbuild. Install dependencies before building.');
|
|
462
|
+
}
|
|
512
463
|
ensureDirSync(OUT_DIR);
|
|
513
464
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
514
465
|
ensureDirSync(scriptsDir);
|
|
@@ -589,18 +540,30 @@ async function ensureFacetsRuntime() {
|
|
|
589
540
|
});
|
|
590
541
|
`;
|
|
591
542
|
const shim = { name: 'facets-vanilla', setup(){} };
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
543
|
+
try {
|
|
544
|
+
await esbuild.build({
|
|
545
|
+
stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-facets-entry.js', loader: 'js' },
|
|
546
|
+
outfile: outFile,
|
|
547
|
+
platform: 'browser',
|
|
548
|
+
format: 'iife',
|
|
549
|
+
bundle: true,
|
|
550
|
+
sourcemap: false,
|
|
551
|
+
target: ['es2018'],
|
|
552
|
+
logLevel: 'silent',
|
|
553
|
+
minify: true,
|
|
554
|
+
plugins: [shim]
|
|
555
|
+
});
|
|
556
|
+
} catch (e) {
|
|
557
|
+
const message = e && e.message ? e.message : e;
|
|
558
|
+
throw new Error(`RelatedItems runtime build failed: ${message}`);
|
|
603
559
|
}
|
|
560
|
+
try {
|
|
561
|
+
const { logLine } = require('./log');
|
|
562
|
+
let size = 0; try { const st = fs.statSync(outFile); size = st && st.size || 0; } catch (_) {}
|
|
563
|
+
const kb = size ? ` (${(size/1024).toFixed(1)} KB)` : '';
|
|
564
|
+
const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
|
|
565
|
+
logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
|
|
566
|
+
} catch (_) {}
|
|
604
567
|
}
|
|
605
568
|
|
|
606
569
|
// Bundle a separate client runtime for the Clover Slider to keep payloads split.
|
|
@@ -611,7 +574,7 @@ async function ensureSliderRuntime() {
|
|
|
611
574
|
} catch (_) {
|
|
612
575
|
try { esbuild = require("esbuild"); } catch (_) {}
|
|
613
576
|
}
|
|
614
|
-
if (!esbuild)
|
|
577
|
+
if (!esbuild) throw new Error('Slider runtime bundling requires esbuild. Install dependencies before building.');
|
|
615
578
|
ensureDirSync(OUT_DIR);
|
|
616
579
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
617
580
|
ensureDirSync(scriptsDir);
|
|
@@ -735,8 +698,8 @@ async function ensureSliderRuntime() {
|
|
|
735
698
|
plugins: [plugin],
|
|
736
699
|
});
|
|
737
700
|
} catch (e) {
|
|
738
|
-
|
|
739
|
-
|
|
701
|
+
const message = e && e.message ? e.message : e;
|
|
702
|
+
throw new Error(`Slider runtime build failed: ${message}`);
|
|
740
703
|
}
|
|
741
704
|
try {
|
|
742
705
|
const { logLine } = require('./log');
|
|
@@ -757,7 +720,7 @@ async function ensureReactGlobals() {
|
|
|
757
720
|
esbuild = require("esbuild");
|
|
758
721
|
} catch (_) {}
|
|
759
722
|
}
|
|
760
|
-
if (!esbuild)
|
|
723
|
+
if (!esbuild) throw new Error('React globals bundling requires esbuild. Install dependencies before building.');
|
|
761
724
|
const { path } = require("../common");
|
|
762
725
|
ensureDirSync(OUT_DIR);
|
|
763
726
|
const scriptsDir = path.join(OUT_DIR, "scripts");
|
|
@@ -792,7 +755,7 @@ async function ensureReactGlobals() {
|
|
|
792
755
|
async function ensureHeroRuntime() {
|
|
793
756
|
let esbuild = null;
|
|
794
757
|
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
795
|
-
if (!esbuild)
|
|
758
|
+
if (!esbuild) throw new Error('Hero runtime bundling requires esbuild. Install dependencies before building.');
|
|
796
759
|
const { path } = require("../common");
|
|
797
760
|
ensureDirSync(OUT_DIR);
|
|
798
761
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
package/lib/build/runtimes.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
1
|
const { logLine } = require('./log');
|
|
3
|
-
const { fs,
|
|
2
|
+
const { fs, path, OUT_DIR, ensureDirSync } = require('../common');
|
|
4
3
|
|
|
5
4
|
async function prepareAllRuntimes() {
|
|
6
5
|
const mdx = require('./mdx');
|
|
7
6
|
try { await mdx.ensureClientRuntime(); } catch (_) {}
|
|
8
7
|
try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
|
|
9
|
-
// Optional: Hero runtime is SSR-only by default; enable explicitly to avoid bundling Node deps in browser
|
|
10
8
|
try {
|
|
11
9
|
if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
|
|
12
10
|
if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
|
|
@@ -14,103 +12,60 @@ async function prepareAllRuntimes() {
|
|
|
14
12
|
} catch (_) {}
|
|
15
13
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
16
14
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
17
|
-
|
|
15
|
+
await prepareCommandRuntime();
|
|
18
16
|
try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
async function resolveEsbuild() {
|
|
20
|
+
try { return require('../../ui/node_modules/esbuild'); } catch (_) {
|
|
21
|
+
try { return require('esbuild'); } catch (_) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
async function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Do not inject legacy trigger buttons or inputs
|
|
50
|
-
var records = await loadRecords(); function render(items){ list.innerHTML=''; if(!items.length){ panel.style.display='none'; return; } var groups=new Map(); items.forEach(function(r){ var t=String(r.type||'page'); if(!groups.has(t)) groups.set(t, []); groups.get(t).push(r); }); function gl(t){ if(t==='work') return 'Works'; if(t==='page') return 'Pages'; return t.charAt(0).toUpperCase()+t.slice(1);} var ordered=[].concat(groupOrder.filter(function(t){return groups.has(t);})).concat(Array.from(groups.keys()).filter(function(t){return groupOrder.indexOf(t)===-1;})); ordered.forEach(function(t){ var hdr=document.createElement('div'); hdr.textContent=gl(t); hdr.style.cssText='padding:6px 12px;font-weight:600;color:#374151'; list.appendChild(hdr); groups.get(t).forEach(function(r){ var it=document.createElement('div'); it.setAttribute('data-canopy-item',''); it.tabIndex=0; it.style.cssText='display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;outline:none;'; var thumb=(String(r.type||'')==='work' && r.thumbnail)?r.thumbnail:''; if(thumb){ var img=document.createElement('img'); img.src=thumb; img.alt=''; img.style.cssText='width:40px;height:40px;object-fit:cover;border-radius:4px'; it.appendChild(img);} var span=document.createElement('span'); span.textContent=r.title||r.href; it.appendChild(span); it.onmouseenter=function(){ it.style.background='#f8fafc'; }; it.onmouseleave=function(){ it.style.background='transparent'; }; it.onfocus=function(){ it.style.background='#eef2ff'; try{ it.scrollIntoView({ block: 'nearest' }); }catch(_){} }; it.onblur=function(){ it.style.background='transparent'; }; it.onclick=function(){ try{ window.location.href = withBase(String(r.href||'')); }catch(_){} panel.style.display='none'; }; list.appendChild(it); }); }); }
|
|
51
|
-
function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
|
|
52
|
-
function filterAndShow(q){ try{ var qq=norm(q); if(!qq){ try{ panel.style.display='block'; list.innerHTML=''; }catch(_){} return; } var out=[]; for(var i=0;i<records.length;i++){ var r=records[i]; var title=String((r&&r.title)||''); if(!title) continue; if(norm(title).indexOf(qq)!==-1) out.push(r); if(out.length>=maxResults) break; } render(out); }catch(_){} }
|
|
53
|
-
input.addEventListener('input', function(){ if(onSearchPage){ try{ var ev = new CustomEvent('canopy:search:setQuery', { detail: { query: (input.value||'') } }); window.dispatchEvent(ev); }catch(_){ } return; } filterAndShow(input.value||''); });
|
|
54
|
-
// Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
|
|
55
|
-
input.addEventListener('keydown', function(e){
|
|
56
|
-
if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
|
|
57
|
-
else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
|
|
58
|
-
});
|
|
59
|
-
list.addEventListener('keydown', function(e){
|
|
60
|
-
var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
|
|
61
|
-
if(!cur) return;
|
|
62
|
-
if(e.key==='ArrowDown'){
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
|
|
65
|
-
} else if(e.key==='ArrowUp'){
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
try{ var arr2=getItems(); var i2=arr2.indexOf(cur); if(i2<=0){ input && input.focus && input.focus(); } else { var prv=arr2[i2-1]; (prv||cur).focus(); } }catch(_){}
|
|
68
|
-
} else if(e.key==='Enter'){
|
|
69
|
-
e.preventDefault(); try{ cur.click(); }catch(_){}
|
|
70
|
-
} else if(e.key==='Escape'){
|
|
71
|
-
panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
|
|
75
|
-
document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
|
|
76
|
-
// Hotkey support (e.g., mod+k)
|
|
77
|
-
document.addEventListener('keydown', function(e){
|
|
78
|
-
try {
|
|
79
|
-
var want = String((cfg && cfg.hotkey) || '').toLowerCase();
|
|
80
|
-
if (!want) return;
|
|
81
|
-
var isMod = e.metaKey || e.ctrlKey;
|
|
82
|
-
if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
|
|
83
|
-
e.preventDefault();
|
|
84
|
-
if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
|
|
85
|
-
panel.style.display='block';
|
|
86
|
-
(input && input.focus && input.focus());
|
|
87
|
-
filterAndShow(input && input.value || '');
|
|
88
|
-
}
|
|
89
|
-
} catch(_) { }
|
|
90
|
-
});
|
|
91
|
-
function openPanel(){ if(onSearchPage){ try{ var ev3 = new CustomEvent('canopy:search:setQuery', { detail: {} }); window.dispatchEvent(ev3); }catch(_){ } return; } panel.style.display='block'; (input && input.focus && input.focus()); filterAndShow(input && input.value || ''); }
|
|
92
|
-
host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
|
|
93
|
-
try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
|
|
94
|
-
});
|
|
95
|
-
})();
|
|
96
|
-
`;
|
|
97
|
-
await fsp.writeFile(cmdOut, fallback, 'utf8');
|
|
98
|
-
try { logLine(`✓ Wrote ${path.relative(process.cwd(), cmdOut)} (fallback)`, 'cyan'); } catch (_) {}
|
|
27
|
+
async function prepareCommandRuntime() {
|
|
28
|
+
const esbuild = await resolveEsbuild();
|
|
29
|
+
if (!esbuild) throw new Error('Command runtime bundling requires esbuild. Install dependencies before building.');
|
|
30
|
+
ensureDirSync(OUT_DIR);
|
|
31
|
+
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
32
|
+
ensureDirSync(scriptsDir);
|
|
33
|
+
const entry = path.join(__dirname, '..', 'search', 'command-runtime.js');
|
|
34
|
+
const outFile = path.join(scriptsDir, 'canopy-command.js');
|
|
35
|
+
await esbuild.build({
|
|
36
|
+
entryPoints: [entry],
|
|
37
|
+
outfile: outFile,
|
|
38
|
+
platform: 'browser',
|
|
39
|
+
format: 'iife',
|
|
40
|
+
bundle: true,
|
|
41
|
+
sourcemap: false,
|
|
42
|
+
target: ['es2018'],
|
|
43
|
+
logLevel: 'silent',
|
|
44
|
+
minify: true,
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
let size = 0;
|
|
48
|
+
try { const st = fs.statSync(outFile); size = st.size || 0; } catch (_) {}
|
|
49
|
+
const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : '';
|
|
50
|
+
const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
|
|
51
|
+
logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
|
|
52
|
+
} catch (_) {}
|
|
99
53
|
}
|
|
100
54
|
|
|
101
55
|
async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
|
|
102
56
|
const search = require('../search/search');
|
|
103
|
-
try { logLine(`• Writing search runtime${label ? ' ('+label+')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
|
|
57
|
+
try { logLine(`• Writing search runtime${label ? ' (' + label + ')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
|
|
58
|
+
|
|
104
59
|
let timedOut = false;
|
|
105
60
|
await Promise.race([
|
|
106
61
|
search.ensureSearchRuntime(),
|
|
107
62
|
new Promise((_, reject) => setTimeout(() => { timedOut = true; reject(new Error('timeout')); }, Number(timeoutMs)))
|
|
108
63
|
]).catch(() => {
|
|
109
|
-
try { console.warn(`Search: Bundling runtime timed out${label ? ' ('+label+')' : ''}, skipping`); } catch (_) {}
|
|
64
|
+
try { console.warn(`Search: Bundling runtime timed out${label ? ' (' + label + ')' : ''}, skipping`); } catch (_) {}
|
|
110
65
|
});
|
|
111
66
|
if (timedOut) {
|
|
112
|
-
try { logLine(`! Search runtime not bundled${label ? ' ('+label+')' : ''}\n`, 'yellow'); } catch (_) {}
|
|
67
|
+
try { logLine(`! Search runtime not bundled${label ? ' (' + label + ')' : ''}\n`, 'yellow'); } catch (_) {}
|
|
113
68
|
}
|
|
114
69
|
}
|
|
115
70
|
|
|
116
|
-
module.exports = { prepareAllRuntimes,
|
|
71
|
+
module.exports = { prepareAllRuntimes, prepareCommandRuntime, prepareSearchRuntime };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
const { logLine } = require('./log');
|
|
3
|
+
const { rootRelativeHref } = require('../common');
|
|
3
4
|
|
|
4
5
|
function pagesToRecords(pageRecords) {
|
|
5
6
|
const list = Array.isArray(pageRecords) ? pageRecords : [];
|
|
@@ -7,7 +8,7 @@ function pagesToRecords(pageRecords) {
|
|
|
7
8
|
.filter((p) => p && p.href && p.searchInclude)
|
|
8
9
|
.map((p) => ({
|
|
9
10
|
title: p.title || p.href,
|
|
10
|
-
href: p.href,
|
|
11
|
+
href: rootRelativeHref(p.href),
|
|
11
12
|
type: p.searchType || 'page',
|
|
12
13
|
}));
|
|
13
14
|
}
|
|
@@ -18,11 +19,11 @@ function maybeMockRecords() {
|
|
|
18
19
|
const svg = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><rect width="400" height="300" fill="#dbeafe"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="24" fill="#1d4ed8">Mock</text></svg>');
|
|
19
20
|
const thumb = `data:image/svg+xml;charset=utf-8,${svg}`;
|
|
20
21
|
for (let i = 1; i <= 120; i++) {
|
|
21
|
-
mock.push({ title: `Mock Work #${i}`, href: `works/mock-${i}.html
|
|
22
|
+
mock.push({ title: `Mock Work #${i}`, href: rootRelativeHref(`works/mock-${i}.html`), type: 'work', thumbnail: thumb });
|
|
22
23
|
}
|
|
23
|
-
mock.push({ title: 'Mock Doc A', href: 'getting-started/index.html', type: 'docs' });
|
|
24
|
-
mock.push({ title: 'Mock Doc B', href: 'getting-started/example.html', type: 'docs' });
|
|
25
|
-
mock.push({ title: 'Mock Page', href: 'index.html', type: 'page' });
|
|
24
|
+
mock.push({ title: 'Mock Doc A', href: rootRelativeHref('getting-started/index.html'), type: 'docs' });
|
|
25
|
+
mock.push({ title: 'Mock Doc B', href: rootRelativeHref('getting-started/example.html'), type: 'docs' });
|
|
26
|
+
mock.push({ title: 'Mock Page', href: rootRelativeHref('index.html'), type: 'page' });
|
|
26
27
|
return mock;
|
|
27
28
|
}
|
|
28
29
|
|
package/lib/build/search.js
CHANGED
|
@@ -23,7 +23,8 @@ async function buildFacetsForWorks(combined, labelWhitelist) {
|
|
|
23
23
|
const rec = combined[i];
|
|
24
24
|
if (!rec || String(rec.type) !== 'work') continue;
|
|
25
25
|
const href = String(rec.href || '');
|
|
26
|
-
const
|
|
26
|
+
const normalizedHref = href.replace(/^\/+/, '');
|
|
27
|
+
const m = normalizedHref.match(/^works\/(.+)\.html$/i);
|
|
27
28
|
if (!m) continue;
|
|
28
29
|
const slug = m[1];
|
|
29
30
|
const p = path.resolve('.cache/iiif/manifests', slug + '.json');
|
|
@@ -169,7 +170,7 @@ module.exports = { buildFacetsForWorks, writeFacetCollections, writeFacetsSearch
|
|
|
169
170
|
|
|
170
171
|
|
|
171
172
|
async function collectMdxPageRecords() {
|
|
172
|
-
const { fs, fsp, path, CONTENT_DIR } = require('../common');
|
|
173
|
+
const { fs, fsp, path, CONTENT_DIR, rootRelativeHref } = require('../common');
|
|
173
174
|
const mdx = require('./mdx');
|
|
174
175
|
const pagesHelpers = require('./pages');
|
|
175
176
|
const pages = [];
|
|
@@ -185,7 +186,7 @@ async function collectMdxPageRecords() {
|
|
|
185
186
|
const title = mdx.extractTitle(src);
|
|
186
187
|
const rel = path.relative(CONTENT_DIR, p).replace(/\.mdx$/i, '.html');
|
|
187
188
|
if (base !== 'sitemap.mdx') {
|
|
188
|
-
const href = rel.split(path.sep).join('/');
|
|
189
|
+
const href = rootRelativeHref(rel.split(path.sep).join('/'));
|
|
189
190
|
const underSearch = /^search\//i.test(href) || href.toLowerCase() === 'search.html';
|
|
190
191
|
let include = !underSearch;
|
|
191
192
|
let resolvedType = null;
|
package/lib/build/styles.js
CHANGED
|
@@ -70,21 +70,15 @@ async function ensureStyles() {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function resolveTailwindCli() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
|
|
83
|
-
);
|
|
84
|
-
if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
|
|
85
|
-
} catch (_) {}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
73
|
+
const localBin = path.join(
|
|
74
|
+
process.cwd(),
|
|
75
|
+
"node_modules",
|
|
76
|
+
".bin",
|
|
77
|
+
process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
|
|
78
|
+
);
|
|
79
|
+
if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
|
|
80
|
+
return { cmd: 'tailwindcss', args: [] };
|
|
81
|
+
}
|
|
88
82
|
function buildTailwindCli({ input, output, config, minify = true }) {
|
|
89
83
|
try {
|
|
90
84
|
const cli = resolveTailwindCli();
|
package/lib/common.js
CHANGED
|
@@ -60,6 +60,25 @@ function withBase(href) {
|
|
|
60
60
|
return href;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function rootRelativeHref(href) {
|
|
64
|
+
try {
|
|
65
|
+
let raw = href == null ? '' : String(href);
|
|
66
|
+
raw = raw.trim();
|
|
67
|
+
if (!raw) return '/';
|
|
68
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw)) return raw;
|
|
69
|
+
if (raw.startsWith('//')) return raw;
|
|
70
|
+
if (raw.startsWith('#') || raw.startsWith('?')) return raw;
|
|
71
|
+
let cleaned = raw;
|
|
72
|
+
if (cleaned.startsWith('/')) cleaned = cleaned.replace(/^\/+/, '');
|
|
73
|
+
while (cleaned.startsWith('./')) cleaned = cleaned.slice(2);
|
|
74
|
+
while (cleaned.startsWith('../')) cleaned = cleaned.slice(3);
|
|
75
|
+
if (!cleaned) return '/';
|
|
76
|
+
return '/' + cleaned;
|
|
77
|
+
} catch (_) {
|
|
78
|
+
return href;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
// Convert a site-relative path (e.g., "/api/foo.json") to an absolute URL.
|
|
64
83
|
// Handles either:
|
|
65
84
|
// - BASE_ORIGIN that may already include a path prefix (e.g., https://host/org/repo)
|
|
@@ -121,4 +140,5 @@ module.exports = {
|
|
|
121
140
|
BASE_ORIGIN,
|
|
122
141
|
absoluteUrl,
|
|
123
142
|
applyBaseToHtml,
|
|
143
|
+
rootRelativeHref,
|
|
124
144
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const yaml = require('js-yaml');
|
|
4
|
+
const { rootRelativeHref } = require('../common');
|
|
4
5
|
|
|
5
6
|
function firstLabelString(label) {
|
|
6
7
|
if (!label) return 'Untitled';
|
|
@@ -101,7 +102,7 @@ function readFeaturedFromCacheSync() {
|
|
|
101
102
|
if (!m) continue;
|
|
102
103
|
const rec = {
|
|
103
104
|
title: firstLabelString(m && m.label),
|
|
104
|
-
href: path.join('works', slug + '.html').split(path.sep).join('/'),
|
|
105
|
+
href: rootRelativeHref(path.join('works', slug + '.html').split(path.sep).join('/')),
|
|
105
106
|
type: 'work',
|
|
106
107
|
};
|
|
107
108
|
if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);
|