@canopy-iiif/app 0.12.5 → 0.12.7

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
@@ -65,6 +65,7 @@ Logbook
65
65
  - 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.
66
66
  - 2025-09-27 / chatgpt: Expanded search indexing to harvest MDX pages (respecting frontmatter/layout types), injected BASE_PATH hydration data into search.html, and reworked `mdx.extractTitle()` so generated records surface real headings instead of `Untitled`.
67
67
  - 2025-10-19 / chatgpt: Embedded the Tailwind preset/plugin in a packaged config so dev/build fall back automatically; removed `app/styles/tailwind.config.*` from the default app and switched the public stylesheet to Tailwind’s CSS-first (`@import 'tailwindcss'; @theme { ... }`) workflow.
68
+ - 2025-10-20 / chatgpt: Added `lib/config-path.js` to resolve `canopy.yml` from the workspace root (preferring `options.cwd`, then npm `INIT_CWD`, then `process.cwd()`) and updated all loaders (theme, common base URL, search metadata, IIIF builder, featured manifests) to use it so hosted builds and Tailwind runs inside `node_modules/@canopy-iiif/app/ui` pick up user theme settings.
68
69
 
69
70
  Verification Commands
70
71
  ---------------------
package/lib/build/iiif.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  rootRelativeHref,
15
15
  canopyBodyClassForType,
16
16
  } = require("../common");
17
+ const {resolveCanopyConfigPath} = require("../config-path");
17
18
  const mdx = require("./mdx");
18
19
  const {log, logLine, logResponse} = require("./log");
19
20
  const { getPageContext } = require("../page-context");
@@ -1109,7 +1110,7 @@ async function rebuildManifestIndexFromCache() {
1109
1110
  }
1110
1111
 
