@canopy-iiif/app 0.8.4 → 0.8.6
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 +361 -84
- package/lib/build/mdx.js +16 -8
- package/lib/build/pages.js +2 -1
- 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 +239 -97
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +129 -72
- package/ui/dist/server.mjs.map +4 -4
- 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/search/search.js
CHANGED
|
@@ -173,7 +173,7 @@ async function buildSearchPage() {
|
|
|
173
173
|
}
|
|
174
174
|
} catch (_) {}
|
|
175
175
|
let html = htmlShell({ title: 'Search', body, cssHref: null, scriptHref: jsRel, headExtra });
|
|
176
|
-
try { html = require('
|
|
176
|
+
try { html = require('../common').applyBaseToHtml(html); } catch (_) {}
|
|
177
177
|
await fsp.writeFile(outPath, html, 'utf8');
|
|
178
178
|
console.log('Search: Built', path.relative(process.cwd(), outPath));
|
|
179
179
|
} catch (e) {
|
|
@@ -185,14 +185,66 @@ async function buildSearchPage() {
|
|
|
185
185
|
function toSafeString(val, defaultValue = '') {
|
|
186
186
|
try { return String(val == null ? defaultValue : val); } catch (_) { return defaultValue; }
|
|
187
187
|
}
|
|
188
|
-
|
|
188
|
+
|
|
189
|
+
function sanitizeMetadataValues(list) {
|
|
190
|
+
const arr = Array.isArray(list) ? list : [];
|
|
191
|
+
const out = [];
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
for (const val of arr) {
|
|
194
|
+
if (val && typeof val === 'object' && Array.isArray(val.values)) {
|
|
195
|
+
for (const v of val.values) {
|
|
196
|
+
const str = toSafeString(v, '').trim();
|
|
197
|
+
if (!str) continue;
|
|
198
|
+
const clipped = str.length > 500 ? str.slice(0, 500) + '…' : str;
|
|
199
|
+
if (seen.has(clipped)) continue;
|
|
200
|
+
seen.add(clipped);
|
|
201
|
+
out.push(clipped);
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const str = toSafeString(val, '').trim();
|
|
206
|
+
if (!str) continue;
|
|
207
|
+
const clipped = str.length > 500 ? str.slice(0, 500) + '…' : str;
|
|
208
|
+
if (seen.has(clipped)) continue;
|
|
209
|
+
seen.add(clipped);
|
|
210
|
+
out.push(clipped);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sanitizeRecordForIndex(r) {
|
|
189
216
|
const title = toSafeString(r && r.title, '');
|
|
190
|
-
const hrefRaw = toSafeString(r && r.href, '');
|
|
191
|
-
const href = rootRelativeHref(hrefRaw);
|
|
192
217
|
const type = toSafeString(r && r.type, 'page');
|
|
193
|
-
const thumbnail = toSafeString(r && r.thumbnail, '');
|
|
194
218
|
const safeTitle = title.length > 300 ? title.slice(0, 300) + '…' : title;
|
|
195
|
-
const out = { title: safeTitle,
|
|
219
|
+
const out = { title: safeTitle, type };
|
|
220
|
+
const metadataSource =
|
|
221
|
+
(r && r.metadataValues) ||
|
|
222
|
+
(r && r.searchMetadataValues) ||
|
|
223
|
+
(r && r.search && r.search.metadata) ||
|
|
224
|
+
[];
|
|
225
|
+
const metadata = sanitizeMetadataValues(metadataSource);
|
|
226
|
+
if (metadata.length) out.metadata = metadata;
|
|
227
|
+
const summaryVal = toSafeString(
|
|
228
|
+
(r && r.summaryValue) ||
|
|
229
|
+
(r && r.searchSummary) ||
|
|
230
|
+
(r && r.search && r.search.summary),
|
|
231
|
+
''
|
|
232
|
+
).trim();
|
|
233
|
+
if (summaryVal) {
|
|
234
|
+
const clipped = summaryVal.length > 1000 ? summaryVal.slice(0, 1000) + '…' : summaryVal;
|
|
235
|
+
out.summary = clipped;
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sanitizeRecordForDisplay(r) {
|
|
241
|
+
const base = sanitizeRecordForIndex(r);
|
|
242
|
+
const out = { ...base };
|
|
243
|
+
if (out.metadata) delete out.metadata;
|
|
244
|
+
if (out.summary) out.summary = toSafeString(out.summary, '');
|
|
245
|
+
const hrefRaw = toSafeString(r && r.href, '');
|
|
246
|
+
out.href = rootRelativeHref(hrefRaw);
|
|
247
|
+
const thumbnail = toSafeString(r && r.thumbnail, '');
|
|
196
248
|
if (thumbnail) out.thumbnail = thumbnail;
|
|
197
249
|
// Preserve optional thumbnail dimensions for aspect ratio calculations in the UI
|
|
198
250
|
try {
|
|
@@ -204,28 +256,76 @@ function sanitizeRecord(r) {
|
|
|
204
256
|
return out;
|
|
205
257
|
}
|
|
206
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Write search datasets consumed by the runtime layers.
|
|
261
|
+
*
|
|
262
|
+
* Outputs:
|
|
263
|
+
* - search-index.json: compact payload (title/href/type/metadata + stable id) for FlexSearch
|
|
264
|
+
* - search-records.json: richer display data (thumbnail/dimensions + id) for UI rendering
|
|
265
|
+
*/
|
|
207
266
|
async function writeSearchIndex(records) {
|
|
208
267
|
const apiDir = path.join(OUT_DIR, 'api');
|
|
209
268
|
ensureDirSync(apiDir);
|
|
210
269
|
const idxPath = path.join(apiDir, 'search-index.json');
|
|
211
270
|
const list = Array.isArray(records) ? records : [];
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
271
|
+
const indexRecords = list.map(sanitizeRecordForIndex);
|
|
272
|
+
const displayRecords = list.map(sanitizeRecordForDisplay);
|
|
273
|
+
for (let i = 0; i < indexRecords.length; i += 1) {
|
|
274
|
+
const id = String(i);
|
|
275
|
+
if (indexRecords[i]) indexRecords[i].id = id;
|
|
276
|
+
if (displayRecords[i]) displayRecords[i].id = id;
|
|
277
|
+
if (displayRecords[i] && !displayRecords[i].href) {
|
|
278
|
+
const original = list[i];
|
|
279
|
+
const href = original && original.href ? rootRelativeHref(String(original.href)) : '';
|
|
280
|
+
if (href) displayRecords[i].href = href;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const annotationsPath = path.join(apiDir, 'search-index-annotations.json');
|
|
284
|
+
const annotationRecords = [];
|
|
285
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
286
|
+
const raw = list[i];
|
|
287
|
+
const annotationVal = toSafeString(
|
|
288
|
+
(raw && raw.annotationValue) ||
|
|
289
|
+
(raw && raw.searchAnnotation) ||
|
|
290
|
+
(raw && raw.search && raw.search.annotation),
|
|
291
|
+
''
|
|
292
|
+
).trim();
|
|
293
|
+
if (!annotationVal) continue;
|
|
294
|
+
annotationRecords.push({ id: String(i), annotation: annotationVal });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const indexJson = JSON.stringify(indexRecords);
|
|
298
|
+
const approxBytes = Buffer.byteLength(indexJson, 'utf8');
|
|
215
299
|
if (approxBytes > 10 * 1024 * 1024) {
|
|
216
300
|
console.warn('Search: index size is large (', Math.round(approxBytes / (1024 * 1024)), 'MB ). Consider narrowing sources.');
|
|
217
301
|
}
|
|
218
|
-
await fsp.writeFile(idxPath,
|
|
302
|
+
await fsp.writeFile(idxPath, indexJson, 'utf8');
|
|
303
|
+
|
|
304
|
+
const displayPath = path.join(apiDir, 'search-records.json');
|
|
305
|
+
const displayJson = JSON.stringify(displayRecords);
|
|
306
|
+
const displayBytes = Buffer.byteLength(displayJson, 'utf8');
|
|
307
|
+
await fsp.writeFile(displayPath, displayJson, 'utf8');
|
|
308
|
+
let annotationsBytes = 0;
|
|
309
|
+
if (annotationRecords.length) {
|
|
310
|
+
const annotationsJson = JSON.stringify(annotationRecords);
|
|
311
|
+
annotationsBytes = Buffer.byteLength(annotationsJson, 'utf8');
|
|
312
|
+
await fsp.writeFile(annotationsPath, annotationsJson, 'utf8');
|
|
313
|
+
} else {
|
|
314
|
+
try {
|
|
315
|
+
if (fs.existsSync(annotationsPath)) await fsp.unlink(annotationsPath);
|
|
316
|
+
} catch (_) {}
|
|
317
|
+
}
|
|
219
318
|
// Also write a small metadata file with a stable version hash for cache-busting and IDB keying
|
|
220
319
|
try {
|
|
221
|
-
const version = crypto.createHash('sha256').update(
|
|
320
|
+
const version = crypto.createHash('sha256').update(indexJson).digest('hex');
|
|
222
321
|
// Read optional search tabs order from canopy.yml
|
|
223
322
|
let tabsOrder = [];
|
|
224
323
|
try {
|
|
225
324
|
const yaml = require('js-yaml');
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
325
|
+
const common = require('../common');
|
|
326
|
+
const cfgPath = common.path.resolve(process.env.CANOPY_CONFIG || 'canopy.yml');
|
|
327
|
+
if (common.fs.existsSync(cfgPath)) {
|
|
328
|
+
const raw = common.fs.readFileSync(cfgPath, 'utf8');
|
|
229
329
|
const data = yaml.load(raw) || {};
|
|
230
330
|
const searchCfg = data && data.search ? data.search : {};
|
|
231
331
|
const tabs = searchCfg && searchCfg.tabs ? searchCfg.tabs : {};
|
|
@@ -235,17 +335,25 @@ async function writeSearchIndex(records) {
|
|
|
235
335
|
} catch (_) {}
|
|
236
336
|
const meta = {
|
|
237
337
|
version,
|
|
238
|
-
records:
|
|
338
|
+
records: indexRecords.length,
|
|
239
339
|
bytes: approxBytes,
|
|
240
340
|
updatedAt: new Date().toISOString(),
|
|
241
341
|
// Expose optional search config to the client runtime
|
|
242
|
-
search: {
|
|
342
|
+
search: {
|
|
343
|
+
tabs: { order: tabsOrder },
|
|
344
|
+
assets: {
|
|
345
|
+
display: { path: 'search-records.json', bytes: displayBytes },
|
|
346
|
+
...(annotationRecords.length
|
|
347
|
+
? { annotations: { path: 'search-index-annotations.json', bytes: annotationsBytes } }
|
|
348
|
+
: {}),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
243
351
|
};
|
|
244
352
|
const metaPath = path.join(apiDir, 'index.json');
|
|
245
353
|
await fsp.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
246
354
|
try {
|
|
247
355
|
const { logLine } = require('./log');
|
|
248
|
-
logLine(`✓ Search index version ${version.slice(0, 8)} (${
|
|
356
|
+
logLine(`✓ Search index version ${version.slice(0, 8)} (${indexRecords.length} records)`, 'cyan');
|
|
249
357
|
} catch (_) {}
|
|
250
358
|
// Propagate version into IIIF cache index for a single, shared build identifier
|
|
251
359
|
try {
|
|
@@ -263,7 +371,11 @@ async function writeSearchIndex(records) {
|
|
|
263
371
|
|
|
264
372
|
// Compatibility: keep ensureResultTemplate as a no-op builder (template unused by React search)
|
|
265
373
|
async function ensureResultTemplate() {
|
|
266
|
-
try {
|
|
374
|
+
try {
|
|
375
|
+
const { path } = require('../common');
|
|
376
|
+
const p = path.join(OUT_DIR, 'search-result.html');
|
|
377
|
+
await fsp.writeFile(p, '', 'utf8');
|
|
378
|
+
} catch (_) {}
|
|
267
379
|
}
|
|
268
380
|
|
|
269
381
|
module.exports = { ensureSearchRuntime, ensureResultTemplate, buildSearchPage, writeSearchIndex };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canopy-iiif/app",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mat Jordan <mat@northwestern.edu>",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./ui/styles/index.css": "./ui/styles/index.css",
|
|
15
15
|
"./ui/canopy-iiif-plugin": "./ui/tailwind-canopy-iiif-plugin.js",
|
|
16
16
|
"./ui/canopy-iiif-preset": "./ui/tailwind-canopy-iiif-preset.js",
|
|
17
|
+
"./ui/theme": "./ui/theme.js",
|
|
17
18
|
"./lib/components/*": "./lib/components/*",
|
|
18
19
|
"./head": "./lib/head.js",
|
|
19
20
|
"./orchestrator": {
|
|
@@ -28,12 +29,14 @@
|
|
|
28
29
|
"ui/styles/**",
|
|
29
30
|
"ui/tailwind-canopy-iiif-plugin.js",
|
|
30
31
|
"ui/tailwind-canopy-iiif-preset.js",
|
|
32
|
+
"ui/theme.js",
|
|
31
33
|
"types/**"
|
|
32
34
|
],
|
|
33
35
|
"publishConfig": {
|
|
34
36
|
"access": "public"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
39
|
+
"@radix-ui/colors": "^3.0.0",
|
|
37
40
|
"@iiif/helpers": "^1.4.0",
|
|
38
41
|
"@mdx-js/mdx": "^3.1.0",
|
|
39
42
|
"@mdx-js/react": "^3.1.0",
|