@canopy-iiif/app 1.0.1 → 1.1.0

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 CHANGED
@@ -1589,11 +1589,22 @@ async function buildIiifCollectionPages(CONFIG) {
1589
1589
  .join("/")
1590
1590
  : null;
1591
1591
 
1592
+ const moduleScriptRels = [];
1593
+ if (viewerRel) moduleScriptRels.push(viewerRel);
1594
+ if (sliderRel) moduleScriptRels.push(sliderRel);
1595
+ const primaryClassicScripts = [];
1596
+ if (heroRel) primaryClassicScripts.push(heroRel);
1597
+ if (relatedRel) primaryClassicScripts.push(relatedRel);
1598
+ if (timelineRel) primaryClassicScripts.push(timelineRel);
1599
+ const secondaryClassicScripts = [];
1600
+ if (searchFormRel) secondaryClassicScripts.push(searchFormRel);
1592
1601
  let jsRel = null;
1593
- if (needsHeroSlider && heroRel) jsRel = heroRel;
1594
- else if (needsRelated && sliderRel) jsRel = sliderRel;
1595
- else if (needsTimeline && timelineRel) jsRel = timelineRel;
1596
- else if (viewerRel) jsRel = viewerRel;
1602
+ if (primaryClassicScripts.length) {
1603
+ jsRel = primaryClassicScripts.shift();
1604
+ }
1605
+ const classicScriptRels = primaryClassicScripts.concat(
1606
+ secondaryClassicScripts
1607
+ );
1597
1608
 
1598
1609
  const headSegments = [head];
1599
1610
  const needsReact = !!(
@@ -1621,20 +1632,16 @@ async function buildIiifCollectionPages(CONFIG) {
1621
1632
  } catch (_) {}
1622
1633
  }
1623
1634
  const extraScripts = [];
1624
- if (heroRel && jsRel !== heroRel)
1625
- extraScripts.push(`<script defer src="${heroRel}"></script>`);
1626
- if (relatedRel && jsRel !== relatedRel)
1627
- extraScripts.push(`<script defer src="${relatedRel}"></script>`);
1628
- if (timelineRel && jsRel !== timelineRel)
1629
- extraScripts.push(`<script defer src="${timelineRel}"></script>`);
1630
- if (viewerRel && jsRel !== viewerRel)
1631
- extraScripts.push(`<script defer src="${viewerRel}"></script>`);
1632
- if (sliderRel && jsRel !== sliderRel)
1633
- extraScripts.push(`<script defer src="${sliderRel}"></script>`);
1634
- if (searchFormRel && jsRel !== searchFormRel)
1635
- extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
1636
- if (extraScripts.length)
1637
- headSegments.push(extraScripts.join(""));
1635
+ const pushClassicScript = (src) => {
1636
+ if (!src || src === jsRel) return;
1637
+ extraScripts.push(`<script defer src="${src}"></script>`);
1638
+ };
1639
+ const pushModuleScript = (src) => {
1640
+ if (!src) return;
1641
+ extraScripts.push(`<script type="module" src="${src}"></script>`);
1642
+ };
1643
+ classicScriptRels.forEach((src) => pushClassicScript(src));
1644
+ moduleScriptRels.forEach((src) => pushModuleScript(src));
1638
1645
  try {
1639
1646
  const {BASE_PATH} = require("../common");
1640
1647
  if (BASE_PATH)
@@ -1644,7 +1651,9 @@ async function buildIiifCollectionPages(CONFIG) {
1644
1651
  )}</script>` + vendorTag;
1645
1652
  } catch (_) {}
1646
1653
  let pageBody = body;
1647
- const headExtra = headSegments.join("") + vendorTag;
1654
+ if (vendorTag) headSegments.push(vendorTag);
1655
+ if (extraScripts.length) headSegments.push(extraScripts.join(""));
1656
+ const headExtra = headSegments.join("");
1648
1657
  const pageType = (pageDetails && pageDetails.type) || "work";
1649
1658
  const bodyClass = canopyBodyClassForType(pageType);
1650
1659
  let html = htmlShell({
package/lib/build/mdx.js CHANGED
@@ -651,72 +651,19 @@ async function loadCustomLayout(defaultLayout) {
651
651
  return defaultLayout;
652
652
  }
653
653
 
654
- async function ensureClientRuntime() {
655
- // Bundle a lightweight client runtime to hydrate browser-only components
656
- // like the Clover Viewer when placeholders are present in the HTML.
657
- let esbuild = null;
654
+ function resolveEsbuild() {
658
655
  try {
659
- esbuild = require("../../ui/node_modules/esbuild");
656
+ return require("../../ui/node_modules/esbuild");
660
657
  } catch (_) {
661
658
  try {
662
- esbuild = require("esbuild");
663
- } catch (_) {}
664
- }
665
- if (!esbuild)
666
- throw new Error(
667
- "Viewer runtime bundling requires esbuild. Install dependencies before building."
668
- );
669
- ensureDirSync(OUT_DIR);
670
- const scriptsDir = path.join(OUT_DIR, "scripts");
671
- ensureDirSync(scriptsDir);
672
- const outFile = path.join(scriptsDir, "canopy-viewer.js");
673
- const entry = `
674
- import CloverViewer from '@samvera/clover-iiif/viewer';
675
- import CloverScroll from '@samvera/clover-iiif/scroll';
676
- import CloverImage from '@samvera/clover-iiif/image';
677
-
678
- function ready(fn) {
679
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true });
680
- else fn();
659
+ return require("esbuild");
660
+ } catch (_) {
661
+ return null;
681
662
  }
