@canopy-iiif/app 0.10.21 → 0.10.22

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.
@@ -186,6 +186,18 @@ function groupLabel(type) {
186
186
  return type.charAt(0).toUpperCase() + type.slice(1);
187
187
  }
188
188
 
189
+ function hidePanel(panel) {
190
+ if (!panel) return;
191
+ panel.classList.add('is-empty');
192
+ panel.setAttribute('hidden', 'hidden');
193
+ }
194
+
195
+ function showPanel(panel) {
196
+ if (!panel) return;
197
+ panel.classList.remove('is-empty');
198
+ panel.removeAttribute('hidden');
199
+ }
200
+
189
201
  function getItems(list) {
190
202
  try {
191
203
  return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]'));
@@ -196,7 +208,12 @@ function getItems(list) {
196
208
 
197
209
  function renderList(list, records, groupOrder) {
198
210
  list.innerHTML = '';
199
- if (!records.length) return;
211
+ const panel = list.closest('[data-canopy-search-form-panel]');
212
+ if (!records.length) {
213
+ hidePanel(panel);
214
+ return;
215
+ }
216
+ showPanel(panel);
200
217
  const groups = new Map();
201
218
  records.forEach((record) => {
202
219
  const type = String(record && record.type || 'page');
@@ -207,8 +224,8 @@ function renderList(list, records, groupOrder) {
207
224
  const orderedKeys = [...desiredOrder.filter((key) => groups.has(key)), ...Array.from(groups.keys()).filter((key) => !desiredOrder.includes(key))];
208
225
  orderedKeys.forEach((key) => {
209
226
  const header = document.createElement('div');
227
+ header.className = 'canopy-search-teaser__label';
210
228
  header.textContent = groupLabel(key);
211
- header.style.cssText = 'padding:6px 12px;font-weight:600;color:#374151';
212
229
  list.appendChild(header);
213
230
  const entries = groups.get(key) || [];
214
231
  entries.forEach((record) => {
@@ -217,44 +234,39 @@ function renderList(list, records, groupOrder) {
217
234
  item.setAttribute('data-canopy-item', '');
218
235
  item.href = href;
219
236
  item.tabIndex = 0;
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;';
237
+ item.className = 'canopy-card canopy-card--teaser canopy-search-teaser__item';
222
238
 
223
239
  const showThumb = String(record && record.type || '') === 'work' && record && record.thumbnail;
224
240
  if (showThumb) {
225
241
  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;';
242
+ media.className = 'canopy-search-teaser__thumb';
227
243
  const img = document.createElement('img');
228
244
  img.src = record.thumbnail;
229
245
  img.alt = '';
230
246
  img.loading = 'lazy';
231
- img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
247
+ img.className = 'canopy-search-teaser__thumb-img';
232
248
  media.appendChild(img);
233
249
  item.appendChild(media);
234
250
  }
235
251
 
236
252
  const textWrap = document.createElement('div');
237
- textWrap.style.cssText = 'display:flex;flex-direction:column;gap:2px;min-width:0;';
253
+ textWrap.className = 'canopy-search-teaser__text';
238
254
  const title = document.createElement('span');
239
255
  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;';
256
+ title.className = 'canopy-search-teaser__title';
241
257
  textWrap.appendChild(title);
242
258
  const meta = Array.isArray(record && record.metadata) ? record.metadata : [];
243
259
  if (meta.length) {
244
260
  const metaLine = document.createElement('span');
245
261
  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;';
262
+ metaLine.className = 'canopy-search-teaser__meta';
247
263
  textWrap.appendChild(metaLine);
248
264
  }
249
265
  item.appendChild(textWrap);
250
266
 
251
- item.onmouseenter = () => { item.style.background = '#f8fafc'; };
252
- item.onmouseleave = () => { item.style.background = 'transparent'; };
253
267
  item.onfocus = () => {
254
- item.style.background = '#eef2ff';
255
268
  try { item.scrollIntoView({ block: 'nearest' }); } catch (_) {}
256
269
  };
257
- item.onblur = () => { item.style.background = 'transparent'; };
258
270
  list.appendChild(item);
259
271
  });
260
272
  });
@@ -275,15 +287,19 @@ function focusLast(list, resetTarget) {
275
287
  items[items.length - 1].focus();
276
288
  }
277
289
 
278
- function bindKeyboardNavigation({ input, list, panel }) {
290
+ function bindKeyboardNavigation({ input, list, panel, ensureResults }) {
291
+ const ensure = () => {
292
+ if (typeof ensureResults === 'function') ensureResults();
293
+ return getItems(list).length > 0;
294
+ };
279
295
  input.addEventListener('keydown', (event) => {
280
296
  if (event.key === 'ArrowDown') {
281
297
  event.preventDefault();
282
- panel.style.display = 'block';
298
+ if (!ensure()) return;
283
299
  focusFirst(list);
284
300
  } else if (event.key === 'ArrowUp') {
285
301
  event.preventDefault();
286
- panel.style.display = 'block';
302
+ if (!ensure()) return;
287
303
  focusLast(list, input);
288
304
  }
289
305
  });
@@ -311,7 +327,7 @@ function bindKeyboardNavigation({ input, list, panel }) {
311
327
  event.preventDefault();
312
328
  try { current.click(); } catch (_) {}
313
329
  } else if (event.key === 'Escape') {
314
- panel.style.display = 'none';
330
+ hidePanel(panel);
315
331
  try { input && input.focus && input.focus(); } catch (_) {}
316
332
  }
317
333
  });
@@ -336,7 +352,7 @@ async function attachSearchForm(host) {
336
352
  } catch (_) {}
337
353
  }
338
354
 
339
- if (onSearchPage) panel.style.display = 'none';
355
+ if (onSearchPage) hidePanel(panel);
340
356
 
341
357
  const list = (() => {
342
358
  try { return panel.querySelector('#cplist'); } catch (_) { return null; }
@@ -357,13 +373,7 @@ async function attachSearchForm(host) {
357
373
  const records = await loadRecords();
358
374
 
359
375
  function render(items) {
360
- list.innerHTML = '';
361
- if (!items.length) {
362
- panel.style.display = onSearchPage ? 'none' : 'block';
363
- return;
364
- }
365
376
  renderList(list, items, groupOrder);
366
- panel.style.display = 'block';
367
377
  }
368
378
 
369
379
  function filterAndShow(query) {
@@ -371,7 +381,7 @@ async function attachSearchForm(host) {
371
381
  const q = toLower(query);
372
382
  if (!q) {
373
383
  list.innerHTML = '';
374
- panel.style.display = onSearchPage ? 'none' : 'block';
384
+ hidePanel(panel);
375
385
  return;
376
386
  }
377
387
  const out = [];
@@ -404,18 +414,27 @@ async function attachSearchForm(host) {
404
414
  filterAndShow(input.value || '');
405
415
  });
406
416
 
407
- bindKeyboardNavigation({ input, list, panel });
417
+ bindKeyboardNavigation({
418
+ input,
419
+ list,
420
+ panel,
421
+ ensureResults: () => {
422
+ if (!getItems(list).length) {
423
+ filterAndShow(input.value || '');
424
+ }
425
+ },
426
+ });
408
427
 
409
428
  document.addEventListener('keydown', (event) => {
410
429
  if (event.key === 'Escape') {
411
- panel.style.display = 'none';
430
+ hidePanel(panel);
412
431
  }
413
432
  });
414
433
 
415
434
  document.addEventListener('mousedown', (event) => {
416
435
  try {
417
436
  if (!panel.contains(event.target) && !host.contains(event.target)) {
418
- panel.style.display = 'none';
437
+ hidePanel(panel);
419
438
  }
420
439
  } catch (_) {}
421
440
  });
@@ -433,7 +452,6 @@ async function attachSearchForm(host) {
433
452
  try { window.dispatchEvent(new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } })); } catch (_) {}
434
453
  return;
435
454
  }
436
- panel.style.display = 'block';
437
455
  if (input && input.focus) input.focus();
438
456
  filterAndShow(input && input.value || '');
439
457
  }
@@ -446,7 +464,6 @@ async function attachSearchForm(host) {
446
464
  try { window.dispatchEvent(new CustomEvent('canopy:search:setQuery', { detail: {} })); } catch (_) {}
447
465
  return;
448
466
  }
449
- panel.style.display = 'block';
450
467
  if (input && input.focus) input.focus();
451
468
  filterAndShow(input && input.value || '');
452
469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "0.10.21",
3
+ "version": "0.10.22",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
package/ui/dist/index.mjs CHANGED
@@ -225,7 +225,9 @@ function tokenizeInlineMarkdown(input = "") {
225
225
  }
226
226
  }
227
227
  const specials = ["**", "__", "*", "_", "`", "[", "\n"];
228
- const nextIndex = specials.map((needle) => needle === "\n" ? text.indexOf("\n") : text.indexOf(needle)).filter((idx) => idx > 0).reduce((min, idx) => min === -1 || idx < min ? idx : min, -1);
228
+ const nextIndex = specials.map(
229
+ (needle) => needle === "\n" ? text.indexOf("\n") : text.indexOf(needle)
230
+ ).filter((idx) => idx > 0).reduce((min, idx) => min === -1 || idx < min ? idx : min, -1);
229
231
  if (nextIndex === -1) {
230
232
  tokens.push({ type: "text", value: text });
231
233
  break;
@@ -257,7 +259,10 @@ function renderMarkdownTokens(tokens, query, keyPrefix = "token") {
257
259
  }
258
260
  function formatDisplayUrl(href = "") {
259
261
  try {
260
- const url = new URL(href, href.startsWith("http") ? void 0 : "http://example.com");
262
+ const url = new URL(
263
+ href,
264
+ href.startsWith("http") ? void 0 : "http://example.com"
265
+ );
261
266
  if (!href.startsWith("http")) return href;
262
267
  const displayPath = url.pathname.replace(/\/$/, "");
263
268
  return `${url.host}${displayPath}${url.search}`.replace(/\/$/, "");
@@ -285,7 +290,7 @@ function ArticleCard({
285
290
  );
286
291
  const metaList = Array.isArray(metadata) ? metadata.map((m) => String(m || "")).filter(Boolean) : [];
287
292
  const displayUrl = useMemo(() => formatDisplayUrl(href), [href]);
288
- return /* @__PURE__ */ React3.createElement("a", { href, className: "canopy-article-card" }, /* @__PURE__ */ React3.createElement("article", null, displayUrl ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__url" }, displayUrl) : null, /* @__PURE__ */ React3.createElement("h3", null, title), snippet ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__snippet" }, renderMarkdownTokens(snippetTokens, query)) : null, metaList.length ? /* @__PURE__ */ React3.createElement("ul", { className: "canopy-article-card__meta" }, metaList.slice(0, 3).map((item, idx) => /* @__PURE__ */ React3.createElement("li", { key: `${item}-${idx}` }, item))) : null));
293
+ return /* @__PURE__ */ React3.createElement("a", { href, className: "canopy-article-card" }, /* @__PURE__ */ React3.createElement("article", null, /* @__PURE__ */ React3.createElement("h3", null, title), displayUrl ? /* @__PURE__ */ React3.createElement("span", { className: "canopy-article-card__url" }, displayUrl) : null, snippet ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__snippet" }, renderMarkdownTokens(snippetTokens, query)) : null, metaList.length ? /* @__PURE__ */ React3.createElement("ul", { className: "canopy-article-card__meta" }, metaList.slice(0, 3).map((item, idx) => /* @__PURE__ */ React3.createElement("li", { key: `${item}-${idx}` }, item))) : null));
289
294
  }
290
295
 
291
296
  // ui/src/layout/Grid.jsx
@@ -607,15 +612,16 @@ function SearchPanelForm(props = {}) {
607
612
  import React10 from "react";
608
613
  function SearchPanelTeaserResults(props = {}) {
609
614
  const { style, className } = props || {};
610
- const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
615
+ const classes = ["canopy-search-teaser", "is-empty", className].filter(Boolean).join(" ");
611
616
  return /* @__PURE__ */ React10.createElement(
612
617
  "div",
613
618
  {
614
619
  "data-canopy-search-form-panel": true,
620
+ hidden: true,
615
621
  className: classes || void 0,
616
622
  style
617
623
  },
618
- /* @__PURE__ */ React10.createElement("div", { id: "cplist" })
624
+ /* @__PURE__ */ React10.createElement("div", { id: "cplist", className: "canopy-search-teaser__list" })
619
625
  );
620
626
  }
621
627