@glossarist/concept-browser 0.7.50 → 0.7.52
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/cli/index.mjs +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/figure-bridge.ts +22 -23
- package/src/adapters/non-verbal/formula-bridge.ts +11 -9
- package/src/adapters/non-verbal/glossarist-augment.d.ts +133 -0
- package/src/adapters/non-verbal/index.ts +12 -9
- package/src/adapters/non-verbal/kind.ts +2 -1
- package/src/adapters/non-verbal/table-bridge.ts +12 -10
- package/src/adapters/non-verbal/types.ts +36 -54
- package/src/adapters/non-verbal-resolver.ts +6 -3
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/NonVerbalRepDisplay.vue +2 -2
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/components/figure/FigureDisplay.vue +16 -15
- package/src/components/figure/FigureImages.vue +38 -16
- package/src/components/figure/figure-image-pick.ts +1 -1
- package/src/components/figure/figure-layout.ts +1 -1
- package/src/components/formula/FormulaDisplay.vue +11 -9
- package/src/components/formula/FormulaExpression.vue +4 -4
- package/src/components/non-verbal/NonVerbalCaption.vue +5 -5
- package/src/components/non-verbal/NonVerbalSources.vue +3 -11
- package/src/components/table/TableDisplay.vue +6 -4
- package/src/components/table/TableMarkup.vue +1 -1
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/use-non-verbal-entity.ts +2 -1
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +183 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
* exactly one instance per app.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type { NonVerbalKind } from './non-verbal/types';
|
|
19
|
+
import type { NonVerbalEntity } from 'glossarist';
|
|
19
20
|
import { KIND_TO_DIR, KIND_TO_BRIDGE } from './non-verbal/kind';
|
|
20
21
|
import { anchorId } from '../utils/non-verbal-anchor';
|
|
22
|
+
import { NonVerbalEntityNotFoundError } from '../errors';
|
|
21
23
|
|
|
22
|
-
export type {
|
|
24
|
+
export type { NonVerbalKind } from './non-verbal/types';
|
|
25
|
+
export type { NonVerbalEntity } from 'glossarist';
|
|
23
26
|
|
|
24
27
|
export interface NonVerbalEntityResolverOptions {
|
|
25
28
|
basePath?: string;
|
|
@@ -52,7 +55,7 @@ export class NonVerbalEntityResolver {
|
|
|
52
55
|
const resp = await this.fetcher(url);
|
|
53
56
|
if (resp.status === 404) return null;
|
|
54
57
|
if (!resp.ok) {
|
|
55
|
-
throw
|
|
58
|
+
throw NonVerbalEntityNotFoundError.make(datasetId, kind, entityId, resp.status);
|
|
56
59
|
}
|
|
57
60
|
const doc = (await resp.json()) as Record<string, unknown>;
|
|
58
61
|
const entity = KIND_TO_BRIDGE[kind](doc);
|
|
@@ -12,6 +12,10 @@ import { toSectionTree } from '../utils/section-tree';
|
|
|
12
12
|
import { formatSectionLabel, sectionName as sectionLocalized } from '../utils/section-display';
|
|
13
13
|
|
|
14
14
|
const OntologySidebarSection = defineAsyncComponent(() => import('./OntologySidebarSection.vue'));
|
|
15
|
+
import { resolveGroupKind } from '../config/group-types';
|
|
16
|
+
import type { DatasetGroupKind } from '../config/types';
|
|
17
|
+
import { useDatasetSeries } from '../composables/useDatasetSeries';
|
|
18
|
+
const useDatasetSeriesRef = () => useDatasetSeries().series;
|
|
15
19
|
|
|
16
20
|
const store = useVocabularyStore();
|
|
17
21
|
const ui = useUiStore();
|
|
@@ -28,12 +32,13 @@ const isOntologyRoute = computed(() =>
|
|
|
28
32
|
);
|
|
29
33
|
|
|
30
34
|
const datasetEntries = computed(() => {
|
|
31
|
-
const entries: { id: string; title: string; loaded: boolean; conceptCount: number }[] = [];
|
|
35
|
+
const entries: { id: string; title: string; ref?: string; loaded: boolean; conceptCount: number }[] = [];
|
|
32
36
|
for (const [id, adapter] of store.datasets) {
|
|
33
37
|
const m = store.manifests.get(id);
|
|
34
38
|
entries.push({
|
|
35
39
|
id,
|
|
36
40
|
title: m?.title ?? id.toUpperCase(),
|
|
41
|
+
ref: m?.ref,
|
|
37
42
|
loaded: !!m,
|
|
38
43
|
conceptCount: m?.conceptCount ?? 0,
|
|
39
44
|
});
|
|
@@ -49,8 +54,9 @@ interface SidebarGroup {
|
|
|
49
54
|
id: string;
|
|
50
55
|
label: string;
|
|
51
56
|
description?: string;
|
|
52
|
-
color?: string;
|
|
53
|
-
|
|
57
|
+
color?: string | { light: string; dark: string };
|
|
58
|
+
kind: DatasetGroupKind;
|
|
59
|
+
entries: { id: string; title: string; ref?: string; loaded: boolean; conceptCount: number; year?: number; status?: string; isCurrent?: boolean }[];
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
const groupedDatasetEntries = computed<SidebarGroup[]>(() => {
|
|
@@ -61,10 +67,29 @@ const groupedDatasetEntries = computed<SidebarGroup[]>(() => {
|
|
|
61
67
|
const assigned = new Set<string>();
|
|
62
68
|
const result: SidebarGroup[] = [];
|
|
63
69
|
|
|
70
|
+
/* Build a quick lookup of series metadata (year, status) from manifests */
|
|
71
|
+
const seriesMeta = new Map<string, { year?: number; status?: string; isCurrent?: boolean }>();
|
|
72
|
+
for (const s of seriesList.value) {
|
|
73
|
+
for (const m of s.members) {
|
|
74
|
+
seriesMeta.set(m.id, { year: m.year, status: m.status, isCurrent: m.isCurrent });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
for (const g of groups) {
|
|
79
|
+
const kind = resolveGroupKind(g);
|
|
65
80
|
const entries = g.datasets
|
|
66
|
-
.map(id =>
|
|
67
|
-
|
|
81
|
+
.map(id => {
|
|
82
|
+
const e = entryMap.get(id);
|
|
83
|
+
if (!e) return null;
|
|
84
|
+
const meta = seriesMeta.get(id);
|
|
85
|
+
return {
|
|
86
|
+
...e,
|
|
87
|
+
year: meta?.year,
|
|
88
|
+
status: meta?.status,
|
|
89
|
+
isCurrent: meta?.isCurrent,
|
|
90
|
+
};
|
|
91
|
+
})
|
|
92
|
+
.filter((e): e is NonNullable<typeof e> => e !== null);
|
|
68
93
|
for (const e of entries) assigned.add(e.id);
|
|
69
94
|
const trLabel = g.translations?.[locale.value]?.label;
|
|
70
95
|
result.push({
|
|
@@ -72,18 +97,22 @@ const groupedDatasetEntries = computed<SidebarGroup[]>(() => {
|
|
|
72
97
|
label: trLabel || g.label,
|
|
73
98
|
description: g.description,
|
|
74
99
|
color: g.color,
|
|
100
|
+
kind,
|
|
75
101
|
entries,
|
|
76
102
|
});
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
const ungrouped = datasetEntries.value.filter(e => !assigned.has(e.id));
|
|
80
106
|
if (ungrouped.length) {
|
|
81
|
-
result.push({ id: '__ungrouped__', label: '', entries: ungrouped });
|
|
107
|
+
result.push({ id: '__ungrouped__', label: '', kind: 'default', entries: ungrouped });
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
return result;
|
|
85
111
|
});
|
|
86
112
|
|
|
113
|
+
/* Auto-derive series list from useDatasetSeries — used to enrich entries */
|
|
114
|
+
const seriesList = useDatasetSeriesRef();
|
|
115
|
+
|
|
87
116
|
const collapsedGroups = ref<Set<string>>(new Set());
|
|
88
117
|
|
|
89
118
|
function toggleGroup(groupId: string) {
|
|
@@ -246,110 +275,140 @@ const activeSectionId = computed(() => {
|
|
|
246
275
|
<button
|
|
247
276
|
v-if="group.label"
|
|
248
277
|
@click="toggleGroup(group.id)"
|
|
249
|
-
class="sidebar-group-label w-full flex items-
|
|
250
|
-
:style="group.color ? { color: group.color } : {}"
|
|
278
|
+
class="sidebar-group-label w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold transition-colors hover:bg-ink-50 dark:hover:bg-ink-700/60"
|
|
251
279
|
>
|
|
252
|
-
<span class="w-3 text-[10px] mt-0.5 flex-shrink-0">{{ isGroupExpanded(group.id) ? '▾' : '▸' }}</span>
|
|
253
|
-
<span class="flex-1 text-left leading-snug">{{ group.label }}</span>
|
|
280
|
+
<span class="w-3 text-[10px] mt-0.5 flex-shrink-0 text-ink-300 dark:text-ink-400">{{ isGroupExpanded(group.id) ? '▾' : '▸' }}</span>
|
|
281
|
+
<span class="flex-1 text-left leading-snug text-ink-700 dark:text-ink-200 font-serif">{{ group.label }}</span>
|
|
254
282
|
</button>
|
|
255
283
|
|
|
256
284
|
<!-- Group entries -->
|
|
257
285
|
<div v-if="isGroupExpanded(group.id)" class="space-y-1" :class="group.label ? 'ml-1' : ''">
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
<!-- LINEAGE series: timeline-style entries -->
|
|
287
|
+
<template v-if="group.kind === 'lineage'">
|
|
288
|
+
<div class="series-timeline">
|
|
289
|
+
<button
|
|
290
|
+
v-for="ds in group.entries"
|
|
291
|
+
:key="ds.id"
|
|
292
|
+
@click="goToDataset(ds.id)"
|
|
293
|
+
class="series-entry w-full text-left flex items-center gap-2 pl-6 pr-3 py-1.5 rounded-md text-sm border-l-2 transition-all duration-150"
|
|
294
|
+
:class="currentDataset === ds.id
|
|
295
|
+
? 'bg-amber-50/70 dark:bg-amber-400/10 border-l-[3px] text-ink-900 dark:text-ink-50 font-semibold'
|
|
296
|
+
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700/40 hover:text-ink-900 dark:hover:text-ink-50'"
|
|
297
|
+
:style="currentDataset === ds.id ? { borderLeftColor: 'var(--gold-accent, #B8935A)' } : {}"
|
|
298
|
+
>
|
|
299
|
+
<span class="flex-1 truncate text-[13.5px] font-medium leading-snug">{{ ds.ref || ds.title || ds.id }}</span>
|
|
300
|
+
<span
|
|
301
|
+
v-if="ds.status && ds.status !== 'valid'"
|
|
302
|
+
class="text-[9px] uppercase tracking-wide italic text-ink-400 dark:text-ink-400"
|
|
303
|
+
>{{ ds.status }}</span>
|
|
304
|
+
<span
|
|
305
|
+
v-if="ds.isCurrent"
|
|
306
|
+
class="current-star flex-shrink-0"
|
|
307
|
+
title="Current edition"
|
|
308
|
+
>✦</span>
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
</template>
|
|
312
|
+
|
|
313
|
+
<!-- REGULAR group: original entry style with expansion -->
|
|
314
|
+
<template v-else>
|
|
315
|
+
<div
|
|
316
|
+
v-for="ds in group.entries"
|
|
317
|
+
:key="ds.id"
|
|
318
|
+
class="rounded-lg transition-all duration-150"
|
|
319
|
+
:class="currentDataset === ds.id ? 'bg-surface' : ''"
|
|
273
320
|
>
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
321
|
+
<button
|
|
322
|
+
@click="goToDataset(ds.id)"
|
|
323
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm border-l-2"
|
|
324
|
+
:class="[
|
|
325
|
+
currentDataset === ds.id
|
|
326
|
+
? 'text-ink-800 dark:text-ink-50'
|
|
327
|
+
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700 hover:text-ink-800 dark:hover:text-ink-50'
|
|
328
|
+
]"
|
|
329
|
+
:style="currentDataset === ds.id ? { borderLeftColor: getColor(ds.id), borderLeftWidth: '2px' } : {}"
|
|
330
|
+
>
|
|
331
|
+
<div class="font-medium truncate leading-snug">{{ localizedDatasetField(ds.id, 'title', ds.title) }}</div>
|
|
332
|
+
<div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400 dark:text-ink-300' : 'text-ink-300 dark:text-ink-400'">
|
|
333
|
+
{{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
|
|
334
|
+
</div>
|
|
335
|
+
</button>
|
|
279
336
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
>
|
|
291
|
-
<NavIcon :name="page.icon" />
|
|
292
|
-
{{ navTitle(page) }}
|
|
293
|
-
</router-link>
|
|
294
|
-
</nav>
|
|
295
|
-
|
|
296
|
-
<!-- Sections tree -->
|
|
297
|
-
<div v-if="getDatasetSections(ds.id).length" class="mt-2 pt-2 border-t border-ink-100/60">
|
|
298
|
-
<button @click="toggleSectionNode(ds.id + '-sections')"
|
|
299
|
-
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
|
|
300
|
-
>
|
|
301
|
-
<span class="w-3 text-[10px]">{{ expandedSectionNodes.has(ds.id + '-sections') ? '▾' : '▸' }}</span>
|
|
302
|
-
<span class="flex-1 text-left">{{ t('nav.sections') }}</span>
|
|
303
|
-
<span class="badge text-[9px] bg-amber-50 text-amber-600 px-1 py-0.5">{{ getDatasetSections(ds.id).length }}</span>
|
|
304
|
-
</button>
|
|
305
|
-
<div v-if="expandedSectionNodes.has(ds.id + '-sections')" class="mt-0.5 max-h-64 overflow-y-auto">
|
|
306
|
-
<button
|
|
307
|
-
@click="clearSectionFilter()"
|
|
308
|
-
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
309
|
-
:class="!activeSectionId ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
337
|
+
<!-- Expanded dataset: sub-pages + sections + provenance -->
|
|
338
|
+
<div v-if="currentDataset === ds.id && (filteredDatasetPages.length || provenance.owner)" class="px-2 pb-2">
|
|
339
|
+
<nav v-if="filteredDatasetPages.length" class="space-y-0.5 mt-1">
|
|
340
|
+
<router-link
|
|
341
|
+
v-for="page in filteredDatasetPages"
|
|
342
|
+
:key="page.route || 'concepts'"
|
|
343
|
+
:to="pageRoute(page)"
|
|
344
|
+
class="btn-ghost w-full text-left flex items-center gap-2 text-sm"
|
|
345
|
+
:class="isActive(page) ? 'active' : ''"
|
|
346
|
+
@click="closeMobile"
|
|
310
347
|
>
|
|
311
|
-
<
|
|
312
|
-
|
|
348
|
+
<NavIcon :name="page.icon" />
|
|
349
|
+
{{ navTitle(page) }}
|
|
350
|
+
</router-link>
|
|
351
|
+
</nav>
|
|
352
|
+
|
|
353
|
+
<!-- Sections tree -->
|
|
354
|
+
<div v-if="getDatasetSections(ds.id).length" class="mt-2 pt-2 border-t border-ink-100/60">
|
|
355
|
+
<button @click="toggleSectionNode(ds.id + '-sections')"
|
|
356
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors"
|
|
357
|
+
>
|
|
358
|
+
<span class="w-3 text-[10px]">{{ expandedSectionNodes.has(ds.id + '-sections') ? '▾' : '▸' }}</span>
|
|
359
|
+
<span class="flex-1 text-left">{{ t('nav.sections') }}</span>
|
|
360
|
+
<span class="badge text-[9px] bg-amber-50 text-amber-600 px-1 py-0.5">{{ getDatasetSections(ds.id).length }}</span>
|
|
313
361
|
</button>
|
|
314
|
-
<
|
|
315
|
-
<button
|
|
362
|
+
<div v-if="expandedSectionNodes.has(ds.id + '-sections')" class="mt-0.5 max-h-64 overflow-y-auto">
|
|
363
|
+
<button
|
|
364
|
+
@click="clearSectionFilter()"
|
|
316
365
|
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
317
|
-
:class="activeSectionId
|
|
366
|
+
:class="!activeSectionId ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
318
367
|
>
|
|
319
|
-
<span
|
|
320
|
-
<span
|
|
321
|
-
<span class="flex-1 text-left truncate">{{ sectionDisplay(section) }}</span>
|
|
368
|
+
<span class="w-3 text-ink-200">·</span>
|
|
369
|
+
<span class="flex-1 text-left">{{ t('dataset.all') }}</span>
|
|
322
370
|
</button>
|
|
323
|
-
<
|
|
324
|
-
<button
|
|
325
|
-
@click="goToSection(ds.id, 'section-' + child.id)"
|
|
371
|
+
<template v-for="section in getDatasetSections(ds.id)" :key="section.id">
|
|
372
|
+
<button @click="goToSection(ds.id, 'section-' + section.id)"
|
|
326
373
|
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
327
|
-
:class="activeSectionId === 'section-' +
|
|
374
|
+
:class="activeSectionId === 'section-' + section.id ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
328
375
|
>
|
|
329
|
-
<span class="
|
|
330
|
-
<span class="
|
|
376
|
+
<span v-if="section.children?.length" class="text-[10px] text-ink-300 w-3 cursor-pointer" @click.stop="toggleSectionNode(ds.id + '-s-' + section.id)">{{ expandedSectionNodes.has(ds.id + '-s-' + section.id) ? '▾' : '▸' }}</span>
|
|
377
|
+
<span v-else class="w-3 text-ink-200">·</span>
|
|
378
|
+
<span class="flex-1 text-left truncate">{{ sectionDisplay(section) }}</span>
|
|
331
379
|
</button>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
380
|
+
<div v-if="section.children?.length && expandedSectionNodes.has(ds.id + '-s-' + section.id)" class="ml-3">
|
|
381
|
+
<button v-for="child in section.children" :key="child.id"
|
|
382
|
+
@click="goToSection(ds.id, 'section-' + child.id)"
|
|
383
|
+
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[11px] transition-colors"
|
|
384
|
+
:class="activeSectionId === 'section-' + child.id ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
385
|
+
>
|
|
386
|
+
<span class="w-3 text-ink-200">·</span>
|
|
387
|
+
<span class="flex-1 text-left truncate">{{ sectionDisplay(child) }}</span>
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
</template>
|
|
340
391
|
</div>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div v-if="provenance.owner" class="mt-3 pt-3 border-t border-ink-100/60">
|
|
395
|
+
<div class="text-[11px] text-ink-300 space-y-1.5 px-1">
|
|
396
|
+
<div v-if="provenance.ref" class="text-xs font-semibold text-ink-700">
|
|
397
|
+
{{ provenance.ref }}
|
|
398
|
+
</div>
|
|
399
|
+
<div class="flex items-center gap-1">
|
|
400
|
+
<span class="text-ink-400">{{ t('sidebar.publishedBy') }}</span>
|
|
401
|
+
<a v-if="provenance.ownerUrl" :href="provenance.ownerUrl" target="_blank" rel="noopener" class="concept-link font-medium">{{ provenance.owner }}</a>
|
|
402
|
+
<span v-else class="text-ink-600 font-medium">{{ provenance.owner }}</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div v-if="provenance.sourceRepo">
|
|
405
|
+
<a :href="provenance.sourceRepo" target="_blank" rel="noopener" class="concept-link">{{ t('sidebar.viewSource') }}</a>
|
|
406
|
+
</div>
|
|
348
407
|
</div>
|
|
349
408
|
</div>
|
|
350
409
|
</div>
|
|
351
410
|
</div>
|
|
352
|
-
</
|
|
411
|
+
</template>
|
|
353
412
|
</div>
|
|
354
413
|
</div>
|
|
355
414
|
</template>
|
|
@@ -367,13 +426,13 @@ const activeSectionId = computed(() => {
|
|
|
367
426
|
class="w-full text-left px-3 py-2.5 rounded-lg text-sm border-l-2"
|
|
368
427
|
:class="[
|
|
369
428
|
currentDataset === ds.id
|
|
370
|
-
? 'text-ink-800'
|
|
371
|
-
: 'border-transparent text-ink-600 hover:bg-ink-50 hover:text-ink-800'
|
|
429
|
+
? 'text-ink-800 dark:text-ink-50'
|
|
430
|
+
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700 hover:text-ink-800 dark:hover:text-ink-50'
|
|
372
431
|
]"
|
|
373
432
|
:style="currentDataset === ds.id ? { borderLeftColor: getColor(ds.id), borderLeftWidth: '2px' } : {}"
|
|
374
433
|
>
|
|
375
434
|
<div class="font-medium truncate leading-snug">{{ localizedDatasetField(ds.id, 'title', ds.title) }}</div>
|
|
376
|
-
<div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400' : 'text-ink-300'">
|
|
435
|
+
<div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400 dark:text-ink-300' : 'text-ink-300 dark:text-ink-400'">
|
|
377
436
|
{{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
|
|
378
437
|
</div>
|
|
379
438
|
</button>
|
|
@@ -458,3 +517,40 @@ const activeSectionId = computed(() => {
|
|
|
458
517
|
</div>
|
|
459
518
|
</aside>
|
|
460
519
|
</template>
|
|
520
|
+
|
|
521
|
+
<style scoped>
|
|
522
|
+
/* Series timeline entries — used when a dataset group has `series: true`.
|
|
523
|
+
Renders editions as compact year-tagged rows instead of the full dataset
|
|
524
|
+
entry, since within a series the year IS the identity. */
|
|
525
|
+
.series-timeline {
|
|
526
|
+
position: relative;
|
|
527
|
+
padding-left: 0;
|
|
528
|
+
margin-top: 4px;
|
|
529
|
+
}
|
|
530
|
+
/* No vertical rail line — the star indicator is enough cue. */
|
|
531
|
+
|
|
532
|
+
.series-entry {
|
|
533
|
+
position: relative;
|
|
534
|
+
/* All visual states (bg, text color, border) are inline Tailwind classes
|
|
535
|
+
so dark: variants apply with reliable specificity. */
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Star (✦ U+2726, four-pointed) indicates ONE thing only: "is this the
|
|
539
|
+
current/newest valid edition?" Data property — never reflects viewing
|
|
540
|
+
state. Sits at the right edge of the entry so the ref text aligns to
|
|
541
|
+
the left consistently across current and non-current editions. */
|
|
542
|
+
.current-star {
|
|
543
|
+
display: inline-flex;
|
|
544
|
+
align-items: center;
|
|
545
|
+
justify-content: center;
|
|
546
|
+
font-size: 16px;
|
|
547
|
+
line-height: 1;
|
|
548
|
+
color: var(--gold-accent, #B8935A);
|
|
549
|
+
filter: drop-shadow(0 0 4px rgba(184, 147, 90, 0.45));
|
|
550
|
+
}
|
|
551
|
+
:global(.dark) .current-star {
|
|
552
|
+
color: var(--gold-accent, #D4AF6E);
|
|
553
|
+
filter: drop-shadow(0 0 4px rgba(212, 175, 110, 0.55));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
</style>
|
|
@@ -24,6 +24,7 @@ import { useSiteConfig } from '../config/use-site-config';
|
|
|
24
24
|
import ConceptTimeline from './ConceptTimeline.vue';
|
|
25
25
|
import ConceptRdfView from './ConceptRdfView.vue';
|
|
26
26
|
import FormatDownloads from './FormatDownloads.vue';
|
|
27
|
+
import ConceptEditionRail from './ConceptEditionRail.vue';
|
|
27
28
|
import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
|
|
28
29
|
import NonVerbalList from './non-verbal/NonVerbalList.vue';
|
|
29
30
|
import CitationDisplay from './CitationDisplay.vue';
|
|
@@ -554,6 +555,13 @@ const nonVerbalReps = computed(() => {
|
|
|
554
555
|
</Transition>
|
|
555
556
|
</div>
|
|
556
557
|
|
|
558
|
+
<!-- Edition series — supersession chain across vocabulary editions -->
|
|
559
|
+
<ConceptEditionRail
|
|
560
|
+
:concept-uri="conceptUri(props.concept, props.registerId, props.manifest.uriBase)"
|
|
561
|
+
:register-id="registerId"
|
|
562
|
+
:concept-id="conceptId"
|
|
563
|
+
/>
|
|
564
|
+
|
|
557
565
|
<!-- Domains -->
|
|
558
566
|
<div v-if="conceptDomains.length" class="card p-5">
|
|
559
567
|
<div class="section-label">{{ t('concept.domains') }}</div>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ConceptEditionRail — sidebar card showing a concept's supersession chain
|
|
4
|
+
* across vocabulary editions. Rendered INSIDE ConceptDetail's right sidebar
|
|
5
|
+
* (between Relations and other cards), styled identically to its neighbors.
|
|
6
|
+
*
|
|
7
|
+
* Reads `supersedes` edges from the graph engine and walks the full chain
|
|
8
|
+
* in both directions. Filter out malformed URIs that sometimes appear in
|
|
9
|
+
* stub-data scenarios.
|
|
10
|
+
*/
|
|
11
|
+
import { computed } from 'vue';
|
|
12
|
+
import { useRouter } from 'vue-router';
|
|
13
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
14
|
+
import { useDatasetSeries, type DatasetSeriesMember } from '../composables/useDatasetSeries';
|
|
15
|
+
import { useI18n } from '../i18n';
|
|
16
|
+
import { UriRouter } from '../adapters/UriRouter';
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
conceptUri: string;
|
|
22
|
+
registerId: string;
|
|
23
|
+
conceptId: string;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const store = useVocabularyStore();
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
|
|
29
|
+
const { seriesForActive } = useDatasetSeries(() => props.registerId);
|
|
30
|
+
const series = computed(() => seriesForActive.value);
|
|
31
|
+
|
|
32
|
+
interface EditionEntry {
|
|
33
|
+
member: DatasetSeriesMember;
|
|
34
|
+
conceptUri: string;
|
|
35
|
+
conceptId: string;
|
|
36
|
+
edgeType: 'self' | 'supersedes' | 'superseded_by';
|
|
37
|
+
hops: number;
|
|
38
|
+
isCurrentEdition: boolean; // newest valid edition in the series
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resolve a concept URI via the project's UriRouter SSOT. Returns null for
|
|
42
|
+
* malformed or non-concept URIs (URN-form refs, external IRIs, stub data). */
|
|
43
|
+
function parseStrict(uri: string): { registerId: string; conceptId: string } | null {
|
|
44
|
+
return UriRouter.parseUri(uri);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Bidirectional BFS through supersedes edges from the start URI. */
|
|
48
|
+
function walkChain(startUri: string): Map<string, { type: 'supersedes' | 'superseded_by'; hops: number }> {
|
|
49
|
+
const out = new Map<string, { type: 'supersedes' | 'superseded_by'; hops: number }>();
|
|
50
|
+
|
|
51
|
+
/* Forward (concepts THIS supersedes — older editions it replaced) */
|
|
52
|
+
const forwardQueue: Array<{ uri: string; hops: number }> = [{ uri: startUri, hops: 0 }];
|
|
53
|
+
const forwardSeen = new Set<string>([startUri]);
|
|
54
|
+
while (forwardQueue.length > 0) {
|
|
55
|
+
const { uri, hops } = forwardQueue.shift()!;
|
|
56
|
+
for (const e of store.graph.getEdges(uri)) {
|
|
57
|
+
if (e.type === 'supersedes' && !forwardSeen.has(e.target)) {
|
|
58
|
+
forwardSeen.add(e.target);
|
|
59
|
+
out.set(e.target, { type: 'supersedes', hops: hops + 1 });
|
|
60
|
+
forwardQueue.push({ uri: e.target, hops: hops + 1 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Backward (concepts that SUPERSEDED this — newer editions) */
|
|
66
|
+
const backwardQueue: Array<{ uri: string; hops: number }> = [{ uri: startUri, hops: 0 }];
|
|
67
|
+
const backwardSeen = new Set<string>([startUri]);
|
|
68
|
+
while (backwardQueue.length > 0) {
|
|
69
|
+
const { uri, hops } = backwardQueue.shift()!;
|
|
70
|
+
for (const e of store.graph.getIncomingEdges(uri)) {
|
|
71
|
+
if (e.type === 'supersedes' && !backwardSeen.has(e.source)) {
|
|
72
|
+
backwardSeen.add(e.source);
|
|
73
|
+
out.set(e.source, { type: 'superseded_by', hops: hops + 1 });
|
|
74
|
+
backwardQueue.push({ uri: e.source, hops: hops + 1 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const editionChain = computed<EditionEntry[]>(() => {
|
|
83
|
+
const s = series.value;
|
|
84
|
+
const entries: EditionEntry[] = [];
|
|
85
|
+
|
|
86
|
+
/* Always include self — use props directly so we don't depend on URI parsing. */
|
|
87
|
+
const selfMember: DatasetSeriesMember = s?.members.find(m => m.id === props.registerId) ?? {
|
|
88
|
+
id: props.registerId,
|
|
89
|
+
ref: props.registerId,
|
|
90
|
+
year: undefined,
|
|
91
|
+
status: 'valid',
|
|
92
|
+
isCurrent: false,
|
|
93
|
+
isActive: true,
|
|
94
|
+
conceptCount: undefined,
|
|
95
|
+
registerId: props.registerId,
|
|
96
|
+
};
|
|
97
|
+
entries.push({
|
|
98
|
+
member: selfMember,
|
|
99
|
+
conceptUri: props.conceptUri,
|
|
100
|
+
conceptId: props.conceptId,
|
|
101
|
+
edgeType: 'self',
|
|
102
|
+
hops: 0,
|
|
103
|
+
isCurrentEdition: !!selfMember.isCurrent,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/* Walk the chain. Drop malformed URIs (no real concept id). */
|
|
107
|
+
const chain = walkChain(props.conceptUri);
|
|
108
|
+
for (const [uri, info] of chain) {
|
|
109
|
+
if (uri === props.conceptUri) continue;
|
|
110
|
+
const parsed = parseStrict(uri);
|
|
111
|
+
if (!parsed) continue;
|
|
112
|
+
if (entries.some(e => e.conceptUri === uri)) continue;
|
|
113
|
+
|
|
114
|
+
const member = s?.members.find(m => m.id === parsed.registerId) ?? {
|
|
115
|
+
id: parsed.registerId,
|
|
116
|
+
ref: parsed.registerId,
|
|
117
|
+
year: undefined,
|
|
118
|
+
status: 'unknown',
|
|
119
|
+
isCurrent: false,
|
|
120
|
+
isActive: false,
|
|
121
|
+
conceptCount: undefined,
|
|
122
|
+
registerId: parsed.registerId,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
entries.push({
|
|
126
|
+
member,
|
|
127
|
+
conceptUri: uri,
|
|
128
|
+
conceptId: parsed.conceptId,
|
|
129
|
+
edgeType: info.type,
|
|
130
|
+
hops: info.hops,
|
|
131
|
+
isCurrentEdition: !!member.isCurrent,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Sort by year ascending (oldest first). */
|
|
136
|
+
entries.sort((a, b) => (a.member.year ?? 9999) - (b.member.year ?? 9999));
|
|
137
|
+
|
|
138
|
+
return entries;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const hasChain = computed(() => editionChain.value.length > 1);
|
|
142
|
+
|
|
143
|
+
function navigate(entry: EditionEntry) {
|
|
144
|
+
if (entry.edgeType === 'self') return;
|
|
145
|
+
router.push({
|
|
146
|
+
name: 'concept',
|
|
147
|
+
params: { registerId: entry.member.id, conceptId: entry.conceptId },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function edgeLabel(entry: EditionEntry): string {
|
|
152
|
+
switch (entry.edgeType) {
|
|
153
|
+
case 'supersedes': return t('edge.supersedes');
|
|
154
|
+
case 'superseded_by': return t('edge.superseded_by');
|
|
155
|
+
default: return '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<div v-if="series" class="card p-5">
|
|
162
|
+
<div class="section-label">{{ t('concept.editionSeries') }}</div>
|
|
163
|
+
<div class="mt-1 text-xs text-ink-400 italic">{{ series.title }}</div>
|
|
164
|
+
|
|
165
|
+
<div class="mt-3 space-y-1">
|
|
166
|
+
<button
|
|
167
|
+
v-for="entry in editionChain"
|
|
168
|
+
:key="entry.conceptUri"
|
|
169
|
+
type="button"
|
|
170
|
+
class="concept-link block w-full text-left rounded-md px-1.5 py-1.5 transition-colors"
|
|
171
|
+
:class="entry.edgeType === 'self'
|
|
172
|
+
? 'bg-blue-50 dark:bg-blue-900/20'
|
|
173
|
+
: 'hover:bg-ink-50 dark:hover:bg-ink-700/40'"
|
|
174
|
+
:disabled="entry.edgeType === 'self'"
|
|
175
|
+
@click="navigate(entry)"
|
|
176
|
+
>
|
|
177
|
+
<div class="flex items-center gap-1 mb-0.5">
|
|
178
|
+
<span
|
|
179
|
+
v-if="entry.isCurrentEdition"
|
|
180
|
+
class="badge text-[9px] flex-shrink-0"
|
|
181
|
+
:class="entry.edgeType === 'self' ? 'badge-blue' : 'badge-gray'"
|
|
182
|
+
style="background: rgba(184, 147, 90, 0.18); color: #8C6A3A; border: 1px solid rgba(184, 147, 90, 0.35);"
|
|
183
|
+
:title="t('concept.currentEdition')"
|
|
184
|
+
>✦ {{ t('concept.currentEdition') }}</span>
|
|
185
|
+
<span
|
|
186
|
+
v-if="entry.edgeType !== 'self'"
|
|
187
|
+
class="badge text-[9px] flex-shrink-0 badge-gray"
|
|
188
|
+
>{{ edgeLabel(entry) }}</span>
|
|
189
|
+
<span
|
|
190
|
+
v-if="entry.edgeType === 'self'"
|
|
191
|
+
class="badge text-[9px] flex-shrink-0 badge-blue"
|
|
192
|
+
>{{ t('concept.viewing') }}</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="flex items-baseline gap-2">
|
|
195
|
+
<span class="font-mono text-xs text-ink-500 dark:text-ink-400 flex-shrink-0">
|
|
196
|
+
{{ entry.member.year ?? '—' }}
|
|
197
|
+
</span>
|
|
198
|
+
<span class="text-sm text-ink-700 dark:text-ink-200 leading-snug truncate">
|
|
199
|
+
{{ entry.member.ref }}
|
|
200
|
+
</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div
|
|
203
|
+
v-if="entry.edgeType !== 'self' || entry.conceptId !== props.conceptId"
|
|
204
|
+
class="font-mono text-[10px] text-ink-300 dark:text-ink-500 mt-0.5 leading-tight"
|
|
205
|
+
>
|
|
206
|
+
{{ entry.member.id }} · {{ entry.conceptId }}
|
|
207
|
+
</div>
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div v-if="!hasChain" class="mt-3 pt-3 border-t border-ink-100/60 dark:border-ink-700/40">
|
|
212
|
+
<div class="text-xs text-ink-400 italic">
|
|
213
|
+
{{ t('concept.noChain') }}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</template>
|
|
218
|
+
|
|
219
|
+
<style scoped>
|
|
220
|
+
/* No scoped styles — uses global `card`, `section-label`, `badge`, `concept-link` classes
|
|
221
|
+
to match the rest of ConceptDetail's sidebar. */
|
|
222
|
+
</style>
|