663
+ }
664
+ }
682
665
 
683
- function parseProps(el) {
684
- try {
685
- const s = el.querySelector('script[type="application/json"]');
686
- if (s) return JSON.parse(s.textContent || '{}');
687
- const raw = el.getAttribute('data-props') || '{}';
688
- return JSON.parse(raw);
689
- } catch (_) { return {}; }
690
- }
691
-
692
- function mountAll(selector, Component) {
693
- try {
694
- const nodes = document.querySelectorAll(selector);
695
- if (!nodes || !nodes.length || !Component) return;
696
- const React = (window && window.React) || null;
697
- const ReactDOMClient = (window && window.ReactDOMClient) || null;
698
- const createRoot = ReactDOMClient && ReactDOMClient.createRoot;
699
- if (!React || !createRoot) return;
700
- for (const el of nodes) {
701
- try {
702
- if (el.__canopyHydrated) continue;
703
- const props = parseProps(el);
704
- const root = createRoot(el);
705
- root.render(React.createElement(Component, props));
706
- el.__canopyHydrated = true;
707
- } catch (_) { /* skip */ }
708
- }
709
- } catch (_) { /* no-op */ }
710
- }
711
-
712
- function seedScrollSearchInput() {}
713
-
714
- ready(function() {
715
- mountAll('[data-canopy-viewer]', CloverViewer);
716
- mountAll('[data-canopy-scroll]', CloverScroll);
717
- mountAll('[data-canopy-image]', CloverImage);
718
- });
719
- `;
666
+ function createReactShimPlugin() {
720
667
  const reactShim = `
721
668
  const React = (typeof window !== 'undefined' && window.React) || {};
722
669
  export default React;
