@canopy-iiif/app 0.8.4 → 0.8.5
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/iiif.js +359 -83
- package/lib/build/styles.js +53 -1
- package/lib/common.js +28 -6
- package/lib/search/search-app.jsx +177 -25
- package/lib/search/search-form-runtime.js +126 -19
- package/lib/search/search.js +130 -18
- package/package.json +4 -1
- package/ui/dist/index.mjs +201 -97
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +44 -25
- package/ui/dist/server.mjs.map +2 -2
- package/ui/styles/_variables.scss +1 -0
- package/ui/styles/base/_common.scss +27 -5
- package/ui/styles/base/_heading.scss +2 -4
- package/ui/styles/base/index.scss +1 -0
- package/ui/styles/components/_card.scss +47 -4
- package/ui/styles/components/_sub-navigation.scss +14 -14
- package/ui/styles/components/header/_header.scss +1 -4
- package/ui/styles/components/header/_logo.scss +33 -10
- package/ui/styles/components/search/_filters.scss +5 -7
- package/ui/styles/components/search/_form.scss +55 -17
- package/ui/styles/components/search/_results.scss +13 -15
- package/ui/styles/index.css +250 -72
- package/ui/styles/index.scss +2 -4
- package/ui/tailwind-canopy-iiif-plugin.js +10 -2
- package/ui/tailwind-canopy-iiif-preset.js +21 -19
- package/ui/theme.js +303 -0
- package/ui/styles/variables.emit.scss +0 -72
- package/ui/styles/variables.scss +0 -76
package/lib/build/styles.js
CHANGED
|
@@ -97,6 +97,52 @@ async function ensureStyles() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function injectThemeTokens(targetPath) {
|
|
101
|
+
try {
|
|
102
|
+
const { loadCanopyTheme } = require("@canopy-iiif/app/ui/theme");
|
|
103
|
+
const theme = loadCanopyTheme();
|
|
104
|
+
const themeCss = theme && theme.css ? theme.css.trim() : "";
|
|
105
|
+
if (!themeCss) return;
|
|
106
|
+
|
|
107
|
+
let existing = "";
|
|
108
|
+
try {
|
|
109
|
+
existing = fs.readFileSync(targetPath, "utf8");
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
|
|
112
|
+
const marker = "/* canopy-theme */";
|
|
113
|
+
const markerEnd = "/* canopy-theme:end */";
|
|
114
|
+
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${markerEnd}\\n?`, "g");
|
|
115
|
+
const sanitized = existing.replace(markerRegex, "");
|
|
116
|
+
|
|
117
|
+
const layerRegex = /@layer properties\{([\s\S]*?)\}(?=@|$)/;
|
|
118
|
+
const match = layerRegex.exec(sanitized);
|
|
119
|
+
let before = sanitized;
|
|
120
|
+
let after = "";
|
|
121
|
+
let customRulesBlock = "";
|
|
122
|
+
|
|
123
|
+
if (match) {
|
|
124
|
+
before = sanitized.slice(0, match.index);
|
|
125
|
+
after = sanitized.slice(match.index + match[0].length);
|
|
126
|
+
const layerBody = match[1] || "";
|
|
127
|
+
const boundaryMatch = /}\s*:/.exec(layerBody);
|
|
128
|
+
if (boundaryMatch) {
|
|
129
|
+
const start = boundaryMatch.index + boundaryMatch[0].length - 1;
|
|
130
|
+
const customSegment = layerBody.slice(start).trim();
|
|
131
|
+
if (customSegment) {
|
|
132
|
+
const normalized = customSegment.endsWith("}")
|
|
133
|
+
? customSegment
|
|
134
|
+
: `${customSegment}}`;
|
|
135
|
+
customRulesBlock = `@layer properties {\n ${normalized}\n}\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const themeBlock = `${marker}\n${themeCss}\n${markerEnd}\n`;
|
|
141
|
+
const next = `${before}${themeBlock}${customRulesBlock}${after}`;
|
|
142
|
+
fs.writeFileSync(targetPath, next, "utf8");
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
100
146
|
if (configPath && (inputCss || generatedInput)) {
|
|
101
147
|
const ok = buildTailwindCli({
|
|
102
148
|
input: inputCss || generatedInput,
|
|
@@ -104,7 +150,10 @@ async function ensureStyles() {
|
|
|
104
150
|
config: configPath,
|
|
105
151
|
minify: true,
|
|
106
152
|
});
|
|
107
|
-
if (ok)
|
|
153
|
+
if (ok) {
|
|
154
|
+
injectThemeTokens(dest);
|
|
155
|
+
return; // Tailwind compiled CSS
|
|
156
|
+
}
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
function isTailwindSource(p) {
|
|
@@ -118,18 +167,21 @@ async function ensureStyles() {
|
|
|
118
167
|
if (fs.existsSync(customAppCss)) {
|
|
119
168
|
if (!isTailwindSource(customAppCss)) {
|
|
120
169
|
await fsp.copyFile(customAppCss, dest);
|
|
170
|
+
injectThemeTokens(dest);
|
|
121
171
|
return;
|
|
122
172
|
}
|
|
123
173
|
}
|
|
124
174
|
if (fs.existsSync(customContentCss)) {
|
|
125
175
|
if (!isTailwindSource(customContentCss)) {
|
|
126
176
|
await fsp.copyFile(customContentCss, dest);
|
|
177
|
+
injectThemeTokens(dest);
|
|
127
178
|
return;
|
|
128
179
|
}
|
|
129
180
|
}
|
|
130
181
|
|
|
131
182
|
const css = `:root{--max-w:760px;--muted:#6b7280}*{box-sizing:border-box}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif;max-width:var(--max-w);margin:2rem auto;padding:0 1rem;line-height:1.6}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}.site-header,.site-footer{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:1rem 0;border-bottom:1px solid #e5e7eb}.site-footer{border-bottom:0;border-top:1px solid #e5e7eb;color:var(--muted)}.brand{font-weight:600}.content pre{background:#f6f8fa;padding:1rem;overflow:auto}.content code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;background:#f6f8fa;padding:.1rem .3rem;border-radius:4px}.tabs{display:flex;gap:.5rem;align-items:center;border-bottom:1px solid #e5e7eb;margin:.5rem 0}.tab{background:none;border:0;color:#374151;padding:.25rem .5rem;border-radius:.375rem;cursor:pointer}.tab:hover{color:#111827}.tab-active{color:#2563eb;border:1px solid #e5e7eb;border-bottom:0;background:#fff}.masonry{column-gap:1rem;column-count:1}@media(min-width:768px){.masonry{column-count:2}}@media(min-width:1024px){.masonry{column-count:3}}.masonry>*{break-inside:avoid;margin-bottom:1rem;display:block}[data-grid-variant=masonry]{column-gap:var(--grid-gap,1rem);column-count:var(--cols-base,1)}@media(min-width:768px){[data-grid-variant=masonry]{column-count:var(--cols-md,2)}}@media(min-width:1024px){[data-grid-variant=masonry]{column-count:var(--cols-lg,3)}}[data-grid-variant=masonry]>*{break-inside:avoid;margin-bottom:var(--grid-gap,1rem);display:block}[data-grid-variant=grid]{display:grid;grid-template-columns:repeat(var(--cols-base,1),minmax(0,1fr));gap:var(--grid-gap,1rem)}@media(min-width:768px){[data-grid-variant=grid]{grid-template-columns:repeat(var(--cols-md,2),minmax(0,1fr))}}@media(min-width:1024px){[data-grid-variant=grid]{grid-template-columns:repeat(var(--cols-lg,3),minmax(0,1fr))}}`;
|
|
132
183
|
await fsp.writeFile(dest, css, "utf8");
|
|
184
|
+
injectThemeTokens(dest);
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
module.exports = { ensureStyles };
|
package/lib/common.js
CHANGED
|
@@ -8,6 +8,23 @@ const CACHE_DIR = path.resolve('.cache/mdx');
|
|
|
8
8
|
const ASSETS_DIR = path.resolve('assets');
|
|
9
9
|
|
|
10
10
|
const BASE_PATH = String(process.env.CANOPY_BASE_PATH || '').replace(/\/$/, '');
|
|
11
|
+
let cachedAppearance = null;
|
|
12
|
+
|
|
13
|
+
function resolveThemeAppearance() {
|
|
14
|
+
if (cachedAppearance) return cachedAppearance;
|
|
15
|
+
cachedAppearance = 'light';
|
|
16
|
+
try {
|
|
17
|
+
const { loadCanopyTheme } = require('@canopy-iiif/app/ui/theme');
|
|
18
|
+
if (typeof loadCanopyTheme === 'function') {
|
|
19
|
+
const theme = loadCanopyTheme();
|
|
20
|
+
const appearance = theme && theme.appearance ? String(theme.appearance) : '';
|
|
21
|
+
if (appearance.toLowerCase() === 'dark') {
|
|
22
|
+
cachedAppearance = 'dark';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (_) {}
|
|
26
|
+
return cachedAppearance;
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
function readYamlConfigBaseUrl() {
|
|
13
30
|
try {
|
|
@@ -51,7 +68,9 @@ function htmlShell({ title, body, cssHref, scriptHref, headExtra }) {
|
|
|
51
68
|
const scriptTag = scriptHref ? `<script defer src="${scriptHref}"></script>` : '';
|
|
52
69
|
const extra = headExtra ? String(headExtra) : '';
|
|
53
70
|
const cssTag = cssHref ? `<link rel="stylesheet" href="${cssHref}">` : '';
|
|
54
|
-
|
|
71
|
+
const appearance = resolveThemeAppearance();
|
|
72
|
+
const htmlClass = appearance === 'dark' ? ' class="dark"' : '';
|
|
73
|
+
return `<!doctype html><html lang="en"${htmlClass}><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>${title}</title>${extra}${cssTag}${scriptTag}</head><body>${body}</body></html>`;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
function withBase(href) {
|
|
@@ -111,16 +130,19 @@ function absoluteUrl(p) {
|
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
|
|
114
|
-
// Apply BASE_PATH to
|
|
133
|
+
// Apply BASE_PATH to key URL-bearing attributes (href/src/action/formaction) in an HTML string.
|
|
115
134
|
function applyBaseToHtml(html) {
|
|
116
135
|
if (!BASE_PATH) return html;
|
|
117
136
|
try {
|
|
118
137
|
const out = String(html || '');
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
: `/${BASE_PATH.replace(/\/$/, '')}`;
|
|
138
|
+
const baseRaw = BASE_PATH.startsWith('/') ? BASE_PATH : `/${BASE_PATH}`;
|
|
139
|
+
const normalizedBase = baseRaw.replace(/\/$/, '');
|
|
122
140
|
if (!normalizedBase || normalizedBase === '/') return out;
|
|
123
|
-
|
|
141
|
+
|
|
142
|
+
const attrPattern = '(?:href|src|action|formaction)';
|
|
143
|
+
const pathPattern = "\\/(?!\\/)[^'\"\\s<]*";
|
|
144
|
+
const pattern = new RegExp(`(${attrPattern})=(["'])((${pathPattern}))\\2`, 'g');
|
|
145
|
+
|
|
124
146
|
return out.replace(pattern, (match, attr, quote, path) => {
|
|
125
147
|
if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {
|
|
126
148
|
return match;
|
|
@@ -6,6 +6,49 @@ import {
|
|
|
6
6
|
SearchFiltersDialog,
|
|
7
7
|
} from "@canopy-iiif/app/ui";
|
|
8
8
|
|
|
9
|
+
function readBasePath() {
|
|
10
|
+
const normalize = (val) => {
|
|
11
|
+
const raw = typeof val === "string" ? val.trim() : "";
|
|
12
|
+
if (!raw) return "";
|
|
13
|
+
const withLead = raw.startsWith("/") ? raw : `/${raw}`;
|
|
14
|
+
return withLead.replace(/\/+$/, "");
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
if (typeof window !== "undefined" && window.CANOPY_BASE_PATH != null) {
|
|
18
|
+
const fromWindow = normalize(window.CANOPY_BASE_PATH);
|
|
19
|
+
if (fromWindow) return fromWindow;
|
|
20
|
+
}
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
try {
|
|
23
|
+
if (typeof globalThis !== "undefined" && globalThis.CANOPY_BASE_PATH != null) {
|
|
24
|
+
const fromGlobal = normalize(globalThis.CANOPY_BASE_PATH);
|
|
25
|
+
if (fromGlobal) return fromGlobal;
|
|
26
|
+
}
|
|
27
|
+
} catch (_) {}
|
|
28
|
+
try {
|
|
29
|
+
if (typeof process !== "undefined" && process.env && process.env.CANOPY_BASE_PATH) {
|
|
30
|
+
const fromEnv = normalize(process.env.CANOPY_BASE_PATH);
|
|
31
|
+
if (fromEnv) return fromEnv;
|
|
32
|
+
}
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function withBasePath(href) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = typeof href === "string" ? href.trim() : "";
|
|
40
|
+
if (!raw) return href;
|
|
41
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(raw)) return raw;
|
|
42
|
+
if (!raw.startsWith("/")) return raw;
|
|
43
|
+
const base = readBasePath();
|
|
44
|
+
if (!base || base === "/") return raw;
|
|
45
|
+
if (raw === base || raw.startsWith(`${base}/`)) return raw;
|
|
46
|
+
return `${base}${raw}`;
|
|
47
|
+
} catch (_) {
|
|
48
|
+
return href;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
9
52
|
// Lightweight IndexedDB utilities (no deps) with defensive guards
|
|
10
53
|
function hasIDB() {
|
|
11
54
|
try {
|
|
@@ -116,6 +159,7 @@ function createSearchStore() {
|
|
|
116
159
|
facetsDocsMap: {},
|
|
117
160
|
filters: {},
|
|
118
161
|
activeFilterCount: 0,
|
|
162
|
+
annotationsEnabled: false,
|
|
119
163
|
};
|
|
120
164
|
const listeners = new Set();
|
|
121
165
|
function notify() {
|
|
@@ -209,22 +253,34 @@ function createSearchStore() {
|
|
|
209
253
|
counts = {};
|
|
210
254
|
}
|
|
211
255
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
: base.filter(
|
|
216
|
-
(r) => String(r.type).toLowerCase() === String(type).toLowerCase()
|
|
217
|
-
);
|
|
218
|
-
if (shouldFilterWorks && allowed) {
|
|
219
|
-
results = results.filter((r) => allowed.has(r && r.__docIndex));
|
|
220
|
-
}
|
|
256
|
+
const annotationMatches = base.filter((r) => r && r.annotation);
|
|
257
|
+
if (annotationMatches.length)
|
|
258
|
+
counts.annotation = annotationMatches.length;
|
|
221
259
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
const typeLower = String(type).toLowerCase();
|
|
261
|
+
if (typeLower === "annotation") {
|
|
262
|
+
results = annotationMatches.map((r) => ({
|
|
263
|
+
...r,
|
|
264
|
+
type: "annotation",
|
|
265
|
+
originalType: r ? r.type : undefined,
|
|
266
|
+
}));
|
|
267
|
+
totalForType = annotationMatches.length;
|
|
268
|
+
} else {
|
|
269
|
+
results =
|
|
270
|
+
typeLower === "all"
|
|
271
|
+
? base
|
|
272
|
+
: base.filter((r) => String(r.type).toLowerCase() === typeLower);
|
|
273
|
+
if (shouldFilterWorks && allowed) {
|
|
274
|
+
results = results.filter((r) => allowed.has(r && r.__docIndex));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typeLower !== "all") {
|
|
278
|
+
try {
|
|
279
|
+
totalForType = records.filter(
|
|
280
|
+
(r) => String(r.type).toLowerCase() === typeLower
|
|
281
|
+
).length;
|
|
282
|
+
} catch (_) {}
|
|
283
|
+
}
|
|
228
284
|
}
|
|
229
285
|
}
|
|
230
286
|
const {facetsDocsMap: _fMap, ...publicState} = state;
|
|
@@ -390,6 +446,7 @@ function createSearchStore() {
|
|
|
390
446
|
// Try to load meta for cache-busting and tab order; fall back to hash of JSON
|
|
391
447
|
let version = "";
|
|
392
448
|
let tabsOrder = [];
|
|
449
|
+
let annotationsAssetPath = "";
|
|
393
450
|
try {
|
|
394
451
|
const meta = await fetch("./api/index.json")
|
|
395
452
|
.then((r) => (r && r.ok ? r.json() : null))
|
|
@@ -403,11 +460,17 @@ function createSearchStore() {
|
|
|
403
460
|
? meta.search.tabs.order
|
|
404
461
|
: [];
|
|
405
462
|
tabsOrder = ord.map((s) => String(s)).filter(Boolean);
|
|
463
|
+
const annotationsAsset =
|
|
464
|
+
meta &&
|
|
465
|
+
meta.search &&
|
|
466
|
+
meta.search.assets &&
|
|
467
|
+
meta.search.assets.annotations;
|
|
468
|
+
if (annotationsAsset && annotationsAsset.path) {
|
|
469
|
+
annotationsAssetPath = String(annotationsAsset.path);
|
|
470
|
+
}
|
|
406
471
|
} catch (_) {}
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
(version ? `?v=${encodeURIComponent(version)}` : "")
|
|
410
|
-
);
|
|
472
|
+
const suffix = version ? `?v=${encodeURIComponent(version)}` : "";
|
|
473
|
+
const res = await fetch(`./api/search-index.json${suffix}`);
|
|
411
474
|
const text = await res.text();
|
|
412
475
|
const parsed = (() => {
|
|
413
476
|
try {
|
|
@@ -416,16 +479,89 @@ function createSearchStore() {
|
|
|
416
479
|
return [];
|
|
417
480
|
}
|
|
418
481
|
})();
|
|
419
|
-
const
|
|
482
|
+
const indexRecords = Array.isArray(parsed)
|
|
420
483
|
? parsed
|
|
421
484
|
: parsed && parsed.records
|
|
422
485
|
? parsed.records
|
|
423
486
|
: [];
|
|
424
|
-
|
|
487
|
+
|
|
488
|
+
let displayRecords = [];
|
|
489
|
+
try {
|
|
490
|
+
const displayRes = await fetch(`./api/search-records.json${suffix}`);
|
|
491
|
+
if (displayRes && displayRes.ok) {
|
|
492
|
+
const displayJson = await displayRes.json().catch(() => []);
|
|
493
|
+
displayRecords = Array.isArray(displayJson)
|
|
494
|
+
? displayJson
|
|
495
|
+
: displayJson && Array.isArray(displayJson.records)
|
|
496
|
+
? displayJson.records
|
|
497
|
+
: [];
|
|
498
|
+
}
|
|
499
|
+
} catch (_) {}
|
|
500
|
+
|
|
501
|
+
const displayMap = new Map();
|
|
502
|
+
displayRecords.forEach((rec) => {
|
|
503
|
+
if (!rec || typeof rec !== "object") return;
|
|
504
|
+
const key = rec.id ? String(rec.id) : rec.href ? String(rec.href) : "";
|
|
505
|
+
if (!key) return;
|
|
506
|
+
displayMap.set(key, rec);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
let annotationsRecords = [];
|
|
510
|
+
if (annotationsAssetPath) {
|
|
511
|
+
try {
|
|
512
|
+
const annotationsUrl = `./api/${annotationsAssetPath.replace(/^\/+/, '')}` + (version ? `?v=${encodeURIComponent(version)}` : "");
|
|
513
|
+
const annotationsRes = await fetch(annotationsUrl);
|
|
514
|
+
if (annotationsRes && annotationsRes.ok) {
|
|
515
|
+
const annotationsJson = await annotationsRes.json().catch(() => []);
|
|
516
|
+
annotationsRecords = Array.isArray(annotationsJson)
|
|
517
|
+
? annotationsJson
|
|
518
|
+
: annotationsJson && Array.isArray(annotationsJson.records)
|
|
519
|
+
? annotationsJson.records
|
|
520
|
+
: [];
|
|
521
|
+
}
|
|
522
|
+
} catch (_) {}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const annotationsMap = new Map();
|
|
526
|
+
annotationsRecords.forEach((rec, idx) => {
|
|
527
|
+
if (!rec || typeof rec !== "object") return;
|
|
528
|
+
const key = rec.id ? String(rec.id) : String(idx);
|
|
529
|
+
const text = rec.annotation ? String(rec.annotation) : rec.text ? String(rec.text) : "";
|
|
530
|
+
if (!key || !text) return;
|
|
531
|
+
annotationsMap.set(key, text);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const data = indexRecords.map((rec, i) => {
|
|
535
|
+
const key = rec && rec.id ? String(rec.id) : String(i);
|
|
536
|
+
const display = key ? displayMap.get(key) : null;
|
|
537
|
+
const merged = { ...(display || {}), ...(rec || {}), __docIndex: i };
|
|
538
|
+
if (!merged.id && key) merged.id = key;
|
|
539
|
+
if (!merged.href && display && display.href) merged.href = String(display.href);
|
|
540
|
+
if (merged.href) merged.href = withBasePath(merged.href);
|
|
541
|
+
if (!merged.title) merged.title = merged.href || "";
|
|
542
|
+
if (!Array.isArray(merged.metadata)) {
|
|
543
|
+
const meta = Array.isArray(rec && rec.metadata) ? rec.metadata : [];
|
|
544
|
+
merged.metadata = meta;
|
|
545
|
+
}
|
|
546
|
+
if (annotationsMap.has(key) && !merged.annotation)
|
|
547
|
+
merged.annotation = annotationsMap.get(key);
|
|
548
|
+
return merged;
|
|
549
|
+
});
|
|
425
550
|
if (!version)
|
|
426
551
|
version = (parsed && parsed.version) || (await sha256Hex(text));
|
|
427
552
|
|
|
428
553
|
const idx = new Flex.Index({tokenize: "forward"});
|
|
554
|
+
const collectSearchText = (rec) => {
|
|
555
|
+
const parts = [];
|
|
556
|
+
if (rec && rec.title) parts.push(String(rec.title));
|
|
557
|
+
const metadata = Array.isArray(rec && rec.metadata) ? rec.metadata : [];
|
|
558
|
+
for (const value of metadata) {
|
|
559
|
+
if (value) parts.push(String(value));
|
|
560
|
+
}
|
|
561
|
+
if (rec && rec.summary) parts.push(String(rec.summary));
|
|
562
|
+
if (rec && rec.annotation) parts.push(String(rec.annotation));
|
|
563
|
+
return parts.join(" ").trim();
|
|
564
|
+
};
|
|
429
565
|
let hydrated = false;
|
|
430
566
|
const t0 =
|
|
431
567
|
typeof performance !== "undefined" && performance.now
|
|
@@ -455,7 +591,8 @@ function createSearchStore() {
|
|
|
455
591
|
if (!hydrated) {
|
|
456
592
|
data.forEach((rec, i) => {
|
|
457
593
|
try {
|
|
458
|
-
|
|
594
|
+
const payload = collectSearchText(rec);
|
|
595
|
+
idx.add(i, payload);
|
|
459
596
|
} catch (_) {}
|
|
460
597
|
});
|
|
461
598
|
try {
|
|
@@ -521,6 +658,8 @@ function createSearchStore() {
|
|
|
521
658
|
const ts = Array.from(
|
|
522
659
|
new Set(data.map((r) => String((r && r.type) || "page")))
|
|
523
660
|
);
|
|
661
|
+
if (annotationsRecords.length && !ts.includes("annotation"))
|
|
662
|
+
ts.push("annotation");
|
|
524
663
|
const order =
|
|
525
664
|
Array.isArray(tabsOrder) && tabsOrder.length
|
|
526
665
|
? tabsOrder
|
|
@@ -548,7 +687,13 @@ function createSearchStore() {
|
|
|
548
687
|
set({type: def});
|
|
549
688
|
}
|
|
550
689
|
} catch (_) {}
|
|
551
|
-
set({
|
|
690
|
+
set({
|
|
691
|
+
index: idx,
|
|
692
|
+
records: data,
|
|
693
|
+
types: ts,
|
|
694
|
+
loading: false,
|
|
695
|
+
annotationsEnabled: annotationsRecords.length > 0,
|
|
696
|
+
});
|
|
552
697
|
await hydrateFacets();
|
|
553
698
|
} catch (_) {
|
|
554
699
|
set({loading: false});
|
|
@@ -609,10 +754,17 @@ function useStore() {
|
|
|
609
754
|
}
|
|
610
755
|
|
|
611
756
|
function ResultsMount(props = {}) {
|
|
612
|
-
const {results, type, loading} = useStore();
|
|
757
|
+
const {results, type, loading, query} = useStore();
|
|
613
758
|
if (loading) return <div className="text-slate-600">Loading…</div>;
|
|
614
759
|
const layout = (props && props.layout) || "grid";
|
|
615
|
-
return
|
|
760
|
+
return (
|
|
761
|
+
<SearchResultsUI
|
|
762
|
+
results={results}
|
|
763
|
+
type={type}
|
|
764
|
+
layout={layout}
|
|
765
|
+
query={query}
|
|
766
|
+
/>
|
|
767
|
+
);
|
|
616
768
|
}
|
|
617
769
|
function TabsMount() {
|
|
618
770
|
const {
|
|
@@ -79,17 +79,99 @@ async function loadRecords() {
|
|
|
79
79
|
try {
|
|
80
80
|
const base = rootBase();
|
|
81
81
|
let version = '';
|
|
82
|
+
let annotationsAssetPath = '';
|
|
82
83
|
try {
|
|
83
84
|
const meta = await fetch(`${base}/api/index.json`).then((r) => (r && r.ok ? r.json() : null)).catch(() => null);
|
|
84
85
|
if (meta && typeof meta.version === 'string') version = meta.version;
|
|
86
|
+
const annotationsAsset =
|
|
87
|
+
meta &&
|
|
88
|
+
meta.search &&
|
|
89
|
+
meta.search.assets &&
|
|
90
|
+
meta.search.assets.annotations;
|
|
91
|
+
if (annotationsAsset && annotationsAsset.path) {
|
|
92
|
+
annotationsAssetPath = String(annotationsAsset.path);
|
|
93
|
+
}
|
|
85
94
|
} catch (_) {}
|
|
86
95
|
const suffix = version ? `?v=${encodeURIComponent(version)}` : '';
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
const indexUrl = `${base}/api/search-index.json${suffix}`;
|
|
97
|
+
const recordsUrl = `${base}/api/search-records.json${suffix}`;
|
|
98
|
+
const [indexRes, displayRes] = await Promise.all([
|
|
99
|
+
fetch(indexUrl).catch(() => null),
|
|
100
|
+
fetch(recordsUrl).catch(() => null),
|
|
101
|
+
]);
|
|
102
|
+
let indexRecords = [];
|
|
103
|
+
if (indexRes && indexRes.ok) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = await indexRes.json();
|
|
106
|
+
indexRecords = Array.isArray(raw)
|
|
107
|
+
? raw
|
|
108
|
+
: raw && Array.isArray(raw.records)
|
|
109
|
+
? raw.records
|
|
110
|
+
: [];
|
|
111
|
+
} catch (_) {
|
|
112
|
+
indexRecords = [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let displayRecords = [];
|
|
116
|
+
if (displayRes && displayRes.ok) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = await displayRes.json();
|
|
119
|
+
displayRecords = Array.isArray(raw)
|
|
120
|
+
? raw
|
|
121
|
+
: raw && Array.isArray(raw.records)
|
|
122
|
+
? raw.records
|
|
123
|
+
: [];
|
|
124
|
+
} catch (_) {
|
|
125
|
+
displayRecords = [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let annotationsRecords = [];
|
|
129
|
+
if (annotationsAssetPath) {
|
|
130
|
+
try {
|
|
131
|
+
const annotationsUrl = `${base}/api/${annotationsAssetPath.replace(/^\/+/, '')}${suffix}`;
|
|
132
|
+
const annotationsRes = await fetch(annotationsUrl).catch(() => null);
|
|
133
|
+
if (annotationsRes && annotationsRes.ok) {
|
|
134
|
+
const raw = await annotationsRes.json().catch(() => []);
|
|
135
|
+
annotationsRecords = Array.isArray(raw)
|
|
136
|
+
? raw
|
|
137
|
+
: raw && Array.isArray(raw.records)
|
|
138
|
+
? raw.records
|
|
139
|
+
: [];
|
|
140
|
+
}
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
}
|
|
143
|
+
if (!indexRecords.length && displayRecords.length) {
|
|
144
|
+
return displayRecords;
|
|
145
|
+
}
|
|
146
|
+
const displayMap = new Map();
|
|
147
|
+
displayRecords.forEach((rec) => {
|
|
148
|
+
if (!rec || typeof rec !== 'object') return;
|
|
149
|
+
const key = rec.id ? String(rec.id) : rec.href ? String(rec.href) : '';
|
|
150
|
+
if (!key) return;
|
|
151
|
+
displayMap.set(key, rec);
|
|
152
|
+
});
|
|
153
|
+
const annotationsMap = new Map();
|
|
154
|
+
annotationsRecords.forEach((rec, idx) => {
|
|
155
|
+
if (!rec || typeof rec !== 'object') return;
|
|
156
|
+
const key = rec.id ? String(rec.id) : String(idx);
|
|
157
|
+
const text = rec.annotation ? String(rec.annotation) : rec.text ? String(rec.text) : '';
|
|
158
|
+
if (!key || !text) return;
|
|
159
|
+
annotationsMap.set(key, text);
|
|
160
|
+
});
|
|
161
|
+
return indexRecords.map((rec, idx) => {
|
|
162
|
+
const key = rec && rec.id ? String(rec.id) : String(idx);
|
|
163
|
+
const display = key ? displayMap.get(key) : null;
|
|
164
|
+
const merged = { ...(display || {}), ...(rec || {}) };
|
|
165
|
+
if (!merged.id && key) merged.id = key;
|
|
166
|
+
if (!merged.href && display && display.href) merged.href = String(display.href);
|
|
167
|
+
if (!Array.isArray(merged.metadata)) {
|
|
168
|
+
const meta = Array.isArray(rec && rec.metadata) ? rec.metadata : [];
|
|
169
|
+
merged.metadata = meta;
|
|
170
|
+
}
|
|
171
|
+
if (annotationsMap.has(key) && !merged.annotation)
|
|
172
|
+
merged.annotation = annotationsMap.get(key);
|
|
173
|
+
return merged;
|
|
174
|
+
});
|
|
93
175
|
} catch (_) {
|
|
94
176
|
return [];
|
|
95
177
|
}
|
|
@@ -129,22 +211,43 @@ function renderList(list, records, groupOrder) {
|
|
|
129
211
|
header.style.cssText = 'padding:6px 12px;font-weight:600;color:#374151';
|
|
130
212
|
list.appendChild(header);
|
|
131
213
|
const entries = groups.get(key) || [];
|
|
132
|
-
entries.forEach((record
|
|
133
|
-
const
|
|
214
|
+
entries.forEach((record) => {
|
|
215
|
+
const href = withBase(String(record && record.href || ''));
|
|
216
|
+
const item = document.createElement('a');
|
|
134
217
|
item.setAttribute('data-canopy-item', '');
|
|
218
|
+
item.href = href;
|
|
135
219
|
item.tabIndex = 0;
|
|
136
|
-
item.
|
|
220
|
+
item.className = 'canopy-card canopy-card--teaser';
|
|
221
|
+
item.style.cssText = 'display:flex;gap:12px;padding:8px 12px;text-decoration:none;color:#030712;border-radius:8px;align-items:center;outline:none;';
|
|
222
|
+
|
|
137
223
|
const showThumb = String(record && record.type || '') === 'work' && record && record.thumbnail;
|
|
138
224
|
if (showThumb) {
|
|
225
|
+
const media = document.createElement('div');
|
|
226
|
+
media.style.cssText = 'flex:0 0 48px;height:48px;border-radius:6px;overflow:hidden;background:#f1f5f9;display:flex;align-items:center;justify-content:center;';
|
|
139
227
|
const img = document.createElement('img');
|
|
140
228
|
img.src = record.thumbnail;
|
|
141
229
|
img.alt = '';
|
|
142
|
-
img.
|
|
143
|
-
|
|
230
|
+
img.loading = 'lazy';
|
|
231
|
+
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
|
232
|
+
media.appendChild(img);
|
|
233
|
+
item.appendChild(media);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const textWrap = document.createElement('div');
|
|
237
|
+
textWrap.style.cssText = 'display:flex;flex-direction:column;gap:2px;min-width:0;';
|
|
238
|
+
const title = document.createElement('span');
|
|
239
|
+
title.textContent = record.title || record.href || '';
|
|
240
|
+
title.style.cssText = 'font-weight:600;font-size:0.95rem;line-height:1.3;color:#111827;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
241
|
+
textWrap.appendChild(title);
|
|
242
|
+
const meta = Array.isArray(record && record.metadata) ? record.metadata : [];
|
|
243
|
+
if (meta.length) {
|
|
244
|
+
const metaLine = document.createElement('span');
|
|
245
|
+
metaLine.textContent = meta.slice(0, 2).join(' • ');
|
|
246
|
+
metaLine.style.cssText = 'font-size:0.8rem;color:#475569;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
247
|
+
textWrap.appendChild(metaLine);
|
|
144
248
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
item.appendChild(span);
|
|
249
|
+
item.appendChild(textWrap);
|
|
250
|
+
|
|
148
251
|
item.onmouseenter = () => { item.style.background = '#f8fafc'; };
|
|
149
252
|
item.onmouseleave = () => { item.style.background = 'transparent'; };
|
|
150
253
|
item.onfocus = () => {
|
|
@@ -152,9 +255,6 @@ function renderList(list, records, groupOrder) {
|
|
|
152
255
|
try { item.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
|
153
256
|
};
|
|
154
257
|
item.onblur = () => { item.style.background = 'transparent'; };
|
|
155
|
-
item.onclick = () => {
|
|
156
|
-
try { window.location.href = withBase(String(record.href || '')); } catch (_) {}
|
|
157
|
-
};
|
|
158
258
|
list.appendChild(item);
|
|
159
259
|
});
|
|
160
260
|
});
|
|
@@ -278,8 +378,15 @@ async function attachSearchForm(host) {
|
|
|
278
378
|
for (let i = 0; i < records.length; i += 1) {
|
|
279
379
|
const record = records[i];
|
|
280
380
|
const title = String(record && record.title || '');
|
|
281
|
-
|
|
282
|
-
|
|
381
|
+
const parts = [toLower(title)];
|
|
382
|
+
const metadata = Array.isArray(record && record.metadata) ? record.metadata : [];
|
|
383
|
+
for (const value of metadata) {
|
|
384
|
+
parts.push(toLower(value));
|
|
385
|
+
}
|
|
386
|
+
if (record && record.summary) parts.push(toLower(record.summary));
|
|
387
|
+
if (record && record.annotation) parts.push(toLower(record.annotation));
|
|
388
|
+
const haystack = parts.join(' ');
|
|
389
|
+
if (haystack.includes(q)) out.push(record);
|
|
283
390
|
if (out.length >= maxResults) break;
|
|
284
391
|
}
|
|
285
392
|
render(out);
|