@glossarist/concept-browser 0.7.69 → 0.7.73
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/astro.config.mjs +55 -0
- package/cli/index.mjs +28 -9
- package/package.json +15 -9
- package/public/favicon.svg +1 -4
- package/scripts/bridge-to-astro.mjs +149 -0
- package/scripts/generate-data.mjs +12 -0
- package/src/adapters/model-bridge.ts +50 -12
- package/src/adapters/non-verbal/glossarist-augment.d.ts +5 -1
- package/src/assets/glossarist-logo-full.svg +1 -0
- package/src/astro-islands/app.ts +3 -0
- package/src/components/AppFooter.vue +3 -2
- package/src/components/AppHeader.vue +4 -7
- package/src/components/AppSidebar.vue +8 -5
- package/src/components/ConceptDetail.vue +12 -0
- package/src/components/DesignationList.vue +15 -4
- package/src/components/LanguageDetail.vue +6 -0
- package/src/components/RelationSphere.vue +36 -32
- package/src/components/site/SeoMeta.astro +23 -0
- package/src/components/site/SiteFooter.astro +14 -0
- package/src/components/site/SiteHeader.astro +25 -0
- package/src/components/site/SiteSidebar.astro +56 -0
- package/src/content.config.ts +88 -0
- package/src/i18n/locales/eng.yml +1 -0
- package/src/i18n/locales/fra.yml +1 -0
- package/src/islands/AppIsland.vue +7 -0
- package/src/islands/BibliographyIsland.vue +26 -0
- package/src/islands/ConceptDetailIsland.vue +27 -0
- package/src/islands/ConceptDiffIsland.vue +214 -0
- package/src/islands/ConceptIsland.vue +102 -0
- package/src/islands/GraphIsland.vue +15 -0
- package/src/islands/GroupRendererIsland.vue +15 -0
- package/src/islands/HeaderIsland.vue +10 -0
- package/src/islands/NonVerbalIsland.vue +75 -0
- package/src/islands/RelationSphereIsland.vue +20 -0
- package/src/islands/SearchIsland.vue +23 -0
- package/src/islands/SeriesCardIsland.vue +12 -0
- package/src/islands/ShortcutsIsland.vue +13 -0
- package/src/islands/SidebarIsland.vue +15 -0
- package/src/islands/SupersessionDiffIsland.vue +267 -0
- package/src/islands/app-entry.ts +8 -0
- package/src/islands/app.ts +4 -0
- package/src/layouts/Default.astro +70 -0
- package/src/lib/site-config.ts +26 -0
- package/src/pages/404.astro +9 -0
- package/src/pages/[...path].astro +29 -0
- package/src/pages/dataset/[id]/concept/[cid].astro +23 -0
- package/src/pages/dataset/[id]/index.astro +16 -0
- package/src/pages/graph.astro +5 -0
- package/src/pages/group/[id]/index.astro +16 -0
- package/src/pages/index.astro +10 -0
- package/src/pages/search.astro +5 -0
- package/src/styles/tailwind.css +503 -0
- package/src/views/ConceptView.vue +18 -23
- package/src/views/StatsView.vue +1 -1
- package/tsconfig.json +2 -1
- package/vite.config.ts +3 -1
- package/postcss.config.js +0 -13
|
@@ -4,8 +4,11 @@ import { designationTypeInfo, normativeStatusInfo, abbreviationDetails, termType
|
|
|
4
4
|
import { relationshipLabel } from '../utils/relationship-categories';
|
|
5
5
|
import { langName } from '../utils/lang';
|
|
6
6
|
import { getDesignationTarget } from '../adapters/model-bridge';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
import CitationDisplay from './CitationDisplay.vue';
|
|
8
9
|
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
|
|
9
12
|
const props = defineProps<{
|
|
10
13
|
designations: Designation[];
|
|
11
14
|
renderedDesignations: Map<string, string>;
|
|
@@ -57,10 +60,18 @@ function resolvedLabel(dr: { content: string | null; ref?: { source: string | nu
|
|
|
57
60
|
<span v-if="d.system" class="badge text-[10px] bg-gray-50 text-gray-600">system: {{ d.system }}</span>
|
|
58
61
|
</div>
|
|
59
62
|
<div v-if="d.sources?.length" class="mt-1 space-y-0.5">
|
|
60
|
-
<div v-for="(ds, dsi) in d.sources" :key="'ds'+dsi" class="text-xs text-ink-400
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
<div v-for="(ds, dsi) in d.sources" :key="'ds'+dsi" class="text-xs text-ink-400">
|
|
64
|
+
<div class="flex items-center gap-1.5">
|
|
65
|
+
<span v-if="ds.type" class="badge text-[9px]" :class="sourceTypeInfo(ds.type).color">{{ sourceTypeInfo(ds.type).label }}</span>
|
|
66
|
+
<CitationDisplay v-if="ds.origin" :citation="ds.origin" :register-id="registerId" />
|
|
67
|
+
<span v-else-if="ds.modification" class="text-ink-300">{{ ds.modification }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div v-if="ds.sourced_from?.length" class="ml-4 mt-0.5">
|
|
70
|
+
<span class="text-ink-300 text-[9px]">{{ t('concept.sourcedFrom') }}:</span>
|
|
71
|
+
<div v-for="(sf, sfi) in ds.sourced_from" :key="'sf'+sfi" class="ml-2">
|
|
72
|
+
<CitationDisplay v-if="sf" :citation="sf" :register-id="registerId" />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
64
75
|
</div>
|
|
65
76
|
</div>
|
|
66
77
|
<div v-if="d.related?.length" class="mt-0.5 space-y-0.5">
|
|
@@ -176,6 +176,12 @@ function handleContentClick(e: MouseEvent) {
|
|
|
176
176
|
<div class="text-ink-700">
|
|
177
177
|
<CitationDisplay v-if="src.origin" :citation="src.origin" />
|
|
178
178
|
</div>
|
|
179
|
+
<div v-if="src.sourced_from?.length" class="text-xs text-ink-400 mt-1">
|
|
180
|
+
<span class="text-ink-300">{{ t('concept.sourcedFrom') }}:</span>
|
|
181
|
+
<div v-for="(sf, sfi) in src.sourced_from" :key="'sf'+sfi" class="ml-2">
|
|
182
|
+
<CitationDisplay v-if="sf" :citation="sf" />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
179
185
|
</div>
|
|
180
186
|
</div>
|
|
181
187
|
</div>
|
|
@@ -238,6 +238,7 @@ function hashSeed(s: string): number {
|
|
|
238
238
|
function buildGraph() {
|
|
239
239
|
const focusUri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
|
|
240
240
|
const focusId = props.concept?.id || props.registerId;
|
|
241
|
+
const focusNodeId = `${props.registerId}/${focusId}`;
|
|
241
242
|
const lang = sphereLang.value;
|
|
242
243
|
const maxDepth = degree.value;
|
|
243
244
|
|
|
@@ -255,7 +256,7 @@ function buildGraph() {
|
|
|
255
256
|
props.concept?.localization?.('eng') ??
|
|
256
257
|
(conceptLangs.length > 0 ? props.concept?.localization?.(conceptLangs[0]) : undefined);
|
|
257
258
|
const focusNode: SNode = {
|
|
258
|
-
id:
|
|
259
|
+
id: focusNodeId,
|
|
259
260
|
term: getPreferredTerm(focusLc ?? null, focusId),
|
|
260
261
|
ref: props.manifest.ref || props.registerId,
|
|
261
262
|
register: props.registerId,
|
|
@@ -265,9 +266,9 @@ function buildGraph() {
|
|
|
265
266
|
vx: 0, vy: 0, vz: 0,
|
|
266
267
|
};
|
|
267
268
|
|
|
268
|
-
const nodeMap = new Map<string, SNode>([[
|
|
269
|
+
const nodeMap = new Map<string, SNode>([[focusNodeId, focusNode]]);
|
|
269
270
|
const visited: Map<string, number> = new Map([[focusUri, 0]]); // uri → depth
|
|
270
|
-
const idToUri = new Map<string, string>([[
|
|
271
|
+
const idToUri = new Map<string, string>([[focusNodeId, focusUri]]); // for inter-neighbor edge pass
|
|
271
272
|
const resultLinks: SLink[] = [];
|
|
272
273
|
const linkKeys = new Set<string>(); /* dedupe edges */
|
|
273
274
|
const queue: Array<{ uri: string; depth: number; id: string }> = [];
|
|
@@ -280,7 +281,7 @@ function buildGraph() {
|
|
|
280
281
|
const otherUri = isOutgoing ? edge.target : edge.source;
|
|
281
282
|
if (!otherUri || visited.has(otherUri)) continue;
|
|
282
283
|
const parsed = UriRouter.parseUri(otherUri);
|
|
283
|
-
if (!parsed || parsed.conceptId === focusId) continue;
|
|
284
|
+
if (!parsed || (parsed.conceptId === focusId && parsed.registerId === props.registerId)) continue;
|
|
284
285
|
visited.set(otherUri, 1);
|
|
285
286
|
depth1List.push({ uri: otherUri, parsed, isOutgoing, edge });
|
|
286
287
|
}
|
|
@@ -288,9 +289,10 @@ function buildGraph() {
|
|
|
288
289
|
/* Second pass: place depth-1 nodes on an even ring around the focus. */
|
|
289
290
|
const depth1Total = depth1List.length;
|
|
290
291
|
depth1List.forEach(({ uri, parsed, isOutgoing, edge }, i) => {
|
|
292
|
+
const nid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
291
293
|
const pos = fibonacciSpherePosition(1, i, depth1Total, hashSeed(parsed.conceptId));
|
|
292
|
-
nodeMap.set(
|
|
293
|
-
id:
|
|
294
|
+
nodeMap.set(nid, {
|
|
295
|
+
id: nid,
|
|
294
296
|
term: edge.label || parsed.conceptId,
|
|
295
297
|
ref: parsed.registerId,
|
|
296
298
|
register: parsed.registerId,
|
|
@@ -299,9 +301,9 @@ function buildGraph() {
|
|
|
299
301
|
x: pos.x, y: pos.y, z: pos.z,
|
|
300
302
|
vx: 0, vy: 0, vz: 0,
|
|
301
303
|
});
|
|
302
|
-
idToUri.set(
|
|
303
|
-
const src = isOutgoing ?
|
|
304
|
-
const tgt = isOutgoing ?
|
|
304
|
+
idToUri.set(nid, uri);
|
|
305
|
+
const src = isOutgoing ? focusNodeId : nid;
|
|
306
|
+
const tgt = isOutgoing ? nid : focusNodeId;
|
|
305
307
|
const key = `${src}\0${tgt}\0${edge.type}`;
|
|
306
308
|
if (!linkKeys.has(key)) {
|
|
307
309
|
linkKeys.add(key);
|
|
@@ -313,7 +315,7 @@ function buildGraph() {
|
|
|
313
315
|
depth: 1,
|
|
314
316
|
});
|
|
315
317
|
}
|
|
316
|
-
queue.push({ uri, depth: 1, id: parsed.conceptId });
|
|
318
|
+
queue.push({ uri, depth: 1, id: `${parsed.registerId}/${parsed.conceptId}` });
|
|
317
319
|
});
|
|
318
320
|
|
|
319
321
|
/* BFS deeper levels using graph engine (has cross-dataset edges) */
|
|
@@ -332,14 +334,17 @@ function buildGraph() {
|
|
|
332
334
|
const otherUri = isOutgoing ? edge.target : edge.source;
|
|
333
335
|
if (!otherUri || visited.has(otherUri)) continue;
|
|
334
336
|
const parsed = UriRouter.parseUri(otherUri);
|
|
335
|
-
if (!parsed
|
|
337
|
+
if (!parsed) continue;
|
|
338
|
+
const childId = `${parsed.registerId}/${parsed.conceptId}`;
|
|
339
|
+
if (childId === focusNodeId || nodeMap.has(childId)) continue;
|
|
336
340
|
visited.set(otherUri, depth + 1);
|
|
337
341
|
children.push({ uri: otherUri, parsed, isOutgoing, edge });
|
|
338
342
|
}
|
|
339
343
|
children.forEach(({ uri: cUri, parsed, isOutgoing, edge }, i) => {
|
|
344
|
+
const cid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
340
345
|
const pos = fibonacciSpherePosition(depth + 1, i, children.length, hashSeed(parsed.conceptId));
|
|
341
|
-
nodeMap.set(
|
|
342
|
-
id:
|
|
346
|
+
nodeMap.set(cid, {
|
|
347
|
+
id: cid,
|
|
343
348
|
term: parsed.conceptId,
|
|
344
349
|
ref: parsed.registerId,
|
|
345
350
|
register: parsed.registerId,
|
|
@@ -348,9 +353,9 @@ function buildGraph() {
|
|
|
348
353
|
x: pos.x, y: pos.y, z: pos.z,
|
|
349
354
|
vx: 0, vy: 0, vz: 0,
|
|
350
355
|
});
|
|
351
|
-
idToUri.set(
|
|
352
|
-
const src = isOutgoing ? id :
|
|
353
|
-
const tgt = isOutgoing ?
|
|
356
|
+
idToUri.set(cid, cUri);
|
|
357
|
+
const src = isOutgoing ? id : cid;
|
|
358
|
+
const tgt = isOutgoing ? cid : id;
|
|
354
359
|
const key = `${src}\0${tgt}\0${edge.type}`;
|
|
355
360
|
if (!linkKeys.has(key)) {
|
|
356
361
|
linkKeys.add(key);
|
|
@@ -362,7 +367,7 @@ function buildGraph() {
|
|
|
362
367
|
depth: depth + 1,
|
|
363
368
|
});
|
|
364
369
|
}
|
|
365
|
-
queue.push({ uri: cUri, depth: depth + 1, id:
|
|
370
|
+
queue.push({ uri: cUri, depth: depth + 1, id: cid });
|
|
366
371
|
});
|
|
367
372
|
}
|
|
368
373
|
|
|
@@ -795,13 +800,8 @@ function renderDOM() {
|
|
|
795
800
|
The top bar gives instant dataset identification at any angle; the
|
|
796
801
|
tinted background ties same-dataset cards together visually. */
|
|
797
802
|
el.style.setProperty('--ds-color', dsColor);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
el.style.background = `linear-gradient(180deg, color-mix(in srgb, ${dsColor} 10%, var(--surface)) 0%, var(--surface) 30%)`;
|
|
801
|
-
} else {
|
|
802
|
-
/* Focus keeps a solid surface — it's already distinguished by the
|
|
803
|
-
blue ring; adding a tint would compete. */
|
|
804
|
-
}
|
|
803
|
+
el.style.borderTop = `4px solid ${dsColor}`;
|
|
804
|
+
el.style.background = `linear-gradient(180deg, color-mix(in srgb, ${dsColor} 10%, var(--surface)) 0%, var(--surface) 30%)`;
|
|
805
805
|
|
|
806
806
|
el.innerHTML = `
|
|
807
807
|
<div class="sp-ref">${n.register} · ${n.conceptId}</div>
|
|
@@ -978,7 +978,7 @@ function navigate(newConceptId: string, newRegisterId: string) {
|
|
|
978
978
|
/* ── Watch concept change ───────────────────────────────── */
|
|
979
979
|
/* When props.concept changes (via store.viewConcept from sphere click,
|
|
980
980
|
or external navigation), animate the transition with a SLERP tween. */
|
|
981
|
-
let
|
|
981
|
+
let lastConceptKey = '';
|
|
982
982
|
|
|
983
983
|
/* UI language change → sync sphere language (unless user has explicitly
|
|
984
984
|
chosen a different one via the Language selector). We track that with
|
|
@@ -1017,10 +1017,12 @@ async function rebuildForLangChange() {
|
|
|
1017
1017
|
await loadNeighborTerms();
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
|
-
watch(() => props.concept?.id, (newId) => {
|
|
1021
|
-
if (!newId
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1020
|
+
watch(() => [props.concept?.id, props.registerId] as const, ([newId, newReg]) => {
|
|
1021
|
+
if (!newId) return;
|
|
1022
|
+
const key = `${newReg}/${newId}`;
|
|
1023
|
+
if (key === lastConceptKey) return;
|
|
1024
|
+
const wasFirst = !lastConceptKey;
|
|
1025
|
+
lastConceptKey = key;
|
|
1024
1026
|
|
|
1025
1027
|
if (wasFirst) {
|
|
1026
1028
|
/* Initial load — fresh placement, no tween */
|
|
@@ -1290,7 +1292,7 @@ async function waitForCanvas(): Promise<void> {
|
|
|
1290
1292
|
|
|
1291
1293
|
onMounted(async () => {
|
|
1292
1294
|
window.addEventListener('resize', onResize);
|
|
1293
|
-
|
|
1295
|
+
lastConceptKey = `${props.registerId}/${props.concept?.id || ''}`;
|
|
1294
1296
|
await waitForCanvas();
|
|
1295
1297
|
|
|
1296
1298
|
/* Setup pan/zoom via d3-zoom on the canvas.
|
|
@@ -1493,8 +1495,10 @@ defineExpose({ navigate });
|
|
|
1493
1495
|
}
|
|
1494
1496
|
:deep(.sp-rel) { margin-left: auto; }
|
|
1495
1497
|
:deep(.sp-node.focus) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
+
z-index: 20; cursor: default;
|
|
1499
|
+
outline: 2px solid var(--blue);
|
|
1500
|
+
outline-offset: 3px;
|
|
1501
|
+
box-shadow: 0 0 12px 4px rgba(37,99,235,0.25), 0 0 24px 8px rgba(37,99,235,0.12);
|
|
1498
1502
|
}
|
|
1499
1503
|
:deep(.sp-focus-badge) {
|
|
1500
1504
|
position: absolute; top: -10px; right: 12px; font-size: 9px; color: white;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
// SEO meta tags for Astro pages.
|
|
3
|
+
interface Props {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
canonicalUrl?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
}
|
|
10
|
+
const { title, description = '', canonicalUrl, type = 'website', image } = Astro.props;
|
|
11
|
+
const site = Astro.site?.toString().replace(/\/$/, '') || '';
|
|
12
|
+
const canonical = canonicalUrl || `${site}${Astro.url.pathname}`;
|
|
13
|
+
const ogImage = image || `${site}/og-image.png`;
|
|
14
|
+
---
|
|
15
|
+
<meta property="og:title" content={title} />
|
|
16
|
+
<meta property="og:description" content={description} />
|
|
17
|
+
<meta property="og:type" content={type} />
|
|
18
|
+
<meta property="og:url" content={canonical} />
|
|
19
|
+
<meta property="og:image" content={ogImage} />
|
|
20
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
21
|
+
<meta name="twitter:title" content={title} />
|
|
22
|
+
<meta name="twitter:description" content={description} />
|
|
23
|
+
<link rel="canonical" href={canonical} />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getSiteConfig } from '../../lib/site-config';
|
|
3
|
+
const siteConfig = await getSiteConfig();
|
|
4
|
+
---
|
|
5
|
+
<footer class="border-t border-ink-100/60 dark:border-ink-700/40 py-6 mt-12">
|
|
6
|
+
<div class="max-w-[1600px] mx-auto px-4 flex items-center justify-between text-xs text-ink-400">
|
|
7
|
+
<span>{siteConfig.title || 'Glossarist'}</span>
|
|
8
|
+
<div class="flex items-center gap-4">
|
|
9
|
+
<a href="/about" class="hover:text-ink-600 dark:hover:text-ink-300">About</a>
|
|
10
|
+
<a href="/contributors" class="hover:text-ink-600 dark:hover:text-ink-300">Contributors</a>
|
|
11
|
+
{siteConfig.branding?.poweredBy && <span>{siteConfig.branding.poweredBy}</span>}
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</footer>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props { title: string }
|
|
3
|
+
const { title } = Astro.props;
|
|
4
|
+
---
|
|
5
|
+
<header class="border-b border-ink-100/60 dark:border-ink-700/40 bg-surface dark:bg-ink-900 sticky top-0 z-30">
|
|
6
|
+
<div class="max-w-[1600px] mx-auto px-4 h-14 flex items-center justify-between gap-4">
|
|
7
|
+
<div class="flex items-center gap-3">
|
|
8
|
+
<button id="sidebar-toggle" class="lg:hidden p-1.5 rounded hover:bg-ink-100/50 dark:hover:bg-ink-800/40" aria-label="Toggle sidebar">
|
|
9
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
|
10
|
+
</button>
|
|
11
|
+
<a href="/" class="text-lg font-bold text-ink-800 dark:text-ink-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
|
12
|
+
{title}
|
|
13
|
+
</a>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex items-center gap-2">
|
|
16
|
+
<a href="/search" class="p-1.5 rounded hover:bg-ink-100/50 dark:hover:bg-ink-800/40 text-ink-500 dark:text-ink-400" aria-label="Search">
|
|
17
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
18
|
+
</a>
|
|
19
|
+
<button id="theme-toggle" class="p-1.5 rounded hover:bg-ink-100/50 dark:hover:bg-ink-800/40 text-ink-500 dark:text-ink-400" aria-label="Toggle dark mode">
|
|
20
|
+
<svg class="w-5 h-5 dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>
|
|
21
|
+
<svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { getSiteConfig } from '../../lib/site-config';
|
|
4
|
+
|
|
5
|
+
interface Props { activeDataset?: string; activeGroup?: string }
|
|
6
|
+
const { activeDataset, activeGroup } = Astro.props;
|
|
7
|
+
|
|
8
|
+
const datasets = await getCollection('datasets');
|
|
9
|
+
const groups = await getCollection('groups');
|
|
10
|
+
const siteConfig = await getSiteConfig();
|
|
11
|
+
---
|
|
12
|
+
<aside id="sidebar" class="hidden lg:block w-64 shrink-0 border-r border-ink-100/60 dark:border-ink-700/40 min-h-[calc(100vh-3.5rem)] p-3">
|
|
13
|
+
<nav class="space-y-4 text-sm">
|
|
14
|
+
<a href="/" class={`block px-2 py-1.5 rounded ${!activeDataset && !activeGroup ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : 'text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-800/40'}`}>
|
|
15
|
+
Home
|
|
16
|
+
</a>
|
|
17
|
+
<a href="/search" class="block px-2 py-1.5 rounded text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-800/40">Search</a>
|
|
18
|
+
<a href="/graph" class="block px-2 py-1.5 rounded text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-800/40">Graph</a>
|
|
19
|
+
|
|
20
|
+
{groups.length > 0 ? (
|
|
21
|
+
groups.map(g => (
|
|
22
|
+
<div>
|
|
23
|
+
<a href={`/group/${g.data.id}`} class={`block px-2 py-1.5 font-medium rounded ${activeGroup === g.data.id ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' : 'text-ink-700 dark:text-ink-200 hover:bg-ink-50 dark:hover:bg-ink-800/40'}`}>
|
|
24
|
+
{g.data.label}
|
|
25
|
+
</a>
|
|
26
|
+
<ul class="mt-1 space-y-0.5 ml-2">
|
|
27
|
+
{g.data.datasets.map((id: string) => {
|
|
28
|
+
const ds = datasets.find(d => d.data.id === id);
|
|
29
|
+
if (!ds) return null;
|
|
30
|
+
return (
|
|
31
|
+
<li>
|
|
32
|
+
<a href={`/dataset/${id}`} class={`block px-2 py-1 rounded text-xs ${activeDataset === id ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 font-medium' : 'text-ink-500 dark:text-ink-400 hover:bg-ink-50 dark:hover:bg-ink-800/30'}`}>
|
|
33
|
+
{ds.data.title.length > 28 ? ds.data.title.slice(0, 25) + '…' : ds.data.title}
|
|
34
|
+
</a>
|
|
35
|
+
</li>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
))
|
|
41
|
+
) : (
|
|
42
|
+
<div>
|
|
43
|
+
<h3 class="px-2 py-1 text-xs font-semibold text-ink-400 uppercase tracking-wider">Datasets</h3>
|
|
44
|
+
<ul class="space-y-0.5">
|
|
45
|
+
{datasets.map(ds => (
|
|
46
|
+
<li>
|
|
47
|
+
<a href={`/dataset/${ds.data.id}`} class={`block px-2 py-1 rounded text-xs ${activeDataset === ds.data.id ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-300 font-medium' : 'text-ink-500 dark:text-ink-400 hover:bg-ink-50 dark:hover:bg-ink-800/30'}`}>
|
|
48
|
+
{ds.data.title.length > 28 ? ds.data.title.slice(0, 25) + '…' : ds.data.title}
|
|
49
|
+
</a>
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</nav>
|
|
56
|
+
</aside>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineCollection, z } from 'astro:content';
|
|
2
|
+
import { glob } from 'astro/loaders';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
const cbContent = resolve(process.cwd(), '.cb-content');
|
|
6
|
+
|
|
7
|
+
const datasets = defineCollection({
|
|
8
|
+
loader: glob({ pattern: '**/*.json', base: resolve(cbContent, 'datasets') }),
|
|
9
|
+
schema: z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
title: z.string(),
|
|
12
|
+
description: z.string(),
|
|
13
|
+
uri: z.string().optional(),
|
|
14
|
+
uriBase: z.string().optional().default(''),
|
|
15
|
+
year: z.number().optional(),
|
|
16
|
+
status: z.string().optional(),
|
|
17
|
+
ref: z.string().optional(),
|
|
18
|
+
refAliases: z.array(z.string()).optional(),
|
|
19
|
+
owner: z.string().optional(),
|
|
20
|
+
languages: z.array(z.string()).default(['eng']),
|
|
21
|
+
conceptCount: z.number().default(0),
|
|
22
|
+
color: z.union([z.string(), z.object({ light: z.string(), dark: z.string() })]).optional(),
|
|
23
|
+
tags: z.array(z.string()).default([]),
|
|
24
|
+
sections: z.array(z.any()).optional(),
|
|
25
|
+
sourceRepo: z.string().optional(),
|
|
26
|
+
lastUpdated: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const groups = defineCollection({
|
|
31
|
+
loader: glob({ pattern: '**/*.json', base: resolve(cbContent, 'groups') }),
|
|
32
|
+
schema: z.object({
|
|
33
|
+
id: z.string(),
|
|
34
|
+
label: z.string(),
|
|
35
|
+
kind: z.enum(['lineage', 'topic', 'family', 'collection', 'default']).default('default'),
|
|
36
|
+
description: z.string().optional(),
|
|
37
|
+
current: z.string().optional(),
|
|
38
|
+
datasets: z.array(z.string()).default([]),
|
|
39
|
+
color: z.union([z.string(), z.object({ light: z.string(), dark: z.string() })]).optional(),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const concepts = defineCollection({
|
|
44
|
+
loader: glob({ pattern: '**/*.json', base: resolve(cbContent, 'concepts') }),
|
|
45
|
+
schema: z.object({
|
|
46
|
+
registerId: z.string(),
|
|
47
|
+
conceptId: z.string(),
|
|
48
|
+
uri: z.string(),
|
|
49
|
+
status: z.string().default('valid'),
|
|
50
|
+
designations: z.record(z.string(), z.string()).default({}),
|
|
51
|
+
eng: z.string().optional(),
|
|
52
|
+
definition: z.record(z.string(), z.string()).optional(),
|
|
53
|
+
groups: z.array(z.string()).default([]),
|
|
54
|
+
tags: z.array(z.string()).default([]),
|
|
55
|
+
languages: z.array(z.string()).optional(),
|
|
56
|
+
localizations: z.record(z.string(), z.object({
|
|
57
|
+
languageCode: z.string().optional(),
|
|
58
|
+
terms: z.array(z.union([
|
|
59
|
+
z.string(),
|
|
60
|
+
z.object({ designation: z.string(), normativeStatus: z.string().optional() }),
|
|
61
|
+
])).default([]),
|
|
62
|
+
definitions: z.array(z.union([
|
|
63
|
+
z.string(),
|
|
64
|
+
z.object({ content: z.string() }),
|
|
65
|
+
])).default([]),
|
|
66
|
+
notes: z.array(z.union([
|
|
67
|
+
z.string(),
|
|
68
|
+
z.object({ content: z.string() }),
|
|
69
|
+
])).default([]),
|
|
70
|
+
examples: z.array(z.union([
|
|
71
|
+
z.string(),
|
|
72
|
+
z.object({ content: z.string() }),
|
|
73
|
+
])).default([]),
|
|
74
|
+
})).optional(),
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const pages = defineCollection({
|
|
79
|
+
loader: glob({ pattern: '**/*.{md,json}', base: resolve(cbContent, 'pages') }),
|
|
80
|
+
schema: z.object({
|
|
81
|
+
title: z.string(),
|
|
82
|
+
type: z.string().optional(),
|
|
83
|
+
html: z.string().optional(),
|
|
84
|
+
route: z.string().optional(),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const collections = { datasets, groups, concepts, pages };
|
package/src/i18n/locales/eng.yml
CHANGED
|
@@ -64,6 +64,7 @@ concept.metadata: Metadata
|
|
|
64
64
|
concept.noData: No data available for this language.
|
|
65
65
|
concept.lifecycleDates: Lifecycle dates
|
|
66
66
|
concept.conceptSources: Concept sources
|
|
67
|
+
concept.sourcedFrom: Sourced from
|
|
67
68
|
concept.incoming: Incoming
|
|
68
69
|
concept.outgoing: Outgoing
|
|
69
70
|
concept.lang: lang
|
package/src/i18n/locales/fra.yml
CHANGED
|
@@ -64,6 +64,7 @@ concept.metadata: Métadonnées
|
|
|
64
64
|
concept.noData: Aucune donnée disponible pour cette langue.
|
|
65
65
|
concept.lifecycleDates: Dates du cycle de vie
|
|
66
66
|
concept.conceptSources: Sources du concept
|
|
67
|
+
concept.sourcedFrom: Issu de
|
|
67
68
|
concept.incoming: Entrantes
|
|
68
69
|
concept.outgoing: Sortantes
|
|
69
70
|
concept.lang: lang.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section v-if="entries.length" class="bibliography-island">
|
|
3
|
+
<h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">Bibliography</h2>
|
|
4
|
+
<ul class="space-y-1">
|
|
5
|
+
<li v-for="entry in entries" :key="entry.id" class="text-sm flex items-start gap-2">
|
|
6
|
+
<span class="text-ink-400 font-mono text-xs shrink-0">[{{ entry.id }}]</span>
|
|
7
|
+
<div>
|
|
8
|
+
<span class="text-ink-600 dark:text-ink-300">{{ entry.reference }}</span>
|
|
9
|
+
<a v-if="entry.link" :href="entry.link" target="_blank" class="text-blue-600 hover:underline text-xs ml-2">↗</a>
|
|
10
|
+
</div>
|
|
11
|
+
</li>
|
|
12
|
+
</ul>
|
|
13
|
+
</section>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
defineProps<{
|
|
18
|
+
entries: Array<{
|
|
19
|
+
id: string;
|
|
20
|
+
reference: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
link?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
}>;
|
|
25
|
+
}>();
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Full concept detail using the REAL ConceptDetail.vue -->
|
|
3
|
+
<ErrorBoundary title="Concept view failed" :retry-key="conceptUri">
|
|
4
|
+
<ConceptDetail
|
|
5
|
+
:concept="concept"
|
|
6
|
+
:register-id="registerId"
|
|
7
|
+
:concept-uri="conceptUri"
|
|
8
|
+
/>
|
|
9
|
+
</ErrorBoundary>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { onErrorCaptured } from 'vue';
|
|
14
|
+
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
15
|
+
import ErrorBoundary from '../components/ErrorBoundary.vue';
|
|
16
|
+
|
|
17
|
+
defineProps<{
|
|
18
|
+
concept: any;
|
|
19
|
+
registerId: string;
|
|
20
|
+
conceptUri: string;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
onErrorCaptured((err) => {
|
|
24
|
+
console.error('ConceptDetail island error:', err);
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
</script>
|