1111
1112
  async function loadConfig() {
1112
- const cfgPath = path.resolve("canopy.yml");
1113
+ const cfgPath = resolveCanopyConfigPath();
1113
1114
  if (!fs.existsSync(cfgPath)) return {};
1114
1115
  const raw = await fsp.readFile(cfgPath, "utf8");
1115
1116
  let cfg = {};
package/lib/common.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const fsp = fs.promises;
3
3
  const path = require('path');
4
+ const { resolveCanopyConfigPath } = require('./config-path');
4
5
 
5
6
  const CONTENT_DIR = path.resolve('content');
6
7
  const OUT_DIR = path.resolve('site');
@@ -29,7 +30,7 @@ function resolveThemeAppearance() {
29
30
  function readYamlConfigBaseUrl() {
30
31
  try {
31
32
  const y = require('js-yaml');
32
- const p = path.resolve(process.env.CANOPY_CONFIG || 'canopy.yml');
33
+ const p = resolveCanopyConfigPath();
33
34
  if (!fs.existsSync(p)) return '';
34
35
  const raw = fs.readFileSync(p, 'utf8');
35
36
  const data = y.load(raw) || {};
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const yaml = require('js-yaml');
4
4
  const { rootRelativeHref } = require('../common');
5
+ const { resolveCanopyConfigPath } = require('../config-path');
5
6
 
6
7
  function firstLabelString(label) {
7
8
  if (!label) return 'Untitled';
@@ -85,7 +86,7 @@ function findSlugByIdFromDiskSync(nid) {
85
86
  function readFeaturedFromCacheSync() {
86
87
  try {
87
88
  const debug = !!process.env.CANOPY_DEBUG_FEATURED;
88
- const cfg = readYaml(path.resolve('canopy.yml')) || {};
89
+ const cfg = readYaml(resolveCanopyConfigPath()) || {};
89
90
  const featured = Array.isArray(cfg && cfg.featured) ? cfg.featured : [];
90
91
  if (!featured.length) return [];
91
92
  const idx = readJson(path.resolve('.cache/iiif/index.json')) || {};
@@ -0,0 +1,28 @@
1
+ const path = require('path');
2
+
3
+ const DEFAULT_CONFIG_NAME = 'canopy.yml';
4
+
5
+ function resolveWorkspaceRoot(options = {}) {
6
+ const rawCwd = options && options.cwd ? String(options.cwd).trim() : '';
7
+ if (rawCwd) return path.resolve(rawCwd);
8
+ const initCwd = String(process.env.INIT_CWD || '').trim();
9
+ if (initCwd) return path.resolve(initCwd);
10
+ return process.cwd();
11
+ }
12
+
13
+ function resolveCanopyConfigPath(options = {}) {
14
+ const root = resolveWorkspaceRoot(options);
15
+ const explicit = options && options.configPath ? String(options.configPath).trim() : '';
16
+ if (explicit) {
17
+ return path.isAbsolute(explicit) ? explicit : path.resolve(root, explicit);
18
+ }
19
+ const override = options && options.configFile ? String(options.configFile).trim() : '';
20
+ const envOverride = String(process.env.CANOPY_CONFIG || '').trim();
21
+ const fileName = override || envOverride || DEFAULT_CONFIG_NAME;
22
+ return path.resolve(root, fileName);
23
+ }
24
+
25
+ module.exports = {
26
+ resolveWorkspaceRoot,
27
+ resolveCanopyConfigPath,
28
+ };
@@ -12,6 +12,7 @@ const {
12
12
  htmlShell,
13
13
  canopyBodyClassForType,
14
14
  } = require('../common');
15
+ const { resolveCanopyConfigPath } = require('../config-path');
15
16
 
16
17
  const SEARCH_TEMPLATES_ALIAS = '__CANOPY_SEARCH_RESULT_TEMPLATES__';
17
18
  const SEARCH_TEMPLATES_CACHE_DIR = path.resolve('.cache/search');
@@ -444,10 +445,9 @@ async function writeSearchIndex(records) {
444
445
  let resultsConfigEntries = [];
445
446
  try {
446
447
  const yaml = require('js-yaml');
447
- const common = require('../common');
448
- const cfgPath = common.path.resolve(process.env.CANOPY_CONFIG || 'canopy.yml');
449
- if (common.fs.existsSync(cfgPath)) {
450
- const raw = common.fs.readFileSync(cfgPath, 'utf8');
448
+ const cfgPath = resolveCanopyConfigPath();
449
+ if (fs.existsSync(cfgPath)) {
450
+ const raw = fs.readFileSync(cfgPath, 'utf8');
451
451
  const data = yaml.load(raw) || {};
452
452
  const searchCfg = data && data.search ? data.search : {};
453
453
  const tabs = searchCfg && searchCfg.tabs ? searchCfg.tabs : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
package/ui/dist/index.mjs CHANGED
@@ -2187,6 +2187,8 @@ function clampProgress(value) {
2187
2187
 
2188
2188
  // ui/src/content/timeline/Timeline.jsx
2189
2189
  var DAY_MS = 24 * 60 * 60 * 1e3;
2190
+ var DEFAULT_TRACK_HEIGHT = 640;
2191
+ var MIN_HEIGHT_PER_POINT = 220;
2190
2192
  function getThresholdMs(threshold, granularity) {
2191
2193
  const value = Number(threshold);
2192
2194
  if (!Number.isFinite(value) || value <= 0) return 0;
@@ -2316,6 +2318,31 @@ function sanitizePoints(points) {
2316
2318
  };
2317
2319
  }).filter(Boolean);
2318
2320
  }
2321
+ function resolveTrackHeight(height, pointCount) {
2322
+ const minimumPx = Math.max(
2323
+ DEFAULT_TRACK_HEIGHT,
2324
+ pointCount * MIN_HEIGHT_PER_POINT
2325
+ );
2326
+ const fallback = `${minimumPx}px`;
2327
+ if (height == null) return fallback;
2328
+ if (typeof height === "number") {
2329
+ const numeric = Number(height);
2330
+ if (Number.isFinite(numeric)) {
2331
+ return `${Math.max(numeric, pointCount * MIN_HEIGHT_PER_POINT)}px`;
2332
+ }
2333
+ return fallback;
2334
+ }
2335
+ if (typeof height === "string") {
2336
+ const trimmed = height.trim();
2337
+ if (!trimmed) return fallback;
2338
+ const numeric = Number(trimmed);
2339
+ if (Number.isFinite(numeric)) {
2340
+ return `${Math.max(numeric, pointCount * MIN_HEIGHT_PER_POINT)}px`;
2341
+ }
2342
+ return trimmed;
2343
+ }
2344
+ return fallback;
2345
+ }
2319
2346
  function TimelineConnector({ side, isActive, highlight }) {
2320
2347
  const connectorClasses = [
2321
2348
  "canopy-timeline__connector",
@@ -2332,7 +2359,7 @@ function renderResourceSection(point) {
2332
2359
  const manifestCards = Array.isArray(point.manifests) ? point.manifests.filter(Boolean) : [];
2333
2360
  const legacyResources = Array.isArray(point.resources) ? point.resources.filter(Boolean) : [];
2334
2361
  if (!manifestCards.length && !legacyResources.length) return null;
2335
- return /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__resources" }, /* @__PURE__ */ React31.createElement("ul", { className: "canopy-timeline__resources-list" }, manifestCards.map((manifest) => /* @__PURE__ */ React31.createElement("li", { key: manifest.id || manifest.href }, /* @__PURE__ */ React31.createElement(
2362
+ return /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__resources" }, /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__resources-list" }, manifestCards.map((manifest) => /* @__PURE__ */ React31.createElement("div", { key: manifest.id || manifest.href }, /* @__PURE__ */ React31.createElement(
2336
2363
  TeaserCard,
2337
2364
  {
2338
2365
  href: manifest.href,
@@ -2342,7 +2369,7 @@ function renderResourceSection(point) {
2342
2369
  thumbnail: manifest.thumbnail,
2343
2370
  type: manifest.type || "work"
2344
2371
  }
2345
- ))), legacyResources.map((resource, idx) => /* @__PURE__ */ React31.createElement("li", { key: resource.id || resource.href || `legacy-${idx}` }, /* @__PURE__ */ React31.createElement(
2372
+ ))), legacyResources.map((resource, idx) => /* @__PURE__ */ React31.createElement("div", { key: resource.id || resource.href || `legacy-${idx}` }, /* @__PURE__ */ React31.createElement(
2346
2373
  TeaserCard,
2347
2374
  {
2348
2375
  href: resource.href,
@@ -2359,7 +2386,7 @@ function Timeline({
2359
2386
  description,
2360
2387
  range: rangeProp,
2361
2388
  locale: localeProp = "en-US",
2362
- height = 640,
2389
+ height = DEFAULT_TRACK_HEIGHT,
2363
2390
  threshold: thresholdProp = null,
2364
2391
  steps = null,
2365
2392
  points: pointsProp,
@@ -2377,7 +2404,10 @@ function Timeline({
2377
2404
  [rawPoints]
2378
2405
  );
2379
2406
  const localeValue = payload && payload.locale ? payload.locale : localeProp;
2380
- const baseLocale = React31.useMemo(() => createLocale(localeValue), [localeValue]);
2407
+ const baseLocale = React31.useMemo(
2408
+ () => createLocale(localeValue),
2409
+ [localeValue]
2410
+ );
2381
2411
  const rangeInput = payload && payload.range ? payload.range : rangeProp || {};
2382
2412
  const rangeOverrides = React31.useMemo(
2383
2413
  () => deriveRangeOverrides(sanitizedPoints, rangeInput),
@@ -2425,7 +2455,9 @@ function Timeline({
2425
2455
  }),
2426
2456
  [pointsWithPosition, thresholdMs, effectiveRange.granularity, baseLocale]
2427
2457
  );
2428
- const [expandedGroupIds, setExpandedGroupIds] = React31.useState(() => /* @__PURE__ */ new Set());
2458
+ const [expandedGroupIds, setExpandedGroupIds] = React31.useState(
2459
+ () => /* @__PURE__ */ new Set()
2460
+ );
2429
2461
  React31.useEffect(() => {
2430
2462
  setExpandedGroupIds((prev) => {
2431
2463
  if (!prev || prev.size === 0) return prev;
@@ -2449,8 +2481,7 @@ function Timeline({
2449
2481
  return next;
2450
2482
  });
2451
2483
  }, []);
2452
- const resolvedHeight = Number.isFinite(Number(height)) ? Number(height) : 640;
2453
- const trackHeight = Math.max(resolvedHeight, pointsWithPosition.length * 220);
2484
+ const trackHeight = resolveTrackHeight(height, pointsWithPosition.length);
2454
2485
  const containerClasses = ["canopy-timeline", className].filter(Boolean).join(" ");
2455
2486
  const rangeLabel = formatRangeLabel(effectiveRange);
2456
2487
  function renderPointEntry(point) {
@@ -2459,7 +2490,7 @@ function Timeline({
2459
2490
  "canopy-timeline__point-wrapper",
2460
2491
  point.side === "left" ? "canopy-timeline__point-wrapper--left" : "canopy-timeline__point-wrapper--right"
2461
2492
  ].filter(Boolean).join(" ");
2462
- const wrapperStyle = { top: `calc(${point.progress * 100}% - 1rem)` };
2493
+ const wrapperStyle = { top: `${point.progress * 100}%` };
2463
2494
  const cardClasses = [
2464
2495
  "canopy-timeline__point",
2465
2496
  point.id === activeId ? "is-active" : "",
@@ -2491,7 +2522,7 @@ function Timeline({
2491
2522
  "canopy-timeline__point-wrapper",
2492
2523
  entry.side === "left" ? "canopy-timeline__point-wrapper--left" : "canopy-timeline__point-wrapper--right"
2493
2524
  ].filter(Boolean).join(" ");
2494
- const wrapperStyle = { top: `calc(${entry.progress * 100}% - 1rem)` };
2525
+ const wrapperStyle = { top: `${entry.progress * 100}%` };
2495
2526
  const isExpanded = expandedGroupIds.has(entry.id);
2496
2527
  const hasActivePoint = entry.points.some((point) => point.id === activeId);
2497
2528
  const connector = /* @__PURE__ */ React31.createElement(
@@ -2508,7 +2539,7 @@ function Timeline({
2508
2539
  hasActivePoint ? "is-active" : ""
2509
2540
  ].filter(Boolean).join(" ");
2510
2541
  const countLabel = `${entry.count} event${entry.count > 1 ? "s" : ""}`;
2511
- const header = /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__group-header" }, /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__group-summary" }, /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__group-count" }, countLabel), /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__group-range" }, entry.label)), /* @__PURE__ */ React31.createElement(
2542
+ const header = /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__group-header" }, /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__group-summary" }, /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__point-date" }, entry.label), /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__group-count" }, countLabel)), /* @__PURE__ */ React31.createElement(
2512
2543
  "button",
2513
2544
  {
2514
2545
  type: "button",
@@ -2529,7 +2560,7 @@ function Timeline({
2529
2560
  ].filter(Boolean).join(" "),
2530
2561
  onClick: () => setActiveId(point.id)
2531
2562
  },
2532
- /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__group-point-date" }, point.meta.label),
2563
+ /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__point-date" }, point.meta.label),
2533
2564
  /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__group-point-title" }, point.title)
2534
2565
  ))) : null;
2535
2566
  const groupCard = /* @__PURE__ */ React31.createElement("div", { className: groupClasses }, header, groupPoints);
@@ -2549,7 +2580,7 @@ function Timeline({
2549
2580
  {
2550
2581
  className: "canopy-timeline__list",
2551
2582
  role: "list",
2552
- style: { minHeight: `${trackHeight}px` }
2583
+ style: { minHeight: trackHeight }
2553
2584
  },
2554
2585
  /* @__PURE__ */ React31.createElement("div", { className: "canopy-timeline__spine", "aria-hidden": "true" }),
2555
2586
  renderSteps(stepsValue, effectiveRange),
@@ -2570,7 +2601,7 @@ function renderSteps(stepSize, range) {
2570
2601
  "span",
2571
2602
  {
2572
2603
  key: "timeline-step-start",
2573
- className: "canopy-timeline__step canopy-timeline__step--edge",
2604
+ className: "canopy-timeline__step canopy-timeline__step--start",
2574
2605
  style: { top: "0%" },
2575
2606
  "aria-hidden": "true"
2576
2607
  },
@@ -2583,8 +2614,8 @@ function renderSteps(stepSize, range) {
2583
2614
  "span",
2584
2615
  {
2585
2616
  key: "timeline-step-end",
2586
- className: "canopy-timeline__step canopy-timeline__step--edge",
2587
- style: { top: "calc(100% - 1px)" },
2617
+ className: "canopy-timeline__step canopy-timeline__step--end",
2618
+ style: { top: "100%" },
2588
2619
  "aria-hidden": "true"
2589
2620
  },
2590
2621
  /* @__PURE__ */ React31.createElement("span", { className: "canopy-timeline__step-line" }),