@canopy-iiif/app 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/AGENTS.md CHANGED
@@ -60,6 +60,7 @@ Logbook Template
60
60
 
61
61
  Logbook
62
62
  -------
63
+ - 2026-04-26 / mat: Added local file path support to the manifest fetch worker in `build/iiif.js`. Previously, the worker branched on `https?://` and `file://` and SKIPped everything else; relative paths (e.g., `assets/iiif/example.json`) were silently dropped. Added a third branch that catches any value with no URI scheme (regex: no `[scheme]:` prefix) and routes it through `readJsonFromUri()`, which already resolves relative paths against `process.cwd()`. Collection-root URIs were already handled correctly via `readJsonFromUri()`; only the manifest worker needed the fix.
63
64
  - 2025-09-26 / chatgpt: Hardened runtime bundlers to throw when esbuild or source compilation fails and required `content/works/_layout.mdx`; build now aborts instead of silently writing placeholder assets.
64
65
  - 2025-09-26 / chatgpt: Replaced the legacy command runtime stub with an esbuild-bundled runtime (`search/search-form-runtime.js`); `prepareSearchFormRuntime()` now builds `site/scripts/canopy-search-form.js` and fails if esbuild is missing.
65
66
  - 2025-09-27 / chatgpt: Documented Tailwind token flow in `app/styles/tailwind.config.mts`, compiled UI Sass variables during config load, and exposed `stylesheetHref`/`Stylesheet` helpers via `@canopy-iiif/app/head` so `_app.mdx` can reference the generated CSS directly.
@@ -1,9 +1,4 @@
1
- import Swiper from 'swiper';
2
- import { Navigation, Pagination, Autoplay, EffectFade } from 'swiper/modules';
3
- import 'swiper/css';
4
- import 'swiper/css/navigation';
5
- import 'swiper/css/pagination';
6
- import 'swiper/css/effect-fade';
1
+ import EmblaCarousel from 'embla-carousel';
7
2
 
