@glossarist/concept-browser 0.7.51 → 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/glossarist-augment.d.ts +7 -0
- package/src/adapters/non-verbal-resolver.ts +2 -1
- 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/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/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- 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
|
@@ -12,6 +12,12 @@ import type {
|
|
|
12
12
|
import type { Concept } from 'glossarist';
|
|
13
13
|
import { conceptFromJson } from './model-bridge';
|
|
14
14
|
import { GraphDataSource } from './GraphDataSource';
|
|
15
|
+
import {
|
|
16
|
+
ChunkLoadError,
|
|
17
|
+
ConceptNotFoundError,
|
|
18
|
+
IndexLoadError,
|
|
19
|
+
ManifestLoadError,
|
|
20
|
+
} from '../errors';
|
|
15
21
|
|
|
16
22
|
// ── Wire-format types for JSON responses ────────────────────────────────────
|
|
17
23
|
|
|
@@ -82,7 +88,7 @@ export class DatasetAdapter {
|
|
|
82
88
|
async loadManifest(): Promise<Manifest> {
|
|
83
89
|
if (this.manifestComplete && this.manifest) return this.manifest;
|
|
84
90
|
const resp = await fetch(`${this.baseUrl}/manifest.json`);
|
|
85
|
-
if (!resp.ok) throw
|
|
91
|
+
if (!resp.ok) throw ManifestLoadError.make(this.registerId, resp.status);
|
|
86
92
|
this.manifest = (await resp.json()) as Manifest;
|
|
87
93
|
this.manifestComplete = true;
|
|
88
94
|
return this.manifest;
|
|
@@ -97,7 +103,7 @@ export class DatasetAdapter {
|
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
100
|
-
if (!resp.ok) throw
|
|
106
|
+
if (!resp.ok) throw IndexLoadError.make(this.registerId, resp.status);
|
|
101
107
|
const data = await resp.json();
|
|
102
108
|
|
|
103
109
|
// Handle both old format (with eng/status fields) and new format (with designations map)
|
|
@@ -150,7 +156,7 @@ export class DatasetAdapter {
|
|
|
150
156
|
meta = await metaResp.json();
|
|
151
157
|
} else {
|
|
152
158
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
153
|
-
if (!resp.ok) throw
|
|
159
|
+
if (!resp.ok) throw IndexLoadError.make(this.registerId, resp.status);
|
|
154
160
|
const data = await resp.json();
|
|
155
161
|
this.index = this.normalizeIndex(data as IndexJson);
|
|
156
162
|
this.buildSummaryIndex();
|
|
@@ -180,7 +186,7 @@ export class DatasetAdapter {
|
|
|
180
186
|
if (this.loadedChunks.has(chunkIndex)) return [];
|
|
181
187
|
const chunkFile = `index-${String(chunkIndex).padStart(4, '0')}.json`;
|
|
182
188
|
const resp = await fetch(`${this.baseUrl}/chunks/${chunkFile}`);
|
|
183
|
-
if (!resp.ok) throw
|
|
189
|
+
if (!resp.ok) throw ChunkLoadError.make(this.registerId, chunkIndex, resp.status);
|
|
184
190
|
const data = await resp.json();
|
|
185
191
|
this.loadedChunks.add(chunkIndex);
|
|
186
192
|
return data.concepts as ConceptEntry[];
|
|
@@ -255,7 +261,7 @@ export class DatasetAdapter {
|
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
const resp = await fetch(`${this.baseUrl}/concepts/${conceptId}.json`);
|
|
258
|
-
if (!resp.ok) throw
|
|
264
|
+
if (!resp.ok) throw ConceptNotFoundError.make(this.registerId, conceptId);
|
|
259
265
|
const json = await resp.json();
|
|
260
266
|
const concept = conceptFromJson(json);
|
|
261
267
|
this.conceptCache.set(conceptId, concept);
|
|
@@ -4,6 +4,7 @@ import type { DatasetAdapter } from './DatasetAdapter';
|
|
|
4
4
|
import { UriRouter } from './UriRouter';
|
|
5
5
|
import { slugify } from '../utils/slugify';
|
|
6
6
|
import { toSectionNode, toSectionTree } from '../utils/section-tree';
|
|
7
|
+
import { ConceptIdentity } from './concept-identity';
|
|
7
8
|
|
|
8
9
|
interface DomainNodeJson {
|
|
9
10
|
uri?: string;
|
|
@@ -23,7 +24,7 @@ function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: strin
|
|
|
23
24
|
if (ref.source && !ref.source.startsWith('http')) {
|
|
24
25
|
reg = urnMap?.get(ref.source) ?? ref.source;
|
|
25
26
|
}
|
|
26
|
-
return
|
|
27
|
+
return new ConceptIdentity(ref.id, reg, uriBase).uri;
|
|
27
28
|
}
|
|
28
29
|
if (ref.source && ref.source.startsWith('http')) return ref.source;
|
|
29
30
|
return ref.source || '';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Manifest } from './types';
|
|
2
|
+
import { ConceptIdentity } from './concept-identity';
|
|
2
3
|
|
|
3
4
|
// ── URI pattern matching ────────────────────────────────────────────────────
|
|
4
5
|
|
|
@@ -104,7 +105,7 @@ export class UriRouter {
|
|
|
104
105
|
buildUri(registerId: string, conceptId: string): string {
|
|
105
106
|
const info = this.registerMap.get(registerId);
|
|
106
107
|
const uriBase = info?.uriBase ?? '';
|
|
107
|
-
return
|
|
108
|
+
return new ConceptIdentity(conceptId, registerId, uriBase).uri;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
getRegisteredIds(): string[] {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { InvalidConceptIdentityError, InvalidConceptUriError } from '../errors';
|
|
2
|
+
|
|
3
|
+
export interface ConceptIdentityParts {
|
|
4
|
+
readonly localId: string;
|
|
5
|
+
readonly registerId: string;
|
|
6
|
+
readonly uriBase: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CONCEPT_URI_RE = /^(.+)\/([^/]+)\/concept\/(.+)$/;
|
|
10
|
+
|
|
11
|
+
export class ConceptIdentity implements ConceptIdentityParts {
|
|
12
|
+
private readonly _uri: string;
|
|
13
|
+
private readonly _slug: string;
|
|
14
|
+
private readonly _path: string;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
public readonly localId: string,
|
|
18
|
+
public readonly registerId: string,
|
|
19
|
+
public readonly uriBase: string,
|
|
20
|
+
) {
|
|
21
|
+
if (!localId || !registerId || !uriBase) {
|
|
22
|
+
throw new InvalidConceptIdentityError(
|
|
23
|
+
'ConceptIdentity requires non-empty localId, registerId, and uriBase',
|
|
24
|
+
{ localId, registerId, uriBase },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this._uri = `${uriBase}/${registerId}/concept/${localId}`;
|
|
28
|
+
this._slug = localId;
|
|
29
|
+
this._path = `${registerId}/concepts/${localId}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get uri(): string { return this._uri; }
|
|
33
|
+
get slug(): string { return this._slug; }
|
|
34
|
+
get path(): string { return this._path; }
|
|
35
|
+
|
|
36
|
+
equals(other: ConceptIdentity): boolean {
|
|
37
|
+
return this._uri === other._uri;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toString(): string { return this._uri; }
|
|
41
|
+
|
|
42
|
+
toJSON(): ConceptIdentityParts {
|
|
43
|
+
return { localId: this.localId, registerId: this.registerId, uriBase: this.uriBase };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
localizationUri(lang: string): string {
|
|
47
|
+
return `${this._uri}/${lang}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
designationUri(lang: string, slug: string): string {
|
|
51
|
+
return `${this._uri}/${lang}/desig/${slug}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
domainUri(domainSlug: string): string {
|
|
55
|
+
return `${this.uriBase}/${this.registerId}/domain/${domainSlug}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static fromUri(uri: string): ConceptIdentity {
|
|
59
|
+
const m = uri.match(CONCEPT_URI_RE);
|
|
60
|
+
if (!m) {
|
|
61
|
+
throw InvalidConceptUriError.make(uri);
|
|
62
|
+
}
|
|
63
|
+
return new ConceptIdentity(m[3], m[2], m[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static isConceptUri(uri: string): boolean {
|
|
67
|
+
return CONCEPT_URI_RE.test(uri);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/adapters/factory.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ReferenceResolver } from './ReferenceResolver';
|
|
|
5
5
|
import { UriRouter } from './UriRouter';
|
|
6
6
|
import { NonVerbalEntityResolver } from './non-verbal-resolver';
|
|
7
7
|
import { BibliographyAdapter } from './bibliography-adapter';
|
|
8
|
+
import { DatasetRegistryLoadError, UnknownDatasetError } from '../errors';
|
|
8
9
|
|
|
9
10
|
export class AdapterFactory {
|
|
10
11
|
private adapters = new Map<string, DatasetAdapter>();
|
|
@@ -36,7 +37,7 @@ export class AdapterFactory {
|
|
|
36
37
|
registry = JSON.parse(inline.textContent) as DatasetRegistry[];
|
|
37
38
|
} else {
|
|
38
39
|
const resp = await fetch(datasetsUrl);
|
|
39
|
-
if (!resp.ok) throw
|
|
40
|
+
if (!resp.ok) throw DatasetRegistryLoadError.make(resp.status, datasetsUrl);
|
|
40
41
|
registry = await resp.json() as DatasetRegistry[];
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -132,7 +133,7 @@ export class AdapterFactory {
|
|
|
132
133
|
|
|
133
134
|
async loadDataset(registerId: string): Promise<DatasetAdapter> {
|
|
134
135
|
const adapter = this.adapters.get(registerId);
|
|
135
|
-
if (!adapter) throw
|
|
136
|
+
if (!adapter) throw UnknownDatasetError.make(registerId);
|
|
136
137
|
|
|
137
138
|
const manifest = await adapter.loadManifest();
|
|
138
139
|
await adapter.loadIndex();
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
GRAMMAR_PARTS_OF_SPEECH,
|
|
37
37
|
} from 'glossarist/models';
|
|
38
38
|
import type { ConceptSummary } from './types';
|
|
39
|
+
import { ConceptIdentity } from './concept-identity';
|
|
39
40
|
|
|
40
41
|
// ── JSON-LD wire-format types ─────────────────────────────────────────────
|
|
41
42
|
|
|
@@ -634,5 +635,5 @@ export function conceptToSummary(concept: Concept): ConceptSummary {
|
|
|
634
635
|
|
|
635
636
|
export function conceptUri(concept: Concept, registerId: string, uriBase: string): string {
|
|
636
637
|
if (concept.uri) return concept.uri;
|
|
637
|
-
return
|
|
638
|
+
return new ConceptIdentity(concept.id, registerId, uriBase).uri;
|
|
638
639
|
}
|
|
@@ -91,6 +91,13 @@ declare module 'glossarist' {
|
|
|
91
91
|
static register(type: string, cls: typeof NonVerbalReference): void;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
interface NonVerbRep {
|
|
95
|
+
readonly caption: string | null;
|
|
96
|
+
readonly description: string | null;
|
|
97
|
+
readonly alt: string | null;
|
|
98
|
+
readonly images: FigureImage[];
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
class FigureReference extends NonVerbalReference {
|
|
95
102
|
static fromJSON(data: Record<string, unknown> | string): FigureReference;
|
|
96
103
|
}
|
|
@@ -19,6 +19,7 @@ import type { NonVerbalKind } from './non-verbal/types';
|
|
|
19
19
|
import type { NonVerbalEntity } from 'glossarist';
|
|
20
20
|
import { KIND_TO_DIR, KIND_TO_BRIDGE } from './non-verbal/kind';
|
|
21
21
|
import { anchorId } from '../utils/non-verbal-anchor';
|
|
22
|
+
import { NonVerbalEntityNotFoundError } from '../errors';
|
|
22
23
|
|
|
23
24
|
export type { NonVerbalKind } from './non-verbal/types';
|
|
24
25
|
export type { NonVerbalEntity } from 'glossarist';
|
|
@@ -54,7 +55,7 @@ export class NonVerbalEntityResolver {
|
|
|
54
55
|
const resp = await this.fetcher(url);
|
|
55
56
|
if (resp.status === 404) return null;
|
|
56
57
|
if (!resp.ok) {
|
|
57
|
-
throw
|
|
58
|
+
throw NonVerbalEntityNotFoundError.make(datasetId, kind, entityId, resp.status);
|
|
58
59
|
}
|
|
59
60
|
const doc = (await resp.json()) as Record<string, unknown>;
|
|
60
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>
|