@canopy-iiif/app 0.7.9 → 0.7.10
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/build/build.js +20 -1
- package/lib/build/iiif.js +68 -2
- package/lib/build/mdx.js +275 -5
- package/lib/build/pages.js +7 -3
- package/lib/build/runtimes.js +62 -8
- package/lib/build/verify.js +60 -0
- package/lib/components/featured.js +144 -0
- package/lib/search/search-app.jsx +34 -3
- package/lib/search/search.js +11 -2
- package/package.json +1 -1
- package/ui/dist/index.mjs +74 -27
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +3109 -21
- package/ui/dist/server.mjs.map +4 -4
package/lib/build/build.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
const { ensureStyles } = require("./styles");
|
|
20
20
|
const { copyAssets } = require("./assets");
|
|
21
21
|
const { logLine } = require("./log");
|
|
22
|
+
const { verifyBuildOutput } = require("./verify");
|
|
22
23
|
|
|
23
24
|
// hold records between builds if skipping IIIF
|
|
24
25
|
let iiifRecordsCache = [];
|
|
@@ -57,11 +58,15 @@ async function build(options = {}) {
|
|
|
57
58
|
underscore: true,
|
|
58
59
|
});
|
|
59
60
|
let iiifRecords = [];
|
|
61
|
+
const CONFIG = await iiif.loadConfig();
|
|
60
62
|
if (!skipIiif) {
|
|
61
|
-
const CONFIG = await iiif.loadConfig();
|
|
62
63
|
const results = await iiif.buildIiifCollectionPages(CONFIG);
|
|
63
64
|
iiifRecords = results?.iiifRecords;
|
|
64
65
|
}
|
|
66
|
+
// Ensure any configured featured manifests are cached (and thumbnails computed)
|
|
67
|
+
// so SSR components like <Hero /> can resolve items even if they are not part of
|
|
68
|
+
// the traversed collection or when IIIF build is skipped during incremental rebuilds.
|
|
69
|
+
try { await iiif.ensureFeaturedInCache(CONFIG); } catch (_) {}
|
|
65
70
|
|
|
66
71
|
/**
|
|
67
72
|
* Build contextual MDX content from the content directory.
|
|
@@ -72,6 +77,7 @@ async function build(options = {}) {
|
|
|
72
77
|
bright: true,
|
|
73
78
|
underscore: true,
|
|
74
79
|
});
|
|
80
|
+
// FeaturedHero now reads directly from the local IIIF cache; no API file needed
|
|
75
81
|
pageRecords = await searchBuild.collectMdxPageRecords();
|
|
76
82
|
await pages.buildContentTree(CONTENT_DIR, pageRecords);
|
|
77
83
|
logLine("✓ MDX pages built", "green");
|
|
@@ -94,6 +100,8 @@ async function build(options = {}) {
|
|
|
94
100
|
logLine(" " + String(e), "red");
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
// No-op: Featured API file no longer written (SSR reads from cache directly)
|
|
104
|
+
|
|
97
105
|
/**
|
|
98
106
|
* Prepare client runtimes (e.g. search) by bundling with esbuild.
|
|
99
107
|
* This is done early so that MDX content can reference runtime assets if needed.
|
|
@@ -116,6 +124,17 @@ async function build(options = {}) {
|
|
|
116
124
|
underscore: true,
|
|
117
125
|
});
|
|
118
126
|
await copyAssets();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Final verification (checklist)
|
|
130
|
+
*/
|
|
131
|
+
try {
|
|
132
|
+
verifyBuildOutput({ outDir: OUT_DIR });
|
|
133
|
+
} catch (e) {
|
|
134
|
+
logLine("✗ Build verification failed", "red", { bright: true });
|
|
135
|
+
logLine(String(e && e.message ? e.message : e), "red");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
module.exports = { build };
|
package/lib/build/iiif.js
CHANGED
|
@@ -365,6 +365,51 @@ async function saveCachedManifest(manifest, id, parentId) {
|
|
|
365
365
|
} catch (_) {}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// Ensure any configured featured manifests are present in the local cache
|
|
369
|
+
// (and have thumbnails computed) so SSR components like <Hero /> can read them.
|
|
370
|
+
async function ensureFeaturedInCache(cfg) {
|
|
371
|
+
try {
|
|
372
|
+
const CONFIG = cfg || (await loadConfig());
|
|
373
|
+
const featured = Array.isArray(CONFIG && CONFIG.featured) ? CONFIG.featured : [];
|
|
374
|
+
if (!featured.length) return;
|
|
375
|
+
const { getThumbnail } = require("../iiif/thumbnail");
|
|
376
|
+
// Thumbnail sizing config
|
|
377
|
+
const thumbSize = CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && typeof CONFIG.iiif.thumbnails.preferredSize === 'number' ? CONFIG.iiif.thumbnails.preferredSize : 400;
|
|
378
|
+
const unsafeThumbs = !!(CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && (CONFIG.iiif.thumbnails.unsafe === true || CONFIG.iiif.thumbnails.unsafe === 'true'));
|
|
379
|
+
for (const rawId of featured) {
|
|
380
|
+
const id = normalizeIiifId(String(rawId || ''));
|
|
381
|
+
if (!id) continue;
|
|
382
|
+
let manifest = await loadCachedManifestById(id);
|
|
383
|
+
if (!manifest) {
|
|
384
|
+
const m = await readJsonFromUri(id).catch(() => null);
|
|
385
|
+
if (!m) continue;
|
|
386
|
+
const v3 = await normalizeToV3(m);
|
|
387
|
+
if (!v3 || !v3.id) continue;
|
|
388
|
+
await saveCachedManifest(v3, id, '');
|
|
389
|
+
manifest = v3;
|
|
390
|
+
}
|
|
391
|
+
// Ensure thumbnail fields exist in index for this manifest (if computable)
|
|
392
|
+
try {
|
|
393
|
+
const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
394
|
+
if (t && t.url) {
|
|
395
|
+
const idx = await loadManifestIndex();
|
|
396
|
+
if (Array.isArray(idx.byId)) {
|
|
397
|
+
const entry = idx.byId.find((e) => e && e.type === 'Manifest' && normalizeIiifId(String(e.id)) === normalizeIiifId(String(manifest.id)));
|
|
398
|
+
if (entry) {
|
|
399
|
+
entry.thumbnail = String(t.url);
|
|
400
|
+
if (typeof t.width === 'number') entry.thumbnailWidth = t.width;
|
|
401
|
+
if (typeof t.height === 'number') entry.thumbnailHeight = t.height;
|
|
402
|
+
await saveManifestIndex(idx);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch (_) {}
|
|
407
|
+
}
|
|
408
|
+
} catch (_) {
|
|
409
|
+
// ignore failures; fallback SSR will still render a minimal hero without content
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
368
413
|
async function flushManifestCache() {
|
|
369
414
|
try {
|
|
370
415
|
await fsp.rm(IIIF_CACHE_MANIFESTS_DIR, { recursive: true, force: true });
|
|
@@ -762,7 +807,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
762
807
|
href = withBase(href);
|
|
763
808
|
return React.createElement("a", { href, ...rest }, props.children);
|
|
764
809
|
};
|
|
810
|
+
// Map exported UI components into MDX, with sensible aliases/fallbacks
|
|
765
811
|
const compMap = { ...components, a: Anchor };
|
|
812
|
+
if (!compMap.SearchPanel && compMap.CommandPalette) {
|
|
813
|
+
compMap.SearchPanel = compMap.CommandPalette;
|
|
814
|
+
}
|
|
766
815
|
if (!components.HelloWorld) {
|
|
767
816
|
components.HelloWorld = components.Fallback
|
|
768
817
|
? (props) =>
|
|
@@ -811,6 +860,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
811
860
|
.join("/");
|
|
812
861
|
const needsHydrateViewer = body.includes("data-canopy-viewer");
|
|
813
862
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
863
|
+
const needsHero = body.includes("data-canopy-hero");
|
|
814
864
|
const needsCommand = body.includes("data-canopy-command");
|
|
815
865
|
const needsHydrate =
|
|
816
866
|
body.includes("data-canopy-hydrate") ||
|
|
@@ -836,6 +886,15 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
836
886
|
.split(path.sep)
|
|
837
887
|
.join("/")
|
|
838
888
|
: null;
|
|
889
|
+
const heroRel = needsHero
|
|
890
|
+
? path
|
|
891
|
+
.relative(
|
|
892
|
+
path.dirname(outPath),
|
|
893
|
+
path.join(OUT_DIR, "scripts", "canopy-hero.js")
|
|
894
|
+
)
|
|
895
|
+
.split(path.sep)
|
|
896
|
+
.join("/")
|
|
897
|
+
: null;
|
|
839
898
|
const relatedRel = needsRelated
|
|
840
899
|
? path
|
|
841
900
|
.relative(
|
|
@@ -856,11 +915,12 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
856
915
|
: null;
|
|
857
916
|
|
|
858
917
|
let jsRel = null;
|
|
859
|
-
if (
|
|
918
|
+
if (needsHero && heroRel) jsRel = heroRel;
|
|
919
|
+
else if (needsRelated && sliderRel) jsRel = sliderRel;
|
|
860
920
|
else if (viewerRel) jsRel = viewerRel;
|
|
861
921
|
|
|
862
922
|
let headExtra = head;
|
|
863
|
-
const needsReact = !!(needsHydrateViewer || needsRelated);
|
|
923
|
+
const needsReact = !!(needsHydrateViewer || needsRelated || needsHero);
|
|
864
924
|
let vendorTag = "";
|
|
865
925
|
if (needsReact) {
|
|
866
926
|
try {
|
|
@@ -881,6 +941,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
881
941
|
} catch (_) {}
|
|
882
942
|
}
|
|
883
943
|
const extraScripts = [];
|
|
944
|
+
if (heroRel && jsRel !== heroRel)
|
|
945
|
+
extraScripts.push(`<script defer src="${heroRel}"></script>`);
|
|
884
946
|
if (relatedRel && jsRel !== relatedRel)
|
|
885
947
|
extraScripts.push(`<script defer src="${relatedRel}"></script>`);
|
|
886
948
|
if (viewerRel && jsRel !== viewerRel)
|
|
@@ -979,6 +1041,10 @@ module.exports = {
|
|
|
979
1041
|
loadConfig,
|
|
980
1042
|
loadManifestIndex,
|
|
981
1043
|
saveManifestIndex,
|
|
1044
|
+
// Expose helpers used by build for cache warming
|
|
1045
|
+
loadCachedManifestById,
|
|
1046
|
+
saveCachedManifest,
|
|
1047
|
+
ensureFeaturedInCache,
|
|
982
1048
|
};
|
|
983
1049
|
|
|
984
1050
|
// Debug: list collections cache after traversal
|
package/lib/build/mdx.js
CHANGED
|
@@ -44,15 +44,210 @@ async function getMdxProvider() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Lazily load UI components from the workspace package and cache them.
|
|
47
|
+
// Re-import when the built UI server bundle changes on disk.
|
|
47
48
|
let UI_COMPONENTS = null;
|
|
49
|
+
let UI_COMPONENTS_PATH = '';
|
|
50
|
+
let UI_COMPONENTS_MTIME = 0;
|
|
51
|
+
const DEBUG = process.env.CANOPY_DEBUG === '1' || process.env.CANOPY_DEBUG === 'true';
|
|
48
52
|
async function loadUiComponents() {
|
|
49
|
-
|
|
53
|
+
// Do not rely on a cached mapping; re-import each time to avoid transient races.
|
|
54
|
+
const fallbackCommand = function CommandPalette(props = {}) {
|
|
55
|
+
const {
|
|
56
|
+
placeholder = 'Search…',
|
|
57
|
+
hotkey = 'mod+k',
|
|
58
|
+
maxResults = 8,
|
|
59
|
+
groupOrder = ['work', 'page'],
|
|
60
|
+
button = true,
|
|
61
|
+
buttonLabel = 'Search',
|
|
62
|
+
label,
|
|
63
|
+
searchPath = '/search',
|
|
64
|
+
} = props || {};
|
|
65
|
+
const text = (typeof label === 'string' && label.trim()) ? label.trim() : buttonLabel;
|
|
66
|
+
let json = "{}";
|
|
67
|
+
try { json = JSON.stringify({ placeholder, hotkey, maxResults, groupOrder, label: text, searchPath }); } catch (_) { json = "{}"; }
|
|
68
|
+
return React.createElement(
|
|
69
|
+
'div',
|
|
70
|
+
{ 'data-canopy-command': true, className: 'flex-1 min-w-0' },
|
|
71
|
+
// Teaser form fallback
|
|
72
|
+
React.createElement('div', { className: 'relative w-full' },
|
|
73
|
+
React.createElement('style', { dangerouslySetInnerHTML: { __html: ".relative[data-canopy-panel-auto='1']:focus-within [data-canopy-command-panel]{display:block}" } }),
|
|
74
|
+
React.createElement('form', { action: searchPath, method: 'get', role: 'search', className: 'group flex items-center gap-2 px-2 py-1.5 rounded-lg border border-slate-300 bg-white/95 backdrop-blur text-slate-700 shadow-sm hover:shadow transition w-full focus-within:ring-2 focus-within:ring-brand-500' },
|
|
75
|
+
// Left icon
|
|
76
|
+
React.createElement('span', { 'aria-hidden': true, className: 'text-slate-500' }, '🔎'),
|
|
77
|
+
// Input teaser
|
|
78
|
+
React.createElement('input', { type: 'search', name: 'q', 'data-canopy-command-input': true, placeholder, 'aria-label': 'Search', className: 'flex-1 bg-transparent outline-none placeholder:text-slate-400 py-0.5 min-w-0' }),
|
|
79
|
+
// Right submit (navigates)
|
|
80
|
+
React.createElement('button', { type: 'submit', 'data-canopy-command-link': true, className: 'inline-flex items-center gap-1 px-2 py-1 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 text-slate-700' }, text)
|
|
81
|
+
),
|
|
82
|
+
// SSR placeholder panel; runtime will reuse and control visibility
|
|
83
|
+
React.createElement('div', { 'data-canopy-command-panel': true, style: { display: 'none', position: 'absolute', left: 0, right: 0, top: 'calc(100% + 4px)', background: '#fff', border: '1px solid #e5e7eb', borderRadius: '8px', boxShadow: '0 10px 25px rgba(0,0,0,0.12)', zIndex: 1000, overflow: 'auto', maxHeight: '60vh' } },
|
|
84
|
+
React.createElement('div', { id: 'cplist' })
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: json } })
|
|
88
|
+
);
|
|
89
|
+
};
|
|
50
90
|
try {
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
91
|
+
// Prefer resolving the actual dist file and busting cache via mtime to pick up new exports during dev
|
|
92
|
+
let resolved = null;
|
|
93
|
+
// Prefer explicit dist path to avoid export-map issues
|
|
94
|
+
try { resolved = require.resolve("@canopy-iiif/app/ui/dist/server.mjs"); } catch (_) {
|
|
95
|
+
try { resolved = require.resolve("@canopy-iiif/app/ui/server"); } catch (_) { resolved = null; }
|
|
96
|
+
}
|
|
97
|
+
// Determine current mtime for change detection
|
|
98
|
+
let currentPath = resolved || '';
|
|
99
|
+
let currentMtime = 0;
|
|
100
|
+
if (currentPath) {
|
|
101
|
+
try { const st = fs.statSync(currentPath); currentMtime = Math.floor(st.mtimeMs || 0); } catch (_) { currentMtime = 0; }
|
|
102
|
+
}
|
|
103
|
+
// If we have a cached module and the path/mtime have not changed, return cached
|
|
104
|
+
if (UI_COMPONENTS && UI_COMPONENTS_PATH === currentPath && UI_COMPONENTS_MTIME === currentMtime) {
|
|
105
|
+
if (DEBUG) {
|
|
106
|
+
try { console.log('[canopy][mdx] UI components cache hit:', { path: UI_COMPONENTS_PATH, mtime: UI_COMPONENTS_MTIME }); } catch(_){}
|
|
107
|
+
}
|
|
108
|
+
return UI_COMPONENTS;
|
|
109
|
+
}
|
|
110
|
+
let mod = null;
|
|
111
|
+
if (resolved) {
|
|
112
|
+
const { pathToFileURL } = require("url");
|
|
113
|
+
let bust = currentMtime ? `?v=${currentMtime}` : `?v=${Date.now()}`;
|
|
114
|
+
mod = await import(pathToFileURL(resolved).href + bust).catch((e) => {
|
|
115
|
+
if (DEBUG) { try { console.warn('[canopy][mdx] ESM import failed for', resolved, '\n', e && (e.stack || e.message || String(e))); } catch(_){} }
|
|
116
|
+
return null;
|
|
117
|
+
});
|
|
118
|
+
if (DEBUG) {
|
|
119
|
+
try { console.log('[canopy][mdx] UI components resolved', { path: resolved, mtime: currentMtime, loaded: !!mod }); } catch(_){}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!mod) {
|
|
123
|
+
mod = await import("@canopy-iiif/app/ui/server").catch((e) => {
|
|
124
|
+
if (DEBUG) { try { console.warn('[canopy][mdx] Export-map import failed for @canopy-iiif/app/ui/server', '\n', e && (e.stack || e.message || String(e))); } catch(_){} }
|
|
125
|
+
return null;
|
|
126
|
+
});
|
|
127
|
+
if (DEBUG) {
|
|
128
|
+
try { console.log('[canopy][mdx] UI components fallback import via export map', { ok: !!mod }); } catch(_){}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let comp = (mod && typeof mod === 'object') ? mod : {};
|
|
132
|
+
// Fallback: import workspace source directly if key exports missing
|
|
133
|
+
const needFallback = !(comp && (comp.CommandPalette || comp.FeaturedHero || comp.Viewer));
|
|
134
|
+
if (needFallback) {
|
|
135
|
+
try {
|
|
136
|
+
const ws = path.join(process.cwd(), 'packages', 'app', 'ui', 'server.js');
|
|
137
|
+
if (fs.existsSync(ws)) {
|
|
138
|
+
const { pathToFileURL } = require('url');
|
|
139
|
+
// Use workspace server.js mtime for busting
|
|
140
|
+
let bustWs = `?v=${Date.now()}`;
|
|
141
|
+
try { const st = fs.statSync(ws); currentPath = ws; currentMtime = Math.floor(st.mtimeMs || 0); bustWs = `?v=${currentMtime}`; } catch (_) {}
|
|
142
|
+
const wsMod = await import(pathToFileURL(ws).href + bustWs).catch(() => null);
|
|
143
|
+
if (wsMod && typeof wsMod === 'object') {
|
|
144
|
+
comp = { ...wsMod, ...comp };
|
|
145
|
+
}
|
|
146
|
+
if (DEBUG) {
|
|
147
|
+
try { console.log('[canopy][mdx] UI components augmented from workspace source', { ws, ok: !!wsMod }); } catch(_){}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (_) {}
|
|
151
|
+
}
|
|
152
|
+
// Ensure core placeholders exist to avoid MDX compile failures
|
|
153
|
+
if (!comp.CommandPalette) comp.CommandPalette = fallbackCommand;
|
|
154
|
+
if (!comp.SearchPanel) comp.SearchPanel = comp.CommandPalette || fallbackCommand;
|
|
155
|
+
if (DEBUG) {
|
|
156
|
+
try { console.log('[canopy][mdx] UI component sources', {
|
|
157
|
+
path: currentPath,
|
|
158
|
+
mtime: currentMtime,
|
|
159
|
+
hasServerExport: !!mod,
|
|
160
|
+
hasWorkspace: typeof comp !== 'undefined',
|
|
161
|
+
CommandPalette: !!comp.CommandPalette,
|
|
162
|
+
Viewer: !!comp.Viewer,
|
|
163
|
+
Slider: !!comp.Slider,
|
|
164
|
+
}); } catch(_){}
|
|
165
|
+
}
|
|
166
|
+
const mkJson = (props) => {
|
|
167
|
+
try { return JSON.stringify(props || {}); } catch (_) { return '{}'; }
|
|
168
|
+
};
|
|
169
|
+
if (!comp.Viewer) comp.Viewer = function Viewer(props){
|
|
170
|
+
return React.createElement('div', { 'data-canopy-viewer': '1', className: 'not-prose' },
|
|
171
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
if (!comp.Slider) comp.Slider = function Slider(props){
|
|
175
|
+
return React.createElement('div', { 'data-canopy-slider': '1', className: 'not-prose' },
|
|
176
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
if (!comp.RelatedItems) comp.RelatedItems = function RelatedItems(props){
|
|
180
|
+
return React.createElement('div', { 'data-canopy-related-items': '1', className: 'not-prose' },
|
|
181
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
if (!comp.SearchForm) comp.SearchForm = function SearchForm(props){
|
|
185
|
+
return React.createElement('div', { 'data-canopy-search-form': '1' },
|
|
186
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
if (!comp.SearchResults) comp.SearchResults = function SearchResults(props){
|
|
190
|
+
return React.createElement('div', { 'data-canopy-search-results': '1' },
|
|
191
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
if (!comp.SearchSummary) comp.SearchSummary = function SearchSummary(props){
|
|
195
|
+
return React.createElement('div', { 'data-canopy-search-summary': '1' },
|
|
196
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
if (!comp.SearchTotal) comp.SearchTotal = function SearchTotal(props){
|
|
200
|
+
return React.createElement('div', { 'data-canopy-search-total': '1' },
|
|
201
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
if (!comp.SearchTabs) comp.SearchTabs = function SearchTabs(props){
|
|
205
|
+
return React.createElement('div', { 'data-canopy-search-tabs': '1' },
|
|
206
|
+
React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
// Ensure a minimal SSR Hero exists
|
|
210
|
+
if (!comp.Hero) {
|
|
211
|
+
comp.Hero = function SimpleHero({ height = 360, item, className = '', style = {}, ...rest }){
|
|
212
|
+
const h = typeof height === 'number' ? `${height}px` : String(height || '').trim() || '360px';
|
|
213
|
+
const base = { position: 'relative', width: '100%', height: h, overflow: 'hidden', backgroundColor: 'var(--color-gray-muted)', ...style };
|
|
214
|
+
const title = (item && item.title) || '';
|
|
215
|
+
const href = (item && item.href) || '#';
|
|
216
|
+
const thumbnail = (item && item.thumbnail) || '';
|
|
217
|
+
return React.createElement('div', { className: ['canopy-hero', className].filter(Boolean).join(' '), style: base, ...rest },
|
|
218
|
+
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,
|
|
219
|
+
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' } },
|
|
220
|
+
React.createElement('h3', { style: { margin:0, fontSize:'1.5rem', fontWeight:600, lineHeight:1.2, textShadow:'0 1px 3px rgba(0,0,0,0.6)' } },
|
|
221
|
+
React.createElement('a', { href, style:{ color:'inherit', textDecoration:'none' }, className:'canopy-hero-link' }, title)
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Provide a minimal SSR FeaturedHero fallback if missing
|
|
228
|
+
if (!comp.FeaturedHero) {
|
|
229
|
+
try {
|
|
230
|
+
const helpers = require('../components/featured');
|
|
231
|
+
comp.FeaturedHero = function FeaturedHero(props) {
|
|
232
|
+
try {
|
|
233
|
+
const list = helpers && helpers.readFeaturedFromCacheSync ? helpers.readFeaturedFromCacheSync() : [];
|
|
234
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
235
|
+
const index = (props && typeof props.index === 'number') ? Math.max(0, Math.min(list.length - 1, Math.floor(props.index))) : null;
|
|
236
|
+
const pick = (index != null) ? index : ((props && (props.random === true || props.random === 'true')) ? Math.floor(Math.random() * list.length) : 0);
|
|
237
|
+
const item = list[pick] || list[0];
|
|
238
|
+
return React.createElement(comp.Hero, { ...props, item });
|
|
239
|
+
} catch (_) { return null; }
|
|
240
|
+
};
|
|
241
|
+
} catch (_) { /* ignore */ }
|
|
242
|
+
}
|
|
243
|
+
UI_COMPONENTS = comp;
|
|
244
|
+
UI_COMPONENTS_PATH = currentPath;
|
|
245
|
+
UI_COMPONENTS_MTIME = currentMtime;
|
|
54
246
|
} catch (_) {
|
|
55
|
-
|
|
247
|
+
// As a last resort, supply minimal stubs so pages still compile
|
|
248
|
+
UI_COMPONENTS = { CommandPalette: fallbackCommand };
|
|
249
|
+
UI_COMPONENTS_PATH = '';
|
|
250
|
+
UI_COMPONENTS_MTIME = 0;
|
|
56
251
|
}
|
|
57
252
|
return UI_COMPONENTS;
|
|
58
253
|
}
|
|
@@ -672,6 +867,79 @@ async function ensureReactGlobals() {
|
|
|
672
867
|
});
|
|
673
868
|
}
|
|
674
869
|
|
|
870
|
+
// Bundle a small runtime to hydrate <Hero /> placeholders from featured items
|
|
871
|
+
async function ensureHeroRuntime() {
|
|
872
|
+
let esbuild = null;
|
|
873
|
+
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
874
|
+
if (!esbuild) return;
|
|
875
|
+
const { path } = require("../common");
|
|
876
|
+
ensureDirSync(OUT_DIR);
|
|
877
|
+
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
878
|
+
ensureDirSync(scriptsDir);
|
|
879
|
+
const outFile = path.join(scriptsDir, 'canopy-hero.js');
|
|
880
|
+
const entry = `
|
|
881
|
+
import { Hero } from '@canopy-iiif/app/ui';
|
|
882
|
+
function ready(fn){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }
|
|
883
|
+
function parseProps(el){ try{ const s=el.querySelector('script[type="application/json"]'); if(s) return JSON.parse(s.textContent||'{}'); }catch(_){ } return {}; }
|
|
884
|
+
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 ''; } }
|
|
885
|
+
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 ''; } }
|
|
886
|
+
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 []; } }
|
|
887
|
+
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(_){} }
|
|
888
|
+
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(_){} }); });
|
|
889
|
+
`;
|
|
890
|
+
const reactShim = `
|
|
891
|
+
const React = (typeof window !== 'undefined' && window.React) || {};
|
|
892
|
+
export default React;
|
|
893
|
+
export const Children = React.Children;
|
|
894
|
+
export const Component = React.Component;
|
|
895
|
+
export const Fragment = React.Fragment;
|
|
896
|
+
export const createElement = React.createElement;
|
|
897
|
+
export const cloneElement = React.cloneElement;
|
|
898
|
+
export const createContext = React.createContext;
|
|
899
|
+
export const forwardRef = React.forwardRef;
|
|
900
|
+
export const memo = React.memo;
|
|
901
|
+
export const startTransition = React.startTransition;
|
|
902
|
+
export const isValidElement = React.isValidElement;
|
|
903
|
+
export const useEffect = React.useEffect;
|
|
904
|
+
export const useLayoutEffect = React.useLayoutEffect;
|
|
905
|
+
export const useMemo = React.useMemo;
|
|
906
|
+
export const useState = React.useState;
|
|
907
|
+
export const useRef = React.useRef;
|
|
908
|
+
export const useCallback = React.useCallback;
|
|
909
|
+
export const useContext = React.useContext;
|
|
910
|
+
export const useReducer = React.useReducer;
|
|
911
|
+
export const useId = React.useId;
|
|
912
|
+
`;
|
|
913
|
+
const rdomClientShim = `
|
|
914
|
+
const RDC = (typeof window !== 'undefined' && window.ReactDOMClient) || {};
|
|
915
|
+
export default RDC;
|
|
916
|
+
export const createRoot = RDC.createRoot;
|
|
917
|
+
export const hydrateRoot = RDC.hydrateRoot;
|
|
918
|
+
`;
|
|
919
|
+
const plugin = {
|
|
920
|
+
name: 'canopy-react-shims-hero',
|
|
921
|
+
setup(build) {
|
|
922
|
+
const ns = 'canopy-shim';
|
|
923
|
+
build.onResolve({ filter: /^react$/ }, () => ({ path: 'react', namespace: ns }));
|
|
924
|
+
build.onResolve({ filter: /^react-dom\/client$/ }, () => ({ path: 'react-dom-client', namespace: ns }));
|
|
925
|
+
build.onLoad({ filter: /^react$/, namespace: ns }, () => ({ contents: reactShim, loader: 'js' }));
|
|
926
|
+
build.onLoad({ filter: /^react-dom-client$/, namespace: ns }, () => ({ contents: rdomClientShim, loader: 'js' }));
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
await esbuild.build({
|
|
930
|
+
stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-hero-entry.js', loader: 'js' },
|
|
931
|
+
outfile: outFile,
|
|
932
|
+
platform: 'browser',
|
|
933
|
+
format: 'iife',
|
|
934
|
+
bundle: true,
|
|
935
|
+
sourcemap: false,
|
|
936
|
+
target: ['es2018'],
|
|
937
|
+
logLevel: 'silent',
|
|
938
|
+
minify: true,
|
|
939
|
+
plugins: [plugin],
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
675
943
|
module.exports = {
|
|
676
944
|
extractTitle,
|
|
677
945
|
isReservedFile,
|
|
@@ -682,6 +950,7 @@ module.exports = {
|
|
|
682
950
|
loadAppWrapper,
|
|
683
951
|
ensureClientRuntime,
|
|
684
952
|
ensureSliderRuntime,
|
|
953
|
+
ensureHeroRuntime,
|
|
685
954
|
ensureFacetsRuntime,
|
|
686
955
|
ensureReactGlobals,
|
|
687
956
|
resetMdxCaches: function () {
|
|
@@ -689,5 +958,6 @@ module.exports = {
|
|
|
689
958
|
DIR_LAYOUTS.clear();
|
|
690
959
|
} catch (_) {}
|
|
691
960
|
APP_WRAPPER = null;
|
|
961
|
+
UI_COMPONENTS = null;
|
|
692
962
|
},
|
|
693
963
|
};
|
package/lib/build/pages.js
CHANGED
|
@@ -62,9 +62,13 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
62
62
|
const facetsRel = needsFacets
|
|
63
63
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
|
|
64
64
|
: null;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
let commandRel = null;
|
|
66
|
+
if (needsCommand) {
|
|
67
|
+
const cmdAbs = path.join(OUT_DIR, 'scripts', 'canopy-command.js');
|
|
68
|
+
let rel = path.relative(path.dirname(outPath), cmdAbs).split(path.sep).join('/');
|
|
69
|
+
try { const st = fs.statSync(cmdAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
|
|
70
|
+
commandRel = rel;
|
|
71
|
+
}
|
|
68
72
|
let jsRel = null;
|
|
69
73
|
if (needsFacets && sliderRel) jsRel = sliderRel;
|
|
70
74
|
else if (viewerRel) jsRel = viewerRel;
|
package/lib/build/runtimes.js
CHANGED
|
@@ -6,6 +6,12 @@ async function prepareAllRuntimes() {
|
|
|
6
6
|
const mdx = require('./mdx');
|
|
7
7
|
try { await mdx.ensureClientRuntime(); } catch (_) {}
|
|
8
8
|
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
|
+
try {
|
|
11
|
+
if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
|
|
12
|
+
if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
|
|
13
|
+
}
|
|
14
|
+
} catch (_) {}
|
|
9
15
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
10
16
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
11
17
|
try { await ensureCommandFallback(); } catch (_) {}
|
|
@@ -24,15 +30,63 @@ async function ensureCommandFallback() {
|
|
|
24
30
|
function norm(s){ try{ return String(s||'').toLowerCase(); }catch(_){ return ''; } }
|
|
25
31
|
function withBase(href){ try{ var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; if(!bp) return href; if(/^https?:/i.test(href)) return href; var clean = href.replace(/^\\/+/, ''); return (bp.endsWith('/') ? bp.slice(0,-1) : bp) + '/' + clean; } catch(_){ return href; } }
|
|
26
32
|
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 ''; } }
|
|
27
|
-
function
|
|
33
|
+
function isOnSearchPage(){ try{ var base=rootBase(); var p=String(location.pathname||''); if(base && p.startsWith(base)) p=p.slice(base.length); if(p.endsWith('/')) p=p.slice(0,-1); return p==='/search'; }catch(_){ return false; } }
|
|
34
|
+
function createUI(host){ var rel=(function(){ try{ var w=host.querySelector('.relative'); return w||host; }catch(_){ return host; } })(); try{ var cs = window.getComputedStyle(rel); if(!cs || cs.position==='static') rel.style.position='relative'; }catch(_){ } var existing = rel.querySelector('[data-canopy-command-panel]'); if(existing) return existing; var panel=document.createElement('div'); panel.setAttribute('data-canopy-command-panel',''); panel.style.cssText='position:absolute;left:0;right:0;top:calc(100% + 4px);display:none;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 10px 25px rgba(0,0,0,0.12);z-index:1000;overflow:auto;max-height:60vh;'; panel.innerHTML='<div id="cplist"></div>'; rel.appendChild(panel); return panel; }
|
|
28
35
|
async function loadRecords(){ try{ var v=''; try{ var m = await fetch(rootBase() + '/api/index.json').then(function(r){return r&&r.ok?r.json():null;}).catch(function(){return null;}); v=(m&&m.version)||''; }catch(_){} var res = await fetch(rootBase() + '/api/search-index.json' + (v?('?v='+encodeURIComponent(v)):'')).catch(function(){return null;}); var j = res && res.ok ? await res.json().catch(function(){return[];}) : []; return Array.isArray(j) ? j : (j && j.records) || []; } catch(_){ return []; } }
|
|
29
|
-
ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var onSearchPage = isOnSearchPage(); var panel=createUI(host); try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){} if(onSearchPage){ panel.style.display='none'; } var list=panel.querySelector('#cplist'); var teaser = host.querySelector('[data-canopy-command-input]'); var input = teaser; if(!input){ input = document.createElement('input'); input.type='search'; input.placeholder = cfg.placeholder || 'Search…'; input.setAttribute('aria-label','Search'); input.style.cssText='display:block;width:100%;padding:8px 10px;border-bottom:1px solid #e5e7eb;outline:none;'; panel.insertBefore(input, list); }
|
|
37
|
+
// Populate from ?q= URL param if present
|
|
38
|
+
try {
|
|
39
|
+
var sp = new URLSearchParams(location.search || '');
|
|
40
|
+
var qp = sp.get('q');
|
|
41
|
+
if (qp) input.value = qp;
|
|
42
|
+
} catch(_) {}
|
|
43
|
+
// Only inject a legacy trigger button if neither a trigger nor a teaser input exists
|
|
44
|
+
try{ if(!host.querySelector('[data-canopy-command-trigger]') && !host.querySelector('[data-canopy-command-input]')){ var btn=document.createElement('button'); btn.type='button'; btn.setAttribute('data-canopy-command-trigger',''); btn.setAttribute('aria-label','Open search'); btn.className='inline-flex items-center gap-2 px-3 py-1.5 rounded border border-slate-300 text-slate-700 hover:bg-slate-50'; var lbl=((cfg&&cfg.label)||(cfg&&cfg.buttonLabel)||'Search'); var sLbl=document.createElement('span'); sLbl.textContent=String(lbl); var sK=document.createElement('span'); sK.setAttribute('aria-hidden','true'); sK.className='text-slate-500'; sK.textContent='⌘K'; btn.appendChild(sLbl); btn.appendChild(sK); host.appendChild(btn); } }catch(_){ }
|
|
45
|
+
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); }); }); }
|
|
46
|
+
function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
|
|
47
|
+
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(_){} }
|
|
48
|
+
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||''); });
|
|
49
|
+
// Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
|
|
50
|
+
input.addEventListener('keydown', function(e){
|
|
51
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
|
|
52
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
|
|
53
|
+
});
|
|
54
|
+
list.addEventListener('keydown', function(e){
|
|
55
|
+
var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
|
|
56
|
+
if(!cur) return;
|
|
57
|
+
if(e.key==='ArrowDown'){
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
|
|
60
|
+
} else if(e.key==='ArrowUp'){
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
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(_){}
|
|
63
|
+
} else if(e.key==='Enter'){
|
|
64
|
+
e.preventDefault(); try{ cur.click(); }catch(_){}
|
|
65
|
+
} else if(e.key==='Escape'){
|
|
66
|
+
panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
|
|
70
|
+
document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
|
|
71
|
+
// Hotkey support (e.g., mod+k)
|
|
72
|
+
document.addEventListener('keydown', function(e){
|
|
73
|
+
try {
|
|
74
|
+
var want = String((cfg && cfg.hotkey) || '').toLowerCase();
|
|
75
|
+
if (!want) return;
|
|
76
|
+
var isMod = e.metaKey || e.ctrlKey;
|
|
77
|
+
if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
|
|
80
|
+
panel.style.display='block';
|
|
81
|
+
(input && input.focus && input.focus());
|
|
82
|
+
filterAndShow(input && input.value || '');
|
|
83
|
+
}
|
|
84
|
+
} catch(_) { }
|
|
85
|
+
});
|
|
86
|
+
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 || ''); }
|
|
87
|
+
host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
|
|
88
|
+
var btn = document.querySelector('[data-canopy-command-trigger]'); if(btn){ btn.addEventListener('click', function(){ openPanel(); }); }
|
|
89
|
+
try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
|
|
36
90
|
});
|
|
37
91
|
})();
|
|
38
92
|
`;
|