8
3
  function ready(fn) {
9
4
  if (typeof document === 'undefined') return;
@@ -16,61 +11,85 @@ function ready(fn) {
16
11
 
17
12
  function initSlider(host) {
18
13
  if (!host || host.__canopyHeroBound) return;
19
- const slider = host.querySelector('.canopy-interstitial__slider');
20
- if (!slider) return;
21
- const prev = host.querySelector('.canopy-interstitial__nav-btn--prev');
22
- const next = host.querySelector('.canopy-interstitial__nav-btn--next');
23
- const pagination = host.querySelector('.canopy-interstitial__pagination');
24
- const transitionAttr = (host.getAttribute && host.getAttribute('data-transition')) || 'fade';
25
- const transition = transitionAttr && transitionAttr.toLowerCase() === 'slide' ? 'slide' : 'fade';
14
+ const viewport = host.querySelector('.canopy-interstitial__slider');
15
+ if (!viewport) return;
26
16
 
27
- try {
28
- const baseModules = [Navigation, Pagination, Autoplay];
29
- if (transition === 'fade') baseModules.push(EffectFade);
30
- const swiperInstance = new Swiper(slider, {
31
- modules: baseModules,
32
- loop: true,
33
- slidesPerView: 1,
34
- effect: transition,
35
- fadeEffect: transition === 'fade' ? { crossFade: true } : undefined,
36
- navigation: {
37
- prevEl: prev || undefined,
38
- nextEl: next || undefined,
39
- },
40
- pagination: {
41
- el: pagination || undefined,
42
- clickable: true,
43
- },
44
- autoplay: {
45
- delay: 6000,
46
- disableOnInteraction: false,
47
- },
48
- });
17
+ const slides = Array.from(viewport.querySelectorAll('.canopy-interstitial__slide'));
18
+ if (slides.length <= 1) {
49
19
  host.__canopyHeroBound = true;
50
- host.__canopyHeroSwiper = swiperInstance;
51
- } catch (error) {
52
- try {
53
- console.warn('[canopy][hero] failed to initialise slider', error);
54
- } catch (_) {}
20
+ return;
55
21
  }
22
+
23
+ const paginationEl = host.querySelector('.canopy-interstitial__pagination');
24
+ const prevBtn = host.querySelector('.canopy-interstitial__nav-btn--prev');
25
+ const nextBtn = host.querySelector('.canopy-interstitial__nav-btn--next');
26
+
27
+ // Live region for screen-reader slide announcements
28
+ const liveEl = document.createElement('div');
29
+ liveEl.setAttribute('aria-live', 'polite');
30
+ liveEl.setAttribute('aria-atomic', 'true');
31
+ liveEl.className = 'canopy-interstitial__sr-live';
32
+ host.appendChild(liveEl);
33
+
34
+ const embla = EmblaCarousel(viewport, { loop: true, duration: 0 });
35
+
36
+ const announce = (idx) => {
37
+ liveEl.textContent = `Slide ${idx + 1} of ${slides.length}`;
38
+ };
39
+
40
+ if (paginationEl) {
41
+ slides.forEach((_, i) => {
42
+ const dot = document.createElement('button');
43
+ dot.type = 'button';
44
+ dot.className =
45
+ 'canopy-interstitial__dot' +
46
+ (i === 0 ? ' canopy-interstitial__dot--active' : '');
47
+ dot.setAttribute('aria-label', `Go to slide ${i + 1}`);
48
+ dot.setAttribute('aria-current', i === 0 ? 'true' : 'false');
49
+ dot.addEventListener('click', () => embla.scrollTo(i));
50
+ paginationEl.appendChild(dot);
51
+ });
52
+
53
+ embla.on('select', () => {
54
+ const idx = embla.selectedScrollSnap();
55
+ announce(idx);
56
+ paginationEl.querySelectorAll('.canopy-interstitial__dot').forEach((dot, i) => {
57
+ const active = i === idx;
58
+ dot.classList.toggle('canopy-interstitial__dot--active', active);
59
+ dot.setAttribute('aria-current', active ? 'true' : 'false');
60
+ });
61
+ });
62
+ }
63
+
64
+ if (prevBtn) prevBtn.addEventListener('click', () => embla.scrollPrev());
65
+ if (nextBtn) nextBtn.addEventListener('click', () => embla.scrollNext());
66
+
67
+ let timer = setInterval(() => embla.scrollNext(), 6000);
68
+ const stopAutoplay = () => { clearInterval(timer); timer = null; };
69
+ const startAutoplay = () => { if (!timer) timer = setInterval(() => embla.scrollNext(), 6000); };
70
+
71
+ embla.on('pointerDown', stopAutoplay);
72
+ embla.on('pointerUp', startAutoplay);
73
+
74
+ host.__canopyHeroBound = true;
56
75
  }
57
76
 
58
77
  function observeHosts() {
59
78
  try {
60
- const observer = new MutationObserver((mutations) => {
79
+ new MutationObserver((mutations) => {
61
80
  mutations.forEach((mutation) => {
62
81
  mutation.addedNodes &&
63
82
  mutation.addedNodes.forEach((node) => {
64
83
  if (!(node instanceof Element)) return;
65
- if (node.matches && node.matches('[data-canopy-hero-slider]')) initSlider(node);
84
+ if (node.matches && node.matches('[data-canopy-hero-slider]'))
85
+ initSlider(node);
66
86
  const inner = node.querySelectorAll
67
87
  ? node.querySelectorAll('[data-canopy-hero-slider]')
68
88
  : [];
69
89
  inner && inner.forEach && inner.forEach((el) => initSlider(el));
70
90
  });
71
91
  });
72
- });
73
- observer.observe(document.documentElement || document.body, {
92
+ }).observe(document.documentElement || document.body, {
74
93
  childList: true,
75
94
  subtree: true,
76
95
  });
@@ -79,7 +98,6 @@ function observeHosts() {
79
98
 
80
99
  ready(() => {
81
100
  if (typeof document === 'undefined') return;
82
- const hosts = document.querySelectorAll('[data-canopy-hero-slider]');
83
- hosts.forEach((host) => initSlider(host));
101
+ document.querySelectorAll('[data-canopy-hero-slider]').forEach((host) => initSlider(host));
84
102
  observeHosts();
85
103
  });
@@ -1,8 +1,5 @@
1
1
  import React from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import 'swiper/css';
4
- import 'swiper/css/navigation';
5
- import 'swiper/css/pagination';
6
3
  import {
7
4
  mergeSliderOptions,
8
5
  normalizeSliderOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
@@ -1182,7 +1182,6 @@ function Hero({
1182
1182
  item,
1183
1183
  index,
1184
1184
  random = true,
1185
- transition = "fade",
1186
1185
  headline,
1187
1186
  description,
1188
1187
  links = [],
@@ -1308,15 +1307,6 @@ function Hero({
1308
1307
  backgroundClassName,
1309
1308
  className
1310
1309
  ].filter(Boolean).join(" ");
1311
- const normalizedTransition = (() => {
1312
- try {
1313
- const raw = transition == null ? "" : String(transition);
1314
- const normalized = raw.trim().toLowerCase();
1315
- return normalized === "slide" ? "slide" : "fade";
1316
- } catch (_) {
1317
- return "fade";
1318
- }
1319
- })();
1320
1310
  const renderSlide = (slide, idx, { showVeil = true, captionVariant = "overlay" } = {}) => {
1321
1311
  const safeHref = applyBasePath(slide.href || "#");
1322
1312
  const isStaticCaption = captionVariant === "static";
@@ -1351,38 +1341,68 @@ function Hero({
1351
1341
  );
1352
1342
  };
1353
1343
  if (isStaticCaption) {
1354
- return /* @__PURE__ */ React12.createElement("div", { className: "swiper-slide", key: safeHref || idx }, wrapWithLink(
1355
- /* @__PURE__ */ React12.createElement("article", { className: paneClassName }, slide.thumbnail ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__media-frame" }, /* @__PURE__ */ React12.createElement(
1356
- "img",
1357
- {
1358
- ...buildImageProps(
1359
- "canopy-interstitial__media canopy-interstitial__media--static"
1360
- )
1361
- }
1362
- )) : null, slide.title ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__caption canopy-interstitial__caption--static" }, /* @__PURE__ */ React12.createElement("span", { className: "canopy-interstitial__caption-link" }, slide.title)) : null)
1363
- ));
1344
+ return /* @__PURE__ */ React12.createElement(
1345
+ "div",
1346
+ {
1347
+ className: "canopy-interstitial__slide",
1348
+ key: safeHref || idx,
1349
+ role: "group",
1350
+ "aria-roledescription": "slide",
1351
+ "aria-label": `${idx + 1} of ${orderedSlides.length}`
1352
+ },
1353
+ wrapWithLink(
1354
+ /* @__PURE__ */ React12.createElement("article", { className: paneClassName }, slide.thumbnail ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__media-frame" }, /* @__PURE__ */ React12.createElement(
1355
+ "img",
1356
+ {
1357
+ ...buildImageProps(
1358
+ "canopy-interstitial__media canopy-interstitial__media--static"
1359
+ )
1360
+ }
1361
+ )) : null, slide.title ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__caption canopy-interstitial__caption--static" }, /* @__PURE__ */ React12.createElement("span", { className: "canopy-interstitial__caption-link" }, slide.title)) : null)
1362
+ )
1363
+ );
1364
1364
  }
1365
- return /* @__PURE__ */ React12.createElement("div", { className: "swiper-slide", key: safeHref || idx }, wrapWithLink(
1366
- /* @__PURE__ */ React12.createElement("article", { className: paneClassName }, slide.thumbnail ? /* @__PURE__ */ React12.createElement("img", { ...buildImageProps("canopy-interstitial__media") }) : null, showVeil ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__veil", "aria-hidden": "true" }) : null, slide.title ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__caption" }, /* @__PURE__ */ React12.createElement("span", { className: "canopy-interstitial__caption-link" }, slide.title)) : null)
1367
- ));
1365
+ return /* @__PURE__ */ React12.createElement(
1366
+ "div",
1367
+ {
1368
+ className: "canopy-interstitial__slide",
1369
+ key: safeHref || idx,
1370
+ role: "group",
1371
+ "aria-roledescription": "slide",
1372
+ "aria-label": `${idx + 1} of ${orderedSlides.length}`
1373
+ },
1374
+ wrapWithLink(
1375
+ /* @__PURE__ */ React12.createElement("article", { className: paneClassName }, slide.thumbnail ? /* @__PURE__ */ React12.createElement("img", { ...buildImageProps("canopy-interstitial__media") }) : null, showVeil ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__veil", "aria-hidden": "true" }) : null, slide.title ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__caption" }, /* @__PURE__ */ React12.createElement("span", { className: "canopy-interstitial__caption-link" }, slide.title)) : null)
1376
+ )
1377
+ );
1368
1378
  };
1369
- const renderSlider = (options = {}) => /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__slider swiper" }, /* @__PURE__ */ React12.createElement("div", { className: "swiper-wrapper" }, orderedSlides.map((slide, idx) => renderSlide(slide, idx, options))), /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__nav" }, /* @__PURE__ */ React12.createElement(
1370
- "button",
1371
- {
1372
- type: "button",
1373
- "aria-label": "Previous slide",
1374
- className: "canopy-interstitial__nav-btn canopy-interstitial__nav-btn--prev swiper-button-prev"
1375
- },
1376
- /* @__PURE__ */ React12.createElement(PrevArrowIcon, null)
1377
- ), /* @__PURE__ */ React12.createElement(
1378
- "button",
1379
+ const renderSlider = (options = {}) => /* @__PURE__ */ React12.createElement(
1380
+ "div",
1379
1381
  {
1380
- type: "button",
1381
- "aria-label": "Next slide",
1382
- className: "canopy-interstitial__nav-btn canopy-interstitial__nav-btn--next swiper-button-next"
1382
+ className: "canopy-interstitial__slider",
1383
+ role: "region",
1384
+ "aria-roledescription": "carousel",
1385
+ "aria-label": overlayTitle || "Featured content"
1383
1386
  },
1384
- /* @__PURE__ */ React12.createElement(NextArrowIcon, null)
1385
- )), /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__pagination swiper-pagination" }));
1387
+ /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__slide-wrapper" }, orderedSlides.map((slide, idx) => renderSlide(slide, idx, options))),
1388
+ /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__nav" }, /* @__PURE__ */ React12.createElement(
1389
+ "button",
1390
+ {
1391
+ type: "button",
1392
+ "aria-label": "Previous slide",
1393
+ className: "canopy-interstitial__nav-btn canopy-interstitial__nav-btn--prev"
1394
+ },
1395
+ /* @__PURE__ */ React12.createElement(PrevArrowIcon, null)
1396
+ ), /* @__PURE__ */ React12.createElement(
1397
+ "button",
1398
+ {
1399
+ type: "button",
1400
+ "aria-label": "Next slide",
1401
+ className: "canopy-interstitial__nav-btn canopy-interstitial__nav-btn--next"
1402
+ },
1403
+ /* @__PURE__ */ React12.createElement(NextArrowIcon, null)
1404
+ ))
1405
+ );
1386
1406
  const overlayContent = /* @__PURE__ */ React12.createElement(React12.Fragment, null, overlayTitle ? /* @__PURE__ */ React12.createElement("h1", { className: "canopy-interstitial__headline" }, overlayTitle) : null, derivedDescription ? /* @__PURE__ */ React12.createElement("p", { className: "canopy-interstitial__description" }, derivedDescription) : null, finalOverlayLinks.length ? /* @__PURE__ */ React12.createElement(ButtonWrapper, { className: "canopy-interstitial__actions" }, finalOverlayLinks.map((link) => /* @__PURE__ */ React12.createElement(
1387
1407
  Button,
1388
1408
  {
@@ -1401,11 +1421,10 @@ function Hero({
1401
1421
  };
1402
1422
  if (!isBreadcrumbVariant) {
1403
1423
  sectionProps["data-canopy-hero-slider"] = "1";
1404
- sectionProps["data-transition"] = normalizedTransition;
1405
1424
  } else {
1406
1425
  sectionProps["data-canopy-hero-variant"] = "breadcrumb";
1407
1426
  }
1408
- return /* @__PURE__ */ React12.createElement("section", { ...sectionProps }, isBreadcrumbVariant ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__layout canopy-interstitial__layout--breadcrumb" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__panel" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__body" }, breadcrumbNode, overlayContent))) : /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__layout" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__panel" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__body" }, overlayContent)), /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__media-group" }, renderSlider({ showVeil: false, captionVariant: "static" }))));
1427
+ return /* @__PURE__ */ React12.createElement("section", { ...sectionProps }, isBreadcrumbVariant ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__layout canopy-interstitial__layout--breadcrumb" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__panel" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__body" }, breadcrumbNode, overlayContent))) : /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__layout" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__panel" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__body" }, overlayContent)), /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__media-group" }, renderSlider({ showVeil: false, captionVariant: "static" }), /* @__PURE__ */ React12.createElement("div", { className: "canopy-interstitial__pagination" }))));
1409
1428
  }
1410
1429
 
1411
1430
  // ui/src/layout/SubNavigation.jsx