@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 +1 -0
- package/lib/build/iiif.js +2 -1
- package/lib/common.js +2 -1
- package/lib/components/featured.js +2 -1
- package/lib/config-path.js +28 -0
- package/lib/search/search.js +4 -4
- package/package.json +1 -1
- package/ui/dist/index.mjs +46 -15
- package/ui/dist/index.mjs.map +2 -2
- package/ui/dist/server.mjs +46 -15
- package/ui/dist/server.mjs.map +2 -2
- package/ui/styles/components/_timeline.scss +72 -50
- package/ui/styles/index.css +53 -49
- package/ui/theme.js +2 -5
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 =
|
|
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 =
|
|
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(
|
|
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
|
+
};
|
package/lib/search/search.js
CHANGED
|
@@ -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
|
|
448
|
-
|
|
449
|
-
|
|
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
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("
|
|
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("
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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-
|
|
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-
|
|
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:
|
|
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--
|
|
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--
|
|
2587
|
-
style: { top: "
|
|
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" }),
|