@canopy-iiif/app 0.6.28 → 0.7.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/assets.js +30 -0
- package/lib/build/build.js +184 -0
- package/lib/{dev.js → build/dev.js} +5 -5
- package/lib/build/facets.js +19 -0
- package/lib/{iiif.js → build/iiif.js} +222 -373
- package/lib/{mdx.js → build/mdx.js} +13 -10
- package/lib/build/pages.js +141 -0
- package/lib/build/runtimes.js +58 -0
- package/lib/build/search-index.js +42 -0
- package/lib/build/search.js +219 -0
- package/lib/build/styles.js +99 -0
- package/lib/{thumbnail.js → iiif/thumbnail.js} +0 -1
- package/lib/index.js +2 -3
- package/lib/{search.js → search/search.js} +8 -8
- package/package.json +1 -1
- package/lib/build.js +0 -762
- package/lib/components/IIIFCard.js +0 -102
- package/lib/runtime/command-entry.jsx +0 -44
- /package/lib/{devtoast.config.json → build/devtoast.config.json} +0 -0
- /package/lib/{devtoast.css → build/devtoast.css} +0 -0
- /package/lib/{log.js → build/log.js} +0 -0
- /package/lib/{search-app.jsx → search/search-app.jsx} +0 -0
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
CACHE_DIR,
|
|
11
11
|
ensureDirSync,
|
|
12
12
|
withBase,
|
|
13
|
-
} = require("
|
|
13
|
+
} = require("../common");
|
|
14
14
|
const yaml = require("js-yaml");
|
|
15
15
|
|
|
16
16
|
function parseFrontmatter(src) {
|
|
@@ -135,11 +135,14 @@ async function loadAppWrapper() {
|
|
|
135
135
|
// Prefer a component that renders its children, but do not hard-fail if probe fails.
|
|
136
136
|
let ok = false;
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
138
|
+
// Try to render the probe inside an MDXProvider with UI server components
|
|
139
|
+
const components = await loadUiComponents();
|
|
140
|
+
const MDXProvider = await getMdxProvider();
|
|
141
|
+
const probeChild = React.createElement("span", { "data-canopy-probe": "1" });
|
|
142
|
+
const probeTree = React.createElement(App || (() => null), null, probeChild);
|
|
143
|
+
const probe = MDXProvider
|
|
144
|
+
? React.createElement(MDXProvider, { components }, probeTree)
|
|
145
|
+
: probeTree;
|
|
143
146
|
const out = ReactDOMServer.renderToStaticMarkup(probe);
|
|
144
147
|
ok = !!(out && out.indexOf("data-canopy-probe") !== -1);
|
|
145
148
|
} catch (_) {
|
|
@@ -265,7 +268,7 @@ async function ensureClientRuntime() {
|
|
|
265
268
|
// like the Clover Viewer when placeholders are present in the HTML.
|
|
266
269
|
let esbuild = null;
|
|
267
270
|
try {
|
|
268
|
-
esbuild = require("
|
|
271
|
+
esbuild = require("../../ui/node_modules/esbuild");
|
|
269
272
|
} catch (_) {
|
|
270
273
|
try {
|
|
271
274
|
esbuild = require("esbuild");
|
|
@@ -389,7 +392,7 @@ async function ensureClientRuntime() {
|
|
|
389
392
|
// and renders a Slider for each.
|
|
390
393
|
async function ensureFacetsRuntime() {
|
|
391
394
|
let esbuild = null;
|
|
392
|
-
try { esbuild = require("
|
|
395
|
+
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
393
396
|
ensureDirSync(OUT_DIR);
|
|
394
397
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
395
398
|
ensureDirSync(scriptsDir);
|
|
@@ -632,14 +635,14 @@ async function ensureSliderRuntime() {
|
|
|
632
635
|
async function ensureReactGlobals() {
|
|
633
636
|
let esbuild = null;
|
|
634
637
|
try {
|
|
635
|
-
esbuild = require("
|
|
638
|
+
esbuild = require("../../ui/node_modules/esbuild");
|
|
636
639
|
} catch (_) {
|
|
637
640
|
try {
|
|
638
641
|
esbuild = require("esbuild");
|
|
639
642
|
} catch (_) {}
|
|
640
643
|
}
|
|
641
644
|
if (!esbuild) return;
|
|
642
|
-
const { path } = require("
|
|
645
|
+
const { path } = require("../common");
|
|
643
646
|
ensureDirSync(OUT_DIR);
|
|
644
647
|
const scriptsDir = path.join(OUT_DIR, "scripts");
|
|
645
648
|
ensureDirSync(scriptsDir);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const { fs, fsp, path, CONTENT_DIR, OUT_DIR, ensureDirSync, htmlShell } = require('../common');
|
|
2
|
+
const { log } = require('./log');
|
|
3
|
+
const mdx = require('./mdx');
|
|
4
|
+
|
|
5
|
+
// Cache: dir -> frontmatter data for _layout.mdx in that dir
|
|
6
|
+
const LAYOUT_META = new Map();
|
|
7
|
+
|
|
8
|
+
async function getNearestDirLayoutMeta(filePath) {
|
|
9
|
+
const startDir = path.dirname(filePath);
|
|
10
|
+
let dir = startDir;
|
|
11
|
+
while (dir && dir.startsWith(CONTENT_DIR)) {
|
|
12
|
+
const key = path.resolve(dir);
|
|
13
|
+
if (LAYOUT_META.has(key)) {
|
|
14
|
+
const cached = LAYOUT_META.get(key);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
}
|
|
17
|
+
const candidate = path.join(dir, '_layout.mdx');
|
|
18
|
+
if (fs.existsSync(candidate)) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fsp.readFile(candidate, 'utf8');
|
|
21
|
+
const fm = mdx.parseFrontmatter(raw);
|
|
22
|
+
const data = fm && fm.data ? fm.data : null;
|
|
23
|
+
LAYOUT_META.set(key, data);
|
|
24
|
+
if (data) return data;
|
|
25
|
+
} catch (_) {
|
|
26
|
+
LAYOUT_META.set(key, null);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
LAYOUT_META.set(key, null);
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mapContentPathToOutput(filePath) {
|
|
39
|
+
const rel = path.relative(CONTENT_DIR, filePath);
|
|
40
|
+
const outRel = rel.replace(/\.mdx$/i, '.html');
|
|
41
|
+
return path.join(OUT_DIR, outRel);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
45
|
+
const source = await fsp.readFile(filePath, 'utf8');
|
|
46
|
+
const title = mdx.extractTitle(source);
|
|
47
|
+
const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, extraProps);
|
|
48
|
+
const cssRel = path
|
|
49
|
+
.relative(path.dirname(outPath), path.join(OUT_DIR, 'styles', 'styles.css'))
|
|
50
|
+
.split(path.sep)
|
|
51
|
+
.join('/');
|
|
52
|
+
const needsHydrateViewer = body.includes('data-canopy-viewer');
|
|
53
|
+
const needsHydrateSlider = body.includes('data-canopy-slider');
|
|
54
|
+
const needsCommand = true; // command runtime is global
|
|
55
|
+
const needsFacets = body.includes('data-canopy-related-items');
|
|
56
|
+
const viewerRel = needsHydrateViewer
|
|
57
|
+
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-viewer.js')).split(path.sep).join('/')
|
|
58
|
+
: null;
|
|
59
|
+
const sliderRel = (needsHydrateSlider || needsFacets)
|
|
60
|
+
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-slider.js')).split(path.sep).join('/')
|
|
61
|
+
: null;
|
|
62
|
+
const facetsRel = needsFacets
|
|
63
|
+
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
|
|
64
|
+
: null;
|
|
65
|
+
const commandRel = needsCommand
|
|
66
|
+
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-command.js')).split(path.sep).join('/')
|
|
67
|
+
: null;
|
|
68
|
+
let jsRel = null;
|
|
69
|
+
if (needsFacets && sliderRel) jsRel = sliderRel;
|
|
70
|
+
else if (viewerRel) jsRel = viewerRel;
|
|
71
|
+
else if (sliderRel) jsRel = sliderRel;
|
|
72
|
+
else if (facetsRel) jsRel = facetsRel;
|
|
73
|
+
const needsReact = !!(needsHydrateViewer || needsHydrateSlider || needsFacets);
|
|
74
|
+
let vendorTag = '';
|
|
75
|
+
if (needsReact) {
|
|
76
|
+
try {
|
|
77
|
+
await mdx.ensureReactGlobals();
|
|
78
|
+
const vendorAbs = path.join(OUT_DIR, 'scripts', 'react-globals.js');
|
|
79
|
+
let vendorRel = path.relative(path.dirname(outPath), vendorAbs).split(path.sep).join('/');
|
|
80
|
+
try { const stv = fs.statSync(vendorAbs); vendorRel += `?v=${Math.floor(stv.mtimeMs || Date.now())}`; } catch (_) {}
|
|
81
|
+
vendorTag = `<script src="${vendorRel}"></script>`;
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const { BASE_PATH } = require('../common');
|
|
86
|
+
if (BASE_PATH) vendorTag = `<script>window.CANOPY_BASE_PATH=${JSON.stringify(BASE_PATH)}</script>` + vendorTag;
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
let headExtra = head;
|
|
89
|
+
const extraScripts = [];
|
|
90
|
+
if (facetsRel && jsRel !== facetsRel) extraScripts.push(`<script defer src="${facetsRel}"></script>`);
|
|
91
|
+
if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
92
|
+
if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
93
|
+
if (commandRel && jsRel !== commandRel) extraScripts.push(`<script defer src="${commandRel}"></script>`);
|
|
94
|
+
if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
|
|
95
|
+
const html = htmlShell({ title, body, cssHref: cssRel || 'styles.css', scriptHref: jsRel, headExtra: vendorTag + headExtra });
|
|
96
|
+
const { applyBaseToHtml } = require('../common');
|
|
97
|
+
return applyBaseToHtml(html);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function processContentEntry(absPath, pagesMetadata = []) {
|
|
101
|
+
const stat = await fsp.stat(absPath);
|
|
102
|
+
if (stat.isDirectory()) return;
|
|
103
|
+
if (/\.mdx$/i.test(absPath)) {
|
|
104
|
+
if (mdx.isReservedFile(absPath)) return;
|
|
105
|
+
const outPath = mapContentPathToOutput(absPath);
|
|
106
|
+
ensureDirSync(path.dirname(outPath));
|
|
107
|
+
try {
|
|
108
|
+
try { log(`• Processing MDX ${absPath}\n`, 'blue'); } catch (_) {}
|
|
109
|
+
const base = path.basename(absPath);
|
|
110
|
+
const extra = base.toLowerCase() === 'sitemap.mdx' ? { pages: pagesMetadata } : {};
|
|
111
|
+
const html = await renderContentMdxToHtml(absPath, outPath, extra);
|
|
112
|
+
await fsp.writeFile(outPath, html || '', 'utf8');
|
|
113
|
+
try { log(`✓ Built ${path.relative(process.cwd(), outPath)}\n`, 'green'); } catch (_) {}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('MDX build failed for', absPath, '\n', err.message);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const rel = path.relative(CONTENT_DIR, absPath);
|
|
119
|
+
const outPath = path.join(OUT_DIR, rel);
|
|
120
|
+
ensureDirSync(path.dirname(outPath));
|
|
121
|
+
await fsp.copyFile(absPath, outPath);
|
|
122
|
+
try { log(`• Copied ${path.relative(process.cwd(), outPath)}\n`, 'cyan', { dim: true }); } catch (_) {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function buildContentTree(dir, pagesMetadata = []) {
|
|
127
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
const p = path.join(dir, e.name);
|
|
130
|
+
if (e.isDirectory()) await buildContentTree(p, pagesMetadata);
|
|
131
|
+
else if (e.isFile()) await processContentEntry(p, pagesMetadata);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
getNearestDirLayoutMeta,
|
|
137
|
+
mapContentPathToOutput,
|
|
138
|
+
renderContentMdxToHtml,
|
|
139
|
+
processContentEntry,
|
|
140
|
+
buildContentTree,
|
|
141
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
const { logLine } = require('./log');
|
|
3
|
+
const { fs, fsp, path, OUT_DIR, ensureDirSync } = require('../common');
|
|
4
|
+
|
|
5
|
+
async function prepareAllRuntimes() {
|
|
6
|
+
const mdx = require('./mdx');
|
|
7
|
+
try { await mdx.ensureClientRuntime(); } catch (_) {}
|
|
8
|
+
try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
|
|
9
|
+
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
10
|
+
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
11
|
+
try { await ensureCommandFallback(); } catch (_) {}
|
|
12
|
+
try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { prepareAllRuntimes };
|
|
16
|
+
|
|
17
|
+
async function ensureCommandFallback() {
|
|
18
|
+
const cmdOut = path.join(OUT_DIR, 'scripts', 'canopy-command.js');
|
|
19
|
+
ensureDirSync(path.dirname(cmdOut));
|
|
20
|
+
const fallback = `
|
|
21
|
+
(function(){
|
|
22
|
+
function ready(fn){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }
|
|
23
|
+
function parseProps(el){ try{ const s = el.querySelector('script[type="application/json"]'); if(s) return JSON.parse(s.textContent||'{}'); }catch(_){ } return {}; }
|
|
24
|
+
function norm(s){ try{ return String(s||'').toLowerCase(); }catch(_){ return ''; } }
|
|
25
|
+
function withBase(href){ try{ var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; if(!bp) return href; if(/^https?:/i.test(href)) return href; var clean = href.replace(/^\\/+/, ''); return (bp.endsWith('/') ? bp.slice(0,-1) : bp) + '/' + clean; } catch(_){ return href; } }
|
|
26
|
+
function rootBase(){ try { var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; return bp && bp.endsWith('/') ? bp.slice(0,-1) : bp; } catch(_) { return ''; } }
|
|
27
|
+
function createUI(){ var root=document.createElement('div'); root.setAttribute('data-canopy-command-fallback',''); root.style.cssText='position:fixed;inset:0;display:none;align-items:flex-start;justify-content:center;background:rgba(0,0,0,0.3);z-index:9999;padding-top:10vh;'; root.innerHTML='<div style="position:relative;background:#fff;min-width:320px;max-width:720px;width:90%;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.2);overflow:hidden;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif"><button id="cpclose" aria-label="Close" style="position:absolute;top:8px;right:8px;border:1px solid #e5e7eb;background:#fff;border-radius:6px;padding:2px 6px;cursor:pointer">×</button><div style="padding:10px 12px;border-bottom:1px solid #e5e7eb"><input id="cpq" type="text" placeholder="Search…" style="width:100%;padding:8px 10px;border:1px solid #e5e7eb;border-radius:6px;outline:none"/></div><div id="cplist" style="max-height:50vh;overflow:auto;padding:6px 0"></div></div>'; document.body.appendChild(root); return root; }
|
|
28
|
+
async function loadRecords(){ try{ var v=''; try{ var m = await fetch(rootBase() + '/api/index.json').then(function(r){return r&&r.ok?r.json():null;}).catch(function(){return null;}); v=(m&&m.version)||''; }catch(_){} var res = await fetch(rootBase() + '/api/search-index.json' + (v?('?v='+encodeURIComponent(v)):'')).catch(function(){return null;}); var j = res && res.ok ? await res.json().catch(function(){return[];}) : []; return Array.isArray(j) ? j : (j && j.records) || []; } catch(_){ return []; } }
|
|
29
|
+
ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var overlay=createUI(); var input=overlay.querySelector('#cpq'); var list=overlay.querySelector('#cplist'); var btnClose=overlay.querySelector('#cpclose'); var records = await loadRecords(); function render(items){ list.innerHTML=''; if(!items.length){ list.innerHTML='<div style="padding:10px 12px;color:#6b7280">No results found.</div>'; return; } var groups=new Map(); items.forEach(function(r){ var t=String(r.type||'page'); if(!groups.has(t)) groups.set(t, []); groups.get(t).push(r); }); function gl(t){ if(t==='work') return 'Works'; if(t==='page') return 'Pages'; return t.charAt(0).toUpperCase()+t.slice(1);} var ordered=[].concat(groupOrder.filter(function(t){return groups.has(t);})).concat(Array.from(groups.keys()).filter(function(t){return groupOrder.indexOf(t)===-1;})); ordered.forEach(function(t){ var hdr=document.createElement('div'); hdr.textContent=gl(t); hdr.style.cssText='padding:6px 12px;font-weight:600;color:#374151'; list.appendChild(hdr); groups.get(t).forEach(function(r){ var it=document.createElement('div'); it.tabIndex=0; it.style.cssText='display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer'; var thumb=(String(r.type||'')==='work' && r.thumbnail)?r.thumbnail:''; if(thumb){ var img=document.createElement('img'); img.src=thumb; img.alt=''; img.style.cssText='width:40px;height:40px;object-fit:cover;border-radius:4px'; it.appendChild(img);} var span=document.createElement('span'); span.textContent=r.title||r.href; it.appendChild(span); it.onmouseenter=function(){ it.style.background='#f8fafc'; }; it.onmouseleave=function(){ it.style.background='transparent'; }; it.onclick=function(){ try{ window.location.href = withBase(String(r.href||'')); }catch(_){} overlay.style.display='none'; }; list.appendChild(it); }); }); }
|
|
30
|
+
function filterAndShow(q){ try{ var qq=norm(q); if(!qq){ render([]); return; } var out=[]; for(var i=0;i<records.length;i++){ var r=records[i]; var title=String((r&&r.title)||''); if(!title) continue; if(norm(title).indexOf(qq)!==-1) out.push(r); if(out.length>=maxResults) break; } render(out); }catch(_){} }
|
|
31
|
+
btnClose.addEventListener('click', function(){ overlay.style.display='none'; });
|
|
32
|
+
input.addEventListener('input', function(){ filterAndShow(input.value||''); });
|
|
33
|
+
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ overlay.style.display='none'; }});
|
|
34
|
+
host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); overlay.style.display='flex'; input.focus(); filterAndShow(input.value||''); }});
|
|
35
|
+
var btn = document.querySelector('[data-canopy-command-trigger]'); if(btn){ btn.addEventListener('click', function(){ overlay.style.display='flex'; input.focus(); filterAndShow(input.value||''); }); }
|
|
36
|
+
});
|
|
37
|
+
})();
|
|
38
|
+
`;
|
|
39
|
+
await fsp.writeFile(cmdOut, fallback, 'utf8');
|
|
40
|
+
try { logLine(`✓ Wrote ${path.relative(process.cwd(), cmdOut)} (fallback)`, 'cyan'); } catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
|
|
44
|
+
const search = require('../search/search');
|
|
45
|
+
try { logLine(`• Writing search runtime${label ? ' ('+label+')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
|
|
46
|
+
let timedOut = false;
|
|
47
|
+
await Promise.race([
|
|
48
|
+
search.ensureSearchRuntime(),
|
|
49
|
+
new Promise((_, reject) => setTimeout(() => { timedOut = true; reject(new Error('timeout')); }, Number(timeoutMs)))
|
|
50
|
+
]).catch(() => {
|
|
51
|
+
try { console.warn(`Search: Bundling runtime timed out${label ? ' ('+label+')' : ''}, skipping`); } catch (_) {}
|
|
52
|
+
});
|
|
53
|
+
if (timedOut) {
|
|
54
|
+
try { logLine(`! Search runtime not bundled${label ? ' ('+label+')' : ''}\n`, 'yellow'); } catch (_) {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { prepareAllRuntimes, ensureCommandFallback, prepareSearchRuntime };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
const { logLine } = require('./log');
|
|
3
|
+
|
|
4
|
+
function pagesToRecords(pageRecords) {
|
|
5
|
+
const list = Array.isArray(pageRecords) ? pageRecords : [];
|
|
6
|
+
return list
|
|
7
|
+
.filter((p) => p && p.href && p.searchInclude)
|
|
8
|
+
.map((p) => ({
|
|
9
|
+
title: p.title || p.href,
|
|
10
|
+
href: p.href,
|
|
11
|
+
type: p.searchType || 'page',
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function maybeMockRecords() {
|
|
16
|
+
if (process.env.CANOPY_MOCK_SEARCH !== '1') return null;
|
|
17
|
+
const mock = [];
|
|
18
|
+
const svg = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><rect width="400" height="300" fill="#dbeafe"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="24" fill="#1d4ed8">Mock</text></svg>');
|
|
19
|
+
const thumb = `data:image/svg+xml;charset=utf-8,${svg}`;
|
|
20
|
+
for (let i = 1; i <= 120; i++) {
|
|
21
|
+
mock.push({ title: `Mock Work #${i}`, href: `works/mock-${i}.html`, type: 'work', thumbnail: thumb });
|
|
22
|
+
}
|
|
23
|
+
mock.push({ title: 'Mock Doc A', href: 'getting-started/index.html', type: 'docs' });
|
|
24
|
+
mock.push({ title: 'Mock Doc B', href: 'getting-started/example.html', type: 'docs' });
|
|
25
|
+
mock.push({ title: 'Mock Page', href: 'index.html', type: 'page' });
|
|
26
|
+
return mock;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function buildSearchIndex(iiifRecords, pageRecords) {
|
|
30
|
+
const search = require('../search/search');
|
|
31
|
+
const iiif = Array.isArray(iiifRecords) ? iiifRecords : [];
|
|
32
|
+
const mdx = pagesToRecords(pageRecords);
|
|
33
|
+
let combined = [...iiif, ...mdx];
|
|
34
|
+
const mock = maybeMockRecords();
|
|
35
|
+
if (mock) combined = mock;
|
|
36
|
+
try { logLine(`• Building search index (${combined.length})...`, 'blue'); } catch (_) {}
|
|
37
|
+
await search.writeSearchIndex(combined);
|
|
38
|
+
try { logLine('✓ Search index built', 'cyan'); } catch (_) {}
|
|
39
|
+
return combined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { buildSearchIndex };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const { fs, fsp, path, OUT_DIR, ensureDirSync, absoluteUrl } = require('../common');
|
|
2
|
+
const slugify = require('slugify');
|
|
3
|
+
|
|
4
|
+
function firstI18nString(x) {
|
|
5
|
+
if (!x) return '';
|
|
6
|
+
if (typeof x === 'string') return x;
|
|
7
|
+
try {
|
|
8
|
+
const keys = Object.keys(x || {});
|
|
9
|
+
if (!keys.length) return '';
|
|
10
|
+
const arr = x[keys[0]];
|
|
11
|
+
if (Array.isArray(arr) && arr.length) return String(arr[0]);
|
|
12
|
+
} catch (_) {}
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function buildFacetsForWorks(combined, labelWhitelist) {
|
|
17
|
+
const facetsDir = path.resolve('.cache/iiif');
|
|
18
|
+
ensureDirSync(facetsDir);
|
|
19
|
+
const map = new Map(); // label -> Map(value -> Set(docIdx))
|
|
20
|
+
const labels = Array.isArray(labelWhitelist) ? labelWhitelist.map(String) : [];
|
|
21
|
+
if (!Array.isArray(combined)) combined = [];
|
|
22
|
+
for (let i = 0; i < combined.length; i++) {
|
|
23
|
+
const rec = combined[i];
|
|
24
|
+
if (!rec || String(rec.type) !== 'work') continue;
|
|
25
|
+
const href = String(rec.href || '');
|
|
26
|
+
const m = href.match(/^works\/(.+)\.html$/i);
|
|
27
|
+
if (!m) continue;
|
|
28
|
+
const slug = m[1];
|
|
29
|
+
const p = path.resolve('.cache/iiif/manifests', slug + '.json');
|
|
30
|
+
if (!fs.existsSync(p)) continue;
|
|
31
|
+
let manifest = null;
|
|
32
|
+
try { manifest = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { manifest = null; }
|
|
33
|
+
const meta = Array.isArray(manifest && manifest.metadata) ? manifest.metadata : [];
|
|
34
|
+
for (const entry of meta) {
|
|
35
|
+
if (!entry) continue;
|
|
36
|
+
const label = firstI18nString(entry.label);
|
|
37
|
+
const valueRaw = entry.value && (typeof entry.value === 'string' ? entry.value : firstI18nString(entry.value));
|
|
38
|
+
if (!label || !valueRaw) continue;
|
|
39
|
+
if (labels.length && !labels.includes(label)) continue; // only configured labels
|
|
40
|
+
const values = [];
|
|
41
|
+
try {
|
|
42
|
+
if (typeof entry.value === 'string') values.push(entry.value);
|
|
43
|
+
else {
|
|
44
|
+
const obj = entry.value || {};
|
|
45
|
+
for (const k of Object.keys(obj)) {
|
|
46
|
+
const arr = Array.isArray(obj[k]) ? obj[k] : [];
|
|
47
|
+
for (const v of arr) if (v) values.push(String(v));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (_) { values.push(valueRaw); }
|
|
51
|
+
if (!map.has(label)) map.set(label, new Map());
|
|
52
|
+
const vmap = map.get(label);
|
|
53
|
+
for (const v of values) {
|
|
54
|
+
const key = String(v);
|
|
55
|
+
if (!vmap.has(key)) vmap.set(key, new Set());
|
|
56
|
+
vmap.get(key).add(i); // doc index in combined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const [label, vmap] of map.entries()) {
|
|
62
|
+
const labelSlug = slugify(label || 'label', { lower: true, strict: true, trim: true });
|
|
63
|
+
const values = [];
|
|
64
|
+
for (const [value, set] of vmap.entries()) {
|
|
65
|
+
const docs = Array.from(set.values()).sort((a, b) => a - b);
|
|
66
|
+
values.push({ value, slug: slugify(value || 'value', { lower: true, strict: true, trim: true }), doc_count: docs.length, docs });
|
|
67
|
+
}
|
|
68
|
+
values.sort((a, b) => b.doc_count - a.doc_count || String(a.value).localeCompare(String(b.value)));
|
|
69
|
+
out.push({ label, slug: labelSlug, values });
|
|
70
|
+
}
|
|
71
|
+
out.sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
72
|
+
const dest = path.join(facetsDir, 'facets.json');
|
|
73
|
+
await fsp.writeFile(dest, JSON.stringify(out, null, 2), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function writeFacetCollections(labelWhitelist, combined) {
|
|
77
|
+
const facetsPath = path.resolve('.cache/iiif/facets.json');
|
|
78
|
+
if (!fs.existsSync(facetsPath)) return;
|
|
79
|
+
let facets = [];
|
|
80
|
+
try { facets = JSON.parse(fs.readFileSync(facetsPath, 'utf8')) || []; } catch (_) { facets = []; }
|
|
81
|
+
const labels = new Set((Array.isArray(labelWhitelist) ? labelWhitelist : []).map(String));
|
|
82
|
+
const apiRoot = path.join(OUT_DIR, 'api');
|
|
83
|
+
const facetRoot = path.join(apiRoot, 'facet');
|
|
84
|
+
ensureDirSync(facetRoot);
|
|
85
|
+
const list = (Array.isArray(facets) ? facets : []).filter((f) => !labels.size || labels.has(String(f && f.label)));
|
|
86
|
+
const labelIndexItems = [];
|
|
87
|
+
for (const f of list) {
|
|
88
|
+
if (!f || !f.label || !Array.isArray(f.values)) continue;
|
|
89
|
+
const label = String(f.label);
|
|
90
|
+
const labelSlug = slugify(label || 'label', { lower: true, strict: true, trim: true });
|
|
91
|
+
const labelDir = path.join(facetRoot, labelSlug);
|
|
92
|
+
ensureDirSync(labelDir);
|
|
93
|
+
for (const v of f.values) {
|
|
94
|
+
if (!v || typeof v !== 'object') continue;
|
|
95
|
+
const value = String(v.value || '');
|
|
96
|
+
const valueSlug = slugify(value || 'value', { lower: true, strict: true, trim: true });
|
|
97
|
+
const dest = path.join(labelDir, valueSlug + '.json');
|
|
98
|
+
const docIdxs = Array.isArray(v.docs) ? v.docs : [];
|
|
99
|
+
const items = [];
|
|
100
|
+
for (const idx of docIdxs) {
|
|
101
|
+
const rec = combined && Array.isArray(combined) ? combined[idx] : null;
|
|
102
|
+
if (!rec || String(rec.type) !== 'work') continue;
|
|
103
|
+
const id = String(rec.id || '');
|
|
104
|
+
const title = String(rec.title || rec.href || '');
|
|
105
|
+
const thumb = String(rec.thumbnail || '');
|
|
106
|
+
const href = String(rec.href || '');
|
|
107
|
+
const homepageId = absoluteUrl('/' + href.replace(/^\/?/, ''));
|
|
108
|
+
const item = { id, type: 'Manifest', label: { none: [title] } };
|
|
109
|
+
if (thumb) item.thumbnail = [{ id: thumb, type: 'Image' }];
|
|
110
|
+
item.homepage = [{ id: homepageId, type: 'Text', label: { none: [title] } }];
|
|
111
|
+
items.push(item);
|
|
112
|
+
}
|
|
113
|
+
const selfId = absoluteUrl(`/api/facet/${labelSlug}/${valueSlug}.json`);
|
|
114
|
+
const parentId = absoluteUrl(`/api/facet/${labelSlug}.json`);
|
|
115
|
+
const homepage = absoluteUrl(`/search?${encodeURIComponent(labelSlug)}=${encodeURIComponent(valueSlug)}`);
|
|
116
|
+
const col = {
|
|
117
|
+
'@context': 'https://iiif.io/api/presentation/3/context.json',
|
|
118
|
+
id: selfId,
|
|
119
|
+
type: 'Collection',
|
|
120
|
+
label: { none: [value] },
|
|
121
|
+
items,
|
|
122
|
+
partOf: [{ id: parentId, type: 'Collection' }],
|
|
123
|
+
summary: { none: [label] },
|
|
124
|
+
homepage: [{ id: homepage, type: 'Text', label: { none: [value] } }],
|
|
125
|
+
};
|
|
126
|
+
await fsp.writeFile(dest, JSON.stringify(col, null, 2), 'utf8');
|
|
127
|
+
}
|
|
128
|
+
const labelIndexDest = path.join(facetRoot, labelSlug + '.json');
|
|
129
|
+
const labelItems = (f.values || []).map((v) => ({
|
|
130
|
+
id: absoluteUrl(`/api/facet/${labelSlug}/${slugify(String(v && v.value || ''), { lower: true, strict: true, trim: true })}.json`),
|
|
131
|
+
type: 'Collection',
|
|
132
|
+
label: { none: [String(v && v.value || '')] },
|
|
133
|
+
summary: { none: [label] },
|
|
134
|
+
}));
|
|
135
|
+
const labelIndex = {
|
|
136
|
+
'@context': 'https://iiif.io/api/presentation/3/context.json',
|
|
137
|
+
id: absoluteUrl(`/api/facet/${labelSlug}.json`),
|
|
138
|
+
type: 'Collection',
|
|
139
|
+
label: { none: [label] },
|
|
140
|
+
items: labelItems,
|
|
141
|
+
};
|
|
142
|
+
await fsp.writeFile(labelIndexDest, JSON.stringify(labelIndex, null, 2), 'utf8');
|
|
143
|
+
labelIndexItems.push({ id: absoluteUrl(`/api/facet/${labelSlug}.json`), type: 'Collection', label: { none: [label] } });
|
|
144
|
+
}
|
|
145
|
+
const facetIndex = {
|
|
146
|
+
'@context': 'https://iiif.io/api/presentation/3/context.json',
|
|
147
|
+
id: absoluteUrl('/api/facet/index.json'),
|
|
148
|
+
type: 'Collection',
|
|
149
|
+
label: { none: ['Facets'] },
|
|
150
|
+
items: labelIndexItems,
|
|
151
|
+
};
|
|
152
|
+
await fsp.writeFile(path.join(facetRoot, 'index.json'), JSON.stringify(facetIndex, null, 2), 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function writeFacetsSearchApi() {
|
|
156
|
+
const src = path.resolve('.cache/iiif/facets.json');
|
|
157
|
+
if (!fs.existsSync(src)) return;
|
|
158
|
+
let data = null;
|
|
159
|
+
try { data = JSON.parse(fs.readFileSync(src, 'utf8')); } catch (_) { data = null; }
|
|
160
|
+
if (!data) return;
|
|
161
|
+
const destDir = path.join(OUT_DIR, 'api', 'search');
|
|
162
|
+
ensureDirSync(destDir);
|
|
163
|
+
const dest = path.join(destDir, 'facets.json');
|
|
164
|
+
await fsp.writeFile(dest, JSON.stringify(data, null, 2), 'utf8');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { buildFacetsForWorks, writeFacetCollections, writeFacetsSearchApi };
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async function collectMdxPageRecords() {
|
|
172
|
+
const { fs, fsp, path, CONTENT_DIR } = require('../common');
|
|
173
|
+
const mdx = require('./mdx');
|
|
174
|
+
const pagesHelpers = require('./pages');
|
|
175
|
+
const pages = [];
|
|
176
|
+
async function walk(dir) {
|
|
177
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
178
|
+
for (const e of entries) {
|
|
179
|
+
const p = path.join(dir, e.name);
|
|
180
|
+
if (e.isDirectory()) await walk(p);
|
|
181
|
+
else if (e.isFile() && /\.mdx$/i.test(p) && !mdx.isReservedFile(p)) {
|
|
182
|
+
const base = path.basename(p).toLowerCase();
|
|
183
|
+
const src = await fsp.readFile(p, 'utf8');
|
|
184
|
+
const fm = mdx.parseFrontmatter(src);
|
|
185
|
+
const title = mdx.extractTitle(src);
|
|
186
|
+
const rel = path.relative(CONTENT_DIR, p).replace(/\.mdx$/i, '.html');
|
|
187
|
+
if (base !== 'sitemap.mdx') {
|
|
188
|
+
const href = rel.split(path.sep).join('/');
|
|
189
|
+
const underSearch = /^search\//i.test(href) || href.toLowerCase() === 'search.html';
|
|
190
|
+
let include = !underSearch;
|
|
191
|
+
let resolvedType = null;
|
|
192
|
+
const pageFm = fm && fm.data ? fm.data : null;
|
|
193
|
+
if (pageFm) {
|
|
194
|
+
if (pageFm.search === false) include = false;
|
|
195
|
+
if (Object.prototype.hasOwnProperty.call(pageFm, 'type')) {
|
|
196
|
+
if (pageFm.type) resolvedType = String(pageFm.type);
|
|
197
|
+
else include = false;
|
|
198
|
+
} else {
|
|
199
|
+
include = false; // frontmatter present w/o type => exclude per policy
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (include && !resolvedType) {
|
|
203
|
+
const layoutMeta = await pagesHelpers.getNearestDirLayoutMeta(p);
|
|
204
|
+
if (layoutMeta && layoutMeta.type) resolvedType = String(layoutMeta.type);
|
|
205
|
+
}
|
|
206
|
+
if (include && !resolvedType) {
|
|
207
|
+
if (!pageFm) resolvedType = 'page';
|
|
208
|
+
}
|
|
209
|
+
pages.push({ title, href, searchInclude: include && !!resolvedType, searchType: resolvedType || undefined });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await walk(CONTENT_DIR);
|
|
215
|
+
return pages;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
module.exports = { buildFacetsForWorks, writeFacetCollections, writeFacetsSearchApi, collectMdxPageRecords };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const { fs, fsp, path, OUT_DIR, CONTENT_DIR, ensureDirSync } = require('../common');
|
|
2
|
+
|
|
3
|
+
async function ensureStyles() {
|
|
4
|
+
const stylesDir = path.join(OUT_DIR, 'styles');
|
|
5
|
+
const dest = path.join(stylesDir, 'styles.css');
|
|
6
|
+
const customContentCss = path.join(CONTENT_DIR, '_styles.css');
|
|
7
|
+
const appStylesDir = path.join(process.cwd(), 'app', 'styles');
|
|
8
|
+
const customAppCss = path.join(appStylesDir, 'index.css');
|
|
9
|
+
ensureDirSync(stylesDir);
|
|
10
|
+
|
|
11
|
+
const root = process.cwd();
|
|
12
|
+
const twConfigsRoot = [
|
|
13
|
+
path.join(root, 'tailwind.config.js'),
|
|
14
|
+
path.join(root, 'tailwind.config.cjs'),
|
|
15
|
+
path.join(root, 'tailwind.config.mjs'),
|
|
16
|
+
path.join(root, 'tailwind.config.ts'),
|
|
17
|
+
];
|
|
18
|
+
const twConfigsApp = [
|
|
19
|
+
path.join(appStylesDir, 'tailwind.config.js'),
|
|
20
|
+
path.join(appStylesDir, 'tailwind.config.cjs'),
|
|
21
|
+
path.join(appStylesDir, 'tailwind.config.mjs'),
|
|
22
|
+
path.join(appStylesDir, 'tailwind.config.ts'),
|
|
23
|
+
];
|
|
24
|
+
let configPath = [...twConfigsApp, ...twConfigsRoot].find((p) => {
|
|
25
|
+
try { return fs.existsSync(p); } catch (_) { return false; }
|
|
26
|
+
});
|
|
27
|
+
if (!configPath) {
|
|
28
|
+
try {
|
|
29
|
+
const { CACHE_DIR } = require('../common');
|
|
30
|
+
const genDir = path.join(CACHE_DIR, 'tailwind');
|
|
31
|
+
ensureDirSync(genDir);
|
|
32
|
+
const genCfg = path.join(genDir, 'tailwind.config.js');
|
|
33
|
+
const cfg = `module.exports = {\n presets: [require('@canopy-iiif/app/ui/canopy-iiif-preset')],\n content: [\n './content/**/*.{mdx,html}',\n './site/**/*.html',\n './site/**/*.js',\n './packages/app/ui/**/*.{js,jsx,ts,tsx}',\n './packages/app/lib/iiif/components/**/*.{js,jsx}',\n ],\n theme: { extend: {} },\n plugins: [require('@canopy-iiif/app/ui/canopy-iiif-plugin')],\n};\n`;
|
|
34
|
+
fs.writeFileSync(genCfg, cfg, 'utf8');
|
|
35
|
+
configPath = genCfg;
|
|
36
|
+
} catch (_) { configPath = null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputCss = fs.existsSync(customAppCss)
|
|
40
|
+
? customAppCss
|
|
41
|
+
: (fs.existsSync(customContentCss) ? customContentCss : null);
|
|
42
|
+
|
|
43
|
+
let generatedInput = null;
|
|
44
|
+
if (!inputCss) {
|
|
45
|
+
try {
|
|
46
|
+
const { CACHE_DIR } = require('../common');
|
|
47
|
+
const genDir = path.join(CACHE_DIR, 'tailwind');
|
|
48
|
+
ensureDirSync(genDir);
|
|
49
|
+
generatedInput = path.join(genDir, 'index.css');
|
|
50
|
+
const css = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
|
|
51
|
+
fs.writeFileSync(generatedInput, css, 'utf8');
|
|
52
|
+
} catch (_) { generatedInput = null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveTailwindCli() {
|
|
56
|
+
try {
|
|
57
|
+
const cliJs = require.resolve('tailwindcss/lib/cli.js');
|
|
58
|
+
return { cmd: process.execPath, args: [cliJs] };
|
|
59
|
+
} catch (_) {}
|
|
60
|
+
try {
|
|
61
|
+
const localBin = path.join(process.cwd(), 'node_modules', '.bin', process.platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss');
|
|
62
|
+
if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
|
|
63
|
+
} catch (_) {}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function buildTailwindCli({ input, output, config, minify = true }) {
|
|
67
|
+
try {
|
|
68
|
+
const cli = resolveTailwindCli();
|
|
69
|
+
if (!cli) return false;
|
|
70
|
+
const { spawnSync } = require('child_process');
|
|
71
|
+
const args = ['-i', input, '-o', output];
|
|
72
|
+
if (config) args.push('-c', config);
|
|
73
|
+
if (minify) args.push('--minify');
|
|
74
|
+
const res = spawnSync(cli.cmd, [...cli.args, ...args], { stdio: 'inherit', env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: '1' } });
|
|
75
|
+
return !!res && res.status === 0;
|
|
76
|
+
} catch (_) { return false; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (configPath && (inputCss || generatedInput)) {
|
|
80
|
+
const ok = buildTailwindCli({ input: inputCss || generatedInput, output: dest, config: configPath, minify: true });
|
|
81
|
+
if (ok) return; // Tailwind compiled CSS
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isTailwindSource(p) {
|
|
85
|
+
try { const s = fs.readFileSync(p, 'utf8'); return /@tailwind\s+(base|components|utilities)/.test(s); } catch (_) { return false; }
|
|
86
|
+
}
|
|
87
|
+
if (fs.existsSync(customAppCss)) {
|
|
88
|
+
if (!isTailwindSource(customAppCss)) { await fsp.copyFile(customAppCss, dest); return; }
|
|
89
|
+
}
|
|
90
|
+
if (fs.existsSync(customContentCss)) {
|
|
91
|
+
if (!isTailwindSource(customContentCss)) { await fsp.copyFile(customContentCss, dest); return; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const css = `:root{--max-w:760px;--muted:#6b7280}*{box-sizing:border-box}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif;max-width:var(--max-w);margin:2rem auto;padding:0 1rem;line-height:1.6}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}.site-header,.site-footer{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:1rem 0;border-bottom:1px solid #e5e7eb}.site-footer{border-bottom:0;border-top:1px solid #e5e7eb;color:var(--muted)}.brand{font-weight:600}.content pre{background:#f6f8fa;padding:1rem;overflow:auto}.content code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;background:#f6f8fa;padding:.1rem .3rem;border-radius:4px}.tabs{display:flex;gap:.5rem;align-items:center;border-bottom:1px solid #e5e7eb;margin:.5rem 0}.tab{background:none;border:0;color:#374151;padding:.25rem .5rem;border-radius:.375rem;cursor:pointer}.tab:hover{color:#111827}.tab-active{color:#2563eb;border:1px solid #e5e7eb;border-bottom:0;background:#fff}.masonry{column-gap:1rem;column-count:1}@media(min-width:768px){.masonry{column-count:2}}@media(min-width:1024px){.masonry{column-count:3}}.masonry>*{break-inside:avoid;margin-bottom:1rem;display:block}[data-grid-variant=masonry]{column-gap:var(--grid-gap,1rem);column-count:var(--cols-base,1)}@media(min-width:768px){[data-grid-variant=masonry]{column-count:var(--cols-md,2)}}@media(min-width:1024px){[data-grid-variant=masonry]{column-count:var(--cols-lg,3)}}[data-grid-variant=masonry]>*{break-inside:avoid;margin-bottom:var(--grid-gap,1rem);display:block}[data-grid-variant=grid]{display:grid;grid-template-columns:repeat(var(--cols-base,1),minmax(0,1fr));gap:var(--grid-gap,1rem)}@media(min-width:768px){[data-grid-variant=grid]{grid-template-columns:repeat(var(--cols-md,2),minmax(0,1fr))}}@media(min-width:1024px){[data-grid-variant=grid]{grid-template-columns:repeat(var(--cols-lg,3),minmax(0,1fr))}}`;
|
|
95
|
+
await fsp.writeFile(dest, css, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { ensureStyles };
|
|
99
|
+
|
package/lib/index.js
CHANGED