@@ -749,70 +696,145 @@ async function ensureClientRuntime() {
749
696
  `;
750
697
  const rdomClientShim = `
751
698
  const RDC = (typeof window !== 'undefined' && window.ReactDOMClient) || {};
752
- export default RDC;
753
699
  export const createRoot = RDC.createRoot;
754
700
  export const hydrateRoot = RDC.hydrateRoot;
755
701
  `;
756
- const plugin = {
702
+ return {
757
703
  name: "canopy-react-shims",
758
704
  setup(build) {
759
705
  const ns = "canopy-shim";
760
- build.onResolve({filter: /^react$/}, () => ({
761
- path: "react",
762
- namespace: ns,
763
- }));
764
- build.onResolve({filter: /^react-dom$/}, () => ({
765
- path: "react-dom",
766
- namespace: ns,
767
- }));
768
- build.onResolve({filter: /^react-dom\/client$/}, () => ({
769
- path: "react-dom-client",
770
- namespace: ns,
771
- }));
772
- build.onLoad({filter: /^react$/, namespace: ns}, () => ({
773
- contents: reactShim,
774
- loader: "js",
775
- }));
776
- build.onLoad({filter: /^react-dom$/, namespace: ns}, () => ({
777
- contents: rdomShim,
778
- loader: "js",
779
- }));
780
- build.onLoad({filter: /^react-dom-client$/, namespace: ns}, () => ({
781
- contents: rdomClientShim,
782
- loader: "js",
783
- }));
706
+ build.onResolve({ filter: /^react$/ }, () => ({ path: "react", namespace: ns }));
707
+ build.onResolve({ filter: /^react-dom$/ }, () => ({ path: "react-dom", namespace: ns }));
708
+ build.onResolve({ filter: /^react-dom\/client$/ }, () => ({ path: "react-dom-client", namespace: ns }));
709
+ build.onLoad({ filter: /^react$/, namespace: ns }, () => ({ contents: reactShim, loader: "js" }));
710
+ build.onLoad({ filter: /^react-dom$/, namespace: ns }, () => ({ contents: rdomShim, loader: "js" }));
711
+ build.onLoad({ filter: /^react-dom-client$/, namespace: ns }, () => ({ contents: rdomClientShim, loader: "js" }));
784
712
  },
785
713
  };
786
- await esbuild.build({
787
- stdin: {
788
- contents: entry,
789
- resolveDir: process.cwd(),
790
- sourcefile: "canopy-viewer-entry.js",
791
- loader: "js",
714
+ }
715
+
716
+ function createSliderCssInlinePlugin() {
717
+ return {
718
+ name: "canopy-inline-slider-css",
719
+ setup(build) {
720
+ build.onLoad({ filter: /\.css$/ }, (args) => {
721
+ const fs = require("fs");
722
+ let css = "";
723
+ try {
724
+ css = fs.readFileSync(args.path, "utf8");
725
+ } catch (_) {
726
+ css = "";
727
+ }
728
+ const js = [
729
+ `var css = ${JSON.stringify(css)};`,
730
+ `(function(){ try { var s = document.createElement('style'); s.setAttribute('data-canopy-slider-css',''); s.textContent = css; document.head.appendChild(s); } catch (e) {} })();`,
731
+ `export default css;`,
732
+ ].join("\n");
733
+ return { contents: js, loader: "js" };
734
+ });
792
735
  },
793
- outfile: outFile,
794
- platform: "browser",
795
- format: "iife",
736
+ };
737
+ }
738
+
739
+ let cloverRuntimePromise = null;
740
+
741
+ function renameAnonymousChunks(scriptsDir) {
742
+ try {
743
+ const entries = fs
744
+ .readdirSync(scriptsDir)
745
+ .filter((name) => name.startsWith('canopy-chunk-') && name.endsWith('.js'));
746
+ if (!entries.length) return;
747
+ const replacements = [];
748
+ for (const oldName of entries) {
749
+ const newName = `canopy-shared-${oldName.slice('canopy-chunk-'.length)}`;
750
+ if (newName === oldName) continue;
751
+ const fromPath = path.join(scriptsDir, oldName);
752
+ const toPath = path.join(scriptsDir, newName);
753
+ try {
754
+ fs.renameSync(fromPath, toPath);
755
+ replacements.push({ from: oldName, to: newName });
756
+ } catch (_) {}
757
+ }
758
+ if (!replacements.length) return;
759
+ const targetFiles = fs
760
+ .readdirSync(scriptsDir)
761
+ .filter((name) => name.endsWith('.js'));
762
+ for (const filename of targetFiles) {
763
+ const filePath = path.join(scriptsDir, filename);
764
+ let contents = '';
765
+ try {
766
+ contents = fs.readFileSync(filePath, 'utf8');
767
+ } catch (_) {
768
+ continue;
769
+ }
770
+ let changed = false;
771
+ replacements.forEach(({ from, to }) => {
772
+ if (contents.includes(from)) {
773
+ contents = contents.split(from).join(to);
774
+ changed = true;
775
+ }
776
+ });
777
+ if (changed) {
778
+ try {
779
+ fs.writeFileSync(filePath, contents, 'utf8');
780
+ } catch (_) {}
781
+ }
782
+ }
783
+ } catch (_) {}
784
+ }
785
+
786
+ async function buildCloverHydrationRuntimes() {
787
+ const esbuild = resolveEsbuild();
788
+ if (!esbuild)
789
+ throw new Error(
790
+ "Clover hydration runtimes require esbuild. Install dependencies before building."
791
+ );
792
+ ensureDirSync(OUT_DIR);
793
+ const scriptsDir = path.join(OUT_DIR, "scripts");
794
+ ensureDirSync(scriptsDir);
795
+ const entryPoints = {
796
+ viewer: path.join(__dirname, "../components/viewer-runtime-entry.js"),
797
+ slider: path.join(__dirname, "../components/slider-runtime-entry.js"),
798
+ };
799
+ await esbuild.build({
800
+ entryPoints,
801
+ outdir: scriptsDir,
802
+ entryNames: "canopy-[name]",
803
+ chunkNames: "canopy-[name]-[hash]",
796
804
  bundle: true,
805
+ platform: "browser",
806
+ format: "esm",
807
+ splitting: true,
797
808
  sourcemap: false,
798
809
  target: ["es2018"],
799
810
  logLevel: "silent",
800
811
  minify: true,
801
- plugins: [plugin],
812
+ define: { "process.env.NODE_ENV": '"production"' },
813
+ plugins: [createReactShimPlugin(), createSliderCssInlinePlugin()],
802
814
  });
815
+ renameAnonymousChunks(scriptsDir);
803
816
  try {
804
- const {logLine} = require("./log");
805
- let size = 0;
806
- try {
807
- const st = fs.statSync(outFile);
808
- size = (st && st.size) || 0;
809
- } catch (_) {}
810
- const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : "";
811
- const rel = path.relative(process.cwd(), outFile).split(path.sep).join("/");
812
- logLine(`✓ Wrote ${rel}${kb}`, "cyan");
817
+ const { logLine } = require("./log");
818
+ ["canopy-viewer.js", "canopy-slider.js"].forEach((file) => {
819
+ try {
820
+ const abs = path.join(scriptsDir, file);
821
+ const st = fs.statSync(abs);
822
+ const size = st && st.size ? st.size : 0;
823
+ const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : "";
824
+ const rel = path.relative(process.cwd(), abs).split(path.sep).join("/");
825
+ logLine(`✓ Wrote ${rel}${kb}`, "cyan");
826
+ } catch (_) {}
827
+ });
813
828
  } catch (_) {}
814
829
  }
815
830
 
831
+ async function ensureClientRuntime() {
832
+ if (!cloverRuntimePromise) {
833
+ cloverRuntimePromise = buildCloverHydrationRuntimes();
834
+ }
835
+ return cloverRuntimePromise;
836
+ }
837
+
816
838
  // Facets runtime: fetches /api/search/facets.json, picks a value per label (random from top 3),
817
839
  // and renders a Slider for each.
818
840
  async function ensureFacetsRuntime() {
@@ -952,128 +974,8 @@ async function ensureFacetsRuntime() {
952
974
  } catch (_) {}
953
975
  }
954
976
 
955
- // Bundle a separate client runtime for the Clover Slider to keep payloads split.
956
977
  async function ensureSliderRuntime() {
957
- let esbuild = null;
958
- try {
959
- esbuild = require("../ui/node_modules/esbuild");
960
- } catch (_) {
961
- try {
962
- esbuild = require("esbuild");
963
- } catch (_) {}
964
- }
965
- if (!esbuild)
966
- throw new Error(
967
- "Slider runtime bundling requires esbuild. Install dependencies before building."
968
- );
969
- ensureDirSync(OUT_DIR);
970
- const scriptsDir = path.join(OUT_DIR, "scripts");
971
- ensureDirSync(scriptsDir);
972
- const outFile = path.join(scriptsDir, "canopy-slider.js");
973
- const entryFile = path.join(__dirname, "../components/slider-runtime-entry.js");
974
- const reactShim = `
975
- const React = (typeof window !== 'undefined' && window.React) || {};
976
- export default React;
977
- export const Children = React.Children;
978
- export const Component = React.Component;
979
- export const Fragment = React.Fragment;
980
- export const createElement = React.createElement;
981
- export const cloneElement = React.cloneElement;
982
- export const createContext = React.createContext;
983
- export const forwardRef = React.forwardRef;
984
- export const memo = React.memo;
985
- export const startTransition = React.startTransition;
986
- export const isValidElement = React.isValidElement;
987
- export const useEffect = React.useEffect;
988
- export const useLayoutEffect = React.useLayoutEffect;
989
- export const useMemo = React.useMemo;
990
- export const useState = React.useState;
991
- export const useRef = React.useRef;
992
- export const useCallback = React.useCallback;
993
- export const useContext = React.useContext;
994
- export const useReducer = React.useReducer;
995
- export const useId = React.useId;
996
- `;
997
- const rdomClientShim = `
998
- const RDC = (typeof window !== 'undefined' && window.ReactDOMClient) || {};
999
- export const createRoot = RDC.createRoot;
1000
- export const hydrateRoot = RDC.hydrateRoot;
1001
- `;
1002
- const plugin = {
1003
- name: "canopy-react-shims-slider",
1004
- setup(build) {
1005
- const ns = "canopy-shim";
1006
- build.onResolve({filter: /^react$/}, () => ({
1007
- path: "react",
1008
- namespace: ns,
1009
- }));
1010
- build.onResolve({filter: /^react-dom$/}, () => ({
1011
- path: "react-dom",
1012
- namespace: ns,
1013
- }));
1014
- build.onResolve({filter: /^react-dom\/client$/}, () => ({
1015
- path: "react-dom-client",
1016
- namespace: ns,
1017
- }));
1018
- build.onLoad({filter: /^react$/, namespace: ns}, () => ({
1019
- contents: reactShim,
1020
- loader: "js",
1021
- }));
1022
- build.onLoad({filter: /^react-dom$/, namespace: ns}, () => ({
1023
- contents:
1024
- "export default ((typeof window!=='undefined' && window.ReactDOM) || {});",
1025
- loader: "js",
1026
- }));
1027
- build.onLoad({filter: /^react-dom-client$/, namespace: ns}, () => ({
1028
- contents: rdomClientShim,
1029
- loader: "js",
1030
- }));
1031
- // Inline imported CSS into a <style> tag at runtime so we don't need a separate CSS file
1032
- build.onLoad({filter: /\.css$/}, (args) => {
1033
- const fs = require("fs");
1034
- let css = "";
1035
- try {
1036
- css = fs.readFileSync(args.path, "utf8");
1037
- } catch (_) {
1038
- css = "";
1039
- }
1040
- const js = [
1041
- `var css = ${JSON.stringify(css)};`,
1042
- `(function(){ try { var s = document.createElement('style'); s.setAttribute('data-canopy-slider-css',''); s.textContent = css; document.head.appendChild(s); } catch (e) {} })();`,
1043
- `export default css;`,
1044
- ].join("\n");
1045
- return {contents: js, loader: "js"};
1046
- });
1047
- },
1048
- };
1049
- try {
1050
- await esbuild.build({
1051
- entryPoints: [entryFile],
1052
- outfile: outFile,
1053
- platform: "browser",
1054
- format: "iife",
1055
- bundle: true,
1056
- sourcemap: false,
1057
- target: ["es2018"],
1058
- logLevel: "silent",
1059
- minify: true,
1060
- plugins: [plugin],
1061
- });
1062
- } catch (e) {
1063
- const message = e && e.message ? e.message : e;
1064
- throw new Error(`Slider runtime build failed: ${message}`);
1065
- }
1066
- try {
1067
- const {logLine} = require("./log");
1068
- let size = 0;
1069
- try {
1070
- const st = fs.statSync(outFile);
1071
- size = (st && st.size) || 0;
1072
- } catch (_) {}
1073
- const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : "";
1074
- const rel = path.relative(process.cwd(), outFile).split(path.sep).join("/");
1075
- logLine(`✓ Wrote ${rel}${kb}`, "cyan");
1076
- } catch (_) {}
978
+ return ensureClientRuntime();
1077
979
  }
1078
980
 
1079
981
  // Build a small React globals vendor for client-side React pages.
@@ -1291,5 +1193,6 @@ module.exports = {
1291
1193
  } catch (_) {}
1292
1194
  APP_WRAPPER = null;
1293
1195
  UI_COMPONENTS = null;
1196
+ cloverRuntimePromise = null;
1294
1197
  },
1295
1198
  };
@@ -203,13 +203,20 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}, source
203
203
  try { const st = fs.statSync(runtimeAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
204
204
  searchFormRel = rel;
205
205
  }
206
+ const moduleScriptRels = [];
207
+ if (viewerRel) moduleScriptRels.push(viewerRel);
208
+ if (sliderRel) moduleScriptRels.push(sliderRel);
209
+ const primaryClassicScripts = [];
210
+ if (heroRel) primaryClassicScripts.push(heroRel);
211
+ if (timelineRel) primaryClassicScripts.push(timelineRel);
212
+ if (facetsRel) primaryClassicScripts.push(facetsRel);
213
+ const secondaryClassicScripts = [];
214
+ if (searchFormRel) secondaryClassicScripts.push(searchFormRel);
206
215
  let jsRel = null;
207
- if (needsHeroSlider && heroRel) jsRel = heroRel;
208
- else if (needsFacets && sliderRel) jsRel = sliderRel;
209
- else if (needsTimeline && timelineRel) jsRel = timelineRel;
210
- else if (viewerRel) jsRel = viewerRel;
211
- else if (sliderRel) jsRel = sliderRel;
212
- else if (facetsRel) jsRel = facetsRel;
216
+ if (primaryClassicScripts.length) {
217
+ jsRel = primaryClassicScripts.shift();
218
+ }
219
+ const classicScriptRels = primaryClassicScripts.concat(secondaryClassicScripts);
213
220
  const needsReact = !!(needsHydrateViewer || needsHydrateSlider || needsFacets || needsTimeline);
214
221
  let vendorTag = '';
215
222
  if (needsReact) {
@@ -227,12 +234,18 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}, source
227
234
  } catch (_) {}
228
235
  const headSegments = [head];
229
236
  const extraScripts = [];
230
- if (heroRel && jsRel !== heroRel) extraScripts.push(`<script defer src="${heroRel}"></script>`);
231
- if (timelineRel && jsRel !== timelineRel) extraScripts.push(`<script defer src="${timelineRel}"></script>`);
232
- if (facetsRel && jsRel !== facetsRel) extraScripts.push(`<script defer src="${facetsRel}"></script>`);
233
- if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
234
- if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
235
- if (searchFormRel && jsRel !== searchFormRel) extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
237
+ const pushClassicScript = (src) => {
238
+ if (!src || src === jsRel) return;
239
+ extraScripts.push(`<script defer src="${src}"></script>`);
240
+ };
241
+ const pushModuleScript = (src) => {
242
+ if (!src) return;
243
+ extraScripts.push(`<script type="module" src="${src}"></script>`);
244
+ };
245
+ classicScriptRels.forEach((src) => pushClassicScript(src));
246
+ if (moduleScriptRels.length) {
247
+ moduleScriptRels.forEach((src) => pushModuleScript(src));
248
+ }
236
249
  const extraStyles = [];
237
250
  if (heroCssRel) {
238
251
  let rel = heroCssRel;
@@ -244,8 +257,9 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}, source
244
257
  extraStyles.push(`<link rel="stylesheet" href="${rel}">`);
245
258
  }
246
259
  if (extraStyles.length) headSegments.push(extraStyles.join(''));
260
+ if (vendorTag) headSegments.push(vendorTag);
247
261
  if (extraScripts.length) headSegments.push(extraScripts.join(''));
248
- const headExtra = headSegments.join('') + vendorTag;
262
+ const headExtra = headSegments.join('');
249
263
  const typeForClass = resolvedType || 'page';
250
264
  const bodyClass = canopyBodyClassForType(typeForClass);
251
265
  const html = htmlShell({
@@ -4,7 +4,6 @@ const { fs, path, OUT_DIR, ensureDirSync } = require('../common');
4
4
  async function prepareAllRuntimes() {
5
5
  const mdx = require('./mdx');
6
6
  try { await mdx.ensureClientRuntime(); } catch (_) {}
7
- try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
8
7
  try { if (typeof mdx.ensureTimelineRuntime === 'function') await mdx.ensureTimelineRuntime(); } catch (_) {}
9
8
  try { if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime(); } catch (_) {}
10
9
  try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import CloverSlider from '@samvera/clover-iiif/slider';
4
3
  import 'swiper/css';
5
4
  import 'swiper/css/navigation';
6
5
  import 'swiper/css/pagination';
@@ -41,13 +40,29 @@ function withDefaults(rawProps) {
41
40
  };
42
41
  }
43
42
 
43
+ let cloverSliderPromise = null;
44
+
45
+ function loadCloverSlider() {
46
+ if (!cloverSliderPromise) {
47
+ cloverSliderPromise = import('@samvera/clover-iiif/slider')
48
+ .then((mod) => (mod && (mod.default || mod.Slider || mod)) || null)
49
+ .catch(() => null);
50
+ }
51
+ return cloverSliderPromise;
52
+ }
53
+
44
54
  function mount(el) {
45
55
  try {
46
56
  if (!el || el.getAttribute('data-canopy-slider-mounted') === '1') return;
47
57
  const props = withDefaults(parseProps(el));
48
58
  const root = createRoot(el);
49
- root.render(React.createElement(CloverSlider, props));
50
- el.setAttribute('data-canopy-slider-mounted', '1');
59
+ loadCloverSlider().then((Component) => {
60
+ try {
61
+ if (!Component) return;
62
+ root.render(React.createElement(Component, props));
63
+ el.setAttribute('data-canopy-slider-mounted', '1');
64
+ } catch (_) {}
65
+ });
51
66
  } catch (_) {}
52
67
  }
53
68
 
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ function ready(fn) {
5
+ if (typeof document === 'undefined') return;
6
+ if (document.readyState === 'loading') {
7
+ document.addEventListener('DOMContentLoaded', fn, { once: true });
8
+ } else {
9
+ fn();
10
+ }
11
+ }
12
+
13
+ function parseProps(el) {
14
+ try {
15
+ const script = el.querySelector('script[type="application/json"]');
16
+ if (script) return JSON.parse(script.textContent || '{}');
17
+ const raw = el.getAttribute('data-props') || '{}';
18
+ return JSON.parse(raw);
19
+ } catch (_) {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ const componentLoaders = {
25
+ viewer: () => import('@samvera/clover-iiif/viewer'),
26
+ scroll: () => import('@samvera/clover-iiif/scroll'),
27
+ image: () => import('@samvera/clover-iiif/image'),
28
+ };
29
+
30
+ const componentCache = new Map();
31
+
32
+ function resolveComponent(key) {
33
+ if (!componentLoaders[key]) return Promise.resolve(null);
34
+ if (!componentCache.has(key)) {
35
+ const loader = componentLoaders[key];
36
+ componentCache.set(
37
+ key,
38
+ loader()
39
+ .then((mod) => {
40
+ if (!mod) return null;
41
+ return mod.default || mod.Viewer || mod.Scroll || mod.Image || mod;
42
+ })
43
+ .catch(() => null)
44
+ );
45
+ }
46
+ return componentCache.get(key);
47
+ }
48
+
49
+ function mountAll(selector, key) {
50
+ try {
51
+ const nodes = document.querySelectorAll(selector);
52
+ if (!nodes || !nodes.length) return;
53
+ const rootApi = typeof createRoot === 'function' ? createRoot : null;
54
+ if (!React || !rootApi) return;
55
+ resolveComponent(key).then((Component) => {
56
+ if (!Component) return;
57
+ nodes.forEach((el) => {
58
+ try {
59
+ if (el.__canopyHydrated) return;
60
+ const props = parseProps(el);
61
+ const root = rootApi(el);
62
+ root.render(React.createElement(Component, props));
63
+ el.__canopyHydrated = true;
64
+ } catch (_) {}
65
+ });
66
+ });
67
+ } catch (_) {}
68
+ }
69
+
70
+ ready(() => {
71
+ mountAll('[data-canopy-viewer]', 'viewer');
72
+ mountAll('[data-canopy-scroll]', 'scroll');
73
+ mountAll('[data-canopy-image]', 'image');
74
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",