@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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color theme SSOT. Loads `data/colors.json` defaults and merges per-
|
|
3
|
+
* deployment overrides from `site-config.json` `colors` block.
|
|
4
|
+
*
|
|
5
|
+
* Pure data + pure accessors — no Vue reactivity. Reactive consumption
|
|
6
|
+
* is via the `useColorTheme()` composable.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import type { DatasetColorSpec, SiteColors } from '../config/types';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const COLORS_PATH = join(__dirname, '..', '..', 'data', 'colors.json');
|
|
15
|
+
|
|
16
|
+
export interface ColorPair {
|
|
17
|
+
readonly light: string;
|
|
18
|
+
readonly dark: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ColorDefaults {
|
|
22
|
+
readonly relationshipCategory: Record<string, ColorPair>;
|
|
23
|
+
readonly relationshipType: Record<string, ColorPair>;
|
|
24
|
+
readonly conceptStatus: Record<string, ColorPair>;
|
|
25
|
+
readonly groupKind: Record<string, ColorPair>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cachedDefaults: ColorDefaults | undefined;
|
|
29
|
+
|
|
30
|
+
function loadDefaults(): ColorDefaults {
|
|
31
|
+
if (cachedDefaults) return cachedDefaults;
|
|
32
|
+
const raw = JSON.parse(readFileSync(COLORS_PATH, 'utf8'));
|
|
33
|
+
cachedDefaults = {
|
|
34
|
+
relationshipCategory: raw.relationshipCategory,
|
|
35
|
+
relationshipType: raw.relationshipType,
|
|
36
|
+
conceptStatus: raw.conceptStatus,
|
|
37
|
+
groupKind: raw.groupKind,
|
|
38
|
+
};
|
|
39
|
+
return cachedDefaults!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalize(spec: DatasetColorSpec | undefined): ColorPair | undefined {
|
|
43
|
+
if (spec == null) return undefined;
|
|
44
|
+
if (typeof spec === 'string') return { light: spec, dark: spec };
|
|
45
|
+
return { light: spec.light, dark: spec.dark };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolvePair(
|
|
49
|
+
key: string | undefined,
|
|
50
|
+
overrides: Record<string, DatasetColorSpec> | undefined,
|
|
51
|
+
defaults: Record<string, ColorPair>,
|
|
52
|
+
fallback: ColorPair,
|
|
53
|
+
): ColorPair {
|
|
54
|
+
if (key && overrides) {
|
|
55
|
+
const ov = normalize(overrides[key]);
|
|
56
|
+
if (ov) return ov;
|
|
57
|
+
}
|
|
58
|
+
if (key && defaults[key]) return defaults[key];
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createColorTheme(siteColors?: SiteColors) {
|
|
63
|
+
const defaults = loadDefaults();
|
|
64
|
+
|
|
65
|
+
function relationshipCategoryColor(categoryId: string): ColorPair {
|
|
66
|
+
return resolvePair(
|
|
67
|
+
categoryId,
|
|
68
|
+
siteColors?.relationshipCategory,
|
|
69
|
+
defaults.relationshipCategory,
|
|
70
|
+
defaults.relationshipCategory.associative,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function relationshipTypeColor(typeId: string, categoryId?: string): ColorPair {
|
|
75
|
+
const ov = normalize(siteColors?.relationshipType?.[typeId]);
|
|
76
|
+
if (ov) return ov;
|
|
77
|
+
if (defaults.relationshipType[typeId]) return defaults.relationshipType[typeId];
|
|
78
|
+
if (categoryId) return relationshipCategoryColor(categoryId);
|
|
79
|
+
return defaults.relationshipCategory.associative;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function conceptStatusColor(statusId: string): ColorPair {
|
|
83
|
+
return resolvePair(
|
|
84
|
+
statusId,
|
|
85
|
+
siteColors?.conceptStatus,
|
|
86
|
+
defaults.conceptStatus,
|
|
87
|
+
{ light: '#6B6E7D', dark: '#9CA3AF' },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function groupKindColor(kind: string): ColorPair {
|
|
92
|
+
return resolvePair(
|
|
93
|
+
kind,
|
|
94
|
+
siteColors?.groupKind,
|
|
95
|
+
defaults.groupKind,
|
|
96
|
+
defaults.groupKind.default,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function datasetColor(datasetId: string, declared?: DatasetColorSpec): ColorPair {
|
|
101
|
+
const ov = normalize(siteColors?.dataset?.[datasetId]);
|
|
102
|
+
if (ov) return ov;
|
|
103
|
+
const declaredPair = normalize(declared);
|
|
104
|
+
if (declaredPair) return declaredPair;
|
|
105
|
+
return { light: '#3366ff', dark: '#60A5FA' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function groupColor(groupId: string, declared?: DatasetColorSpec): ColorPair {
|
|
109
|
+
const ov = normalize(siteColors?.group?.[groupId]);
|
|
110
|
+
if (ov) return ov;
|
|
111
|
+
const declaredPair = normalize(declared);
|
|
112
|
+
if (declaredPair) return declaredPair;
|
|
113
|
+
return groupKindColor('default');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
relationshipCategoryColor,
|
|
118
|
+
relationshipTypeColor,
|
|
119
|
+
conceptStatusColor,
|
|
120
|
+
groupKindColor,
|
|
121
|
+
datasetColor,
|
|
122
|
+
groupColor,
|
|
123
|
+
defaults,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type ColorTheme = ReturnType<typeof createColorTheme>;
|
|
128
|
+
|
|
129
|
+
export const FALLBACK_COLOR_PAIR: ColorPair = { light: '#6B6E7D', dark: '#9CA3AF' };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
2
|
+
import type { DatasetColorSpec } from '../config/types';
|
|
3
|
+
import { createColorTheme } from './color-theme';
|
|
2
4
|
|
|
3
5
|
const PALETTE = [
|
|
4
6
|
'#3366ff', '#0d9488', '#d97706', '#8b5cf6',
|
|
@@ -7,9 +9,16 @@ const PALETTE = [
|
|
|
7
9
|
];
|
|
8
10
|
|
|
9
11
|
export interface DsStyle {
|
|
12
|
+
/** Single-hex backward-compat color (light mode). */
|
|
10
13
|
color: string;
|
|
14
|
+
/** Explicit light-mode color. */
|
|
11
15
|
light: string;
|
|
16
|
+
/** Explicit dark-mode color. */
|
|
12
17
|
dark: string;
|
|
18
|
+
/** Light-mode rgba with custom alpha. */
|
|
19
|
+
lightAlpha: (a: number) => string;
|
|
20
|
+
/** Dark-mode rgba with custom alpha. */
|
|
21
|
+
darkAlpha: (a: number) => string;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
function hexToRgba(hex: string, alpha: number): string {
|
|
@@ -19,14 +28,29 @@ function hexToRgba(hex: string, alpha: number): string {
|
|
|
19
28
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
|
-
export function makeDsStyle(
|
|
31
|
+
export function makeDsStyle(spec?: DatasetColorSpec, fallbackLight = '#3366ff'): DsStyle {
|
|
32
|
+
const pair = normalizeSpec(spec, fallbackLight);
|
|
23
33
|
return {
|
|
24
|
-
color,
|
|
25
|
-
light:
|
|
26
|
-
dark:
|
|
34
|
+
color: pair.light,
|
|
35
|
+
light: pair.light,
|
|
36
|
+
dark: pair.dark,
|
|
37
|
+
lightAlpha: (a: number) => hexToRgba(pair.light, a),
|
|
38
|
+
darkAlpha: (a: number) => hexToRgba(pair.dark, a),
|
|
27
39
|
};
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
function normalizeSpec(spec: DatasetColorSpec | undefined, fallback: string): { light: string; dark: string } {
|
|
43
|
+
if (spec == null) {
|
|
44
|
+
const dark = hexToRgba(fallback, 0.85);
|
|
45
|
+
return { light: fallback, dark };
|
|
46
|
+
}
|
|
47
|
+
if (typeof spec === 'string') {
|
|
48
|
+
const dark = hexToRgba(spec, 0.85);
|
|
49
|
+
return { light: spec, dark };
|
|
50
|
+
}
|
|
51
|
+
return { light: spec.light, dark: spec.dark };
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
export function paletteColor(index: number): string {
|
|
31
55
|
return PALETTE[index % PALETTE.length];
|
|
32
56
|
}
|
|
@@ -40,8 +64,9 @@ export function useDsStyle() {
|
|
|
40
64
|
|
|
41
65
|
const store = useVocabularyStore();
|
|
42
66
|
const ds = store.datasetList.find(d => d.id === registerId);
|
|
43
|
-
const
|
|
44
|
-
const
|
|
67
|
+
const declared = ds?.manifest?.color as DatasetColorSpec | undefined;
|
|
68
|
+
const fallback = paletteColor(store.datasetList.findIndex(d => d.id === registerId));
|
|
69
|
+
const style = makeDsStyle(declared, fallback);
|
|
45
70
|
cache.set(registerId, style);
|
|
46
71
|
return style;
|
|
47
72
|
}
|
package/src/utils/locale.ts
CHANGED
|
@@ -5,27 +5,19 @@
|
|
|
5
5
|
* Both the non-verbal entity resolver and any other localized content
|
|
6
6
|
* resolution should call `pickLocaleText` / `pickLocaleMap` rather than
|
|
7
7
|
* implement their own fallback chain.
|
|
8
|
+
*
|
|
9
|
+
* Language-code mapping (ISO 639-3 ↔ ISO 639-1) and BCP-47 parsing live in
|
|
10
|
+
* `./bcp47`; this module re-exports the mapping for backwards
|
|
11
|
+
* compatibility.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import { fetchLocalizedString } from 'glossarist';
|
|
15
|
+
import { mapIso6393To6391 } from './bcp47';
|
|
11
16
|
|
|
12
17
|
const DEFAULT_FALLBACK_CHAIN: readonly string[] = ['eng'] as const;
|
|
13
18
|
|
|
14
19
|
const RTL_LOCALES: ReadonlySet<string> = new Set(['ara', 'heb', 'fas', 'urd', 'arb']);
|
|
15
20
|
|
|
16
|
-
const ISO_639_2_TO_BCP47: Record<string, string> = {
|
|
17
|
-
eng: 'en', fra: 'fr', deu: 'de', zho: 'zh', ara: 'ar', jpn: 'ja', rus: 'ru',
|
|
18
|
-
kor: 'ko', spa: 'es', ita: 'it', por: 'pt', nld: 'nl', swe: 'sv', fin: 'fi',
|
|
19
|
-
dan: 'da', nob: 'nb', nno: 'nn', nor: 'no', pol: 'pl', tur: 'tr', ces: 'cs', ell: 'el',
|
|
20
|
-
heb: 'he', hin: 'hi', ind: 'id', fas: 'fa', ukr: 'uk', hun: 'hu', ron: 'ro',
|
|
21
|
-
slk: 'sk', slv: 'sl', hrv: 'hr', srp: 'sr', bul: 'bg', msa: 'ms', tha: 'th',
|
|
22
|
-
vie: 'vi', urd: 'ur', ben: 'bn', tam: 'ta', tel: 'te', mar: 'mr', guj: 'gu',
|
|
23
|
-
pan: 'pa', mal: 'ml', kan: 'kn', ori: 'or', asm: 'as', sin: 'si', nep: 'ne',
|
|
24
|
-
lit: 'lt', lav: 'lv', est: 'et', gle: 'ga', cym: 'cy', eus: 'eu', cat: 'ca',
|
|
25
|
-
glg: 'gl', afr: 'af', sqi: 'sq', mkd: 'mk', bel: 'be', kaz: 'kk', uzb: 'uz',
|
|
26
|
-
aze: 'az', hye: 'hy', kat: 'ka', mon: 'mn', tuk: 'tk', uig: 'ug', tgl: 'tl',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
21
|
export type LocalizedText = Record<string, string>;
|
|
30
22
|
|
|
31
23
|
export interface ResolvedLocaleText {
|
|
@@ -77,7 +69,7 @@ export function isRtl(locale: string): boolean {
|
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
export function localeToBcp47(locale: string): string {
|
|
80
|
-
return
|
|
72
|
+
return mapIso6393To6391(locale) ?? locale;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
export function resolveFallbackChain(datasetLocales?: readonly string[]): readonly string[] {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { escapeHtml } from './escape';
|
|
2
|
+
import { sanitizeUrl } from './url-safety';
|
|
2
3
|
|
|
3
4
|
const INLINE_PATTERNS: [RegExp, (m: RegExpMatchArray) => string][] = [
|
|
4
5
|
[/\*\*(.+?)\*\*/g, m => `<strong>${m[1]}</strong>`],
|
|
5
6
|
[/(?<!\*)\*([^*]+?)\*(?!\*)/g, m => `<em>${m[1]}</em>`],
|
|
6
7
|
[/`([^`]+?)`/g, m => `<code>${m[1]}</code>`],
|
|
7
|
-
[/\[([^\]]+)\]\(([^)]+)\)/g, m =>
|
|
8
|
+
[/\[([^\]]+)\]\(([^)]+)\)/g, m => {
|
|
9
|
+
const href = sanitizeUrl(m[2]);
|
|
10
|
+
if (!href) return escapeHtml(m[1]);
|
|
11
|
+
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${m[1]}</a>`;
|
|
12
|
+
}],
|
|
8
13
|
];
|
|
9
14
|
|
|
10
15
|
function renderInline(text: string): string {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation sphere styling — bridge between the project's semantic SSOTs
|
|
3
|
+
* (taxonomy + color-theme) and the visual encoding the sphere needs.
|
|
4
|
+
*
|
|
5
|
+
* Single source: taxonomy drives category/type identity.
|
|
6
|
+
* Color-theme drives light/dark color pairs.
|
|
7
|
+
* This module adds only the sphere-specific concern: dasharray patterns.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
RELATIONSHIP_CATEGORIES,
|
|
11
|
+
categorizeRelationship,
|
|
12
|
+
relationshipLabel,
|
|
13
|
+
} from './relationship-categories';
|
|
14
|
+
import { colorPairForType, colorPairForCategory, type ColorPair } from './color-theme-integration';
|
|
15
|
+
|
|
16
|
+
export interface SphereRelationCategory {
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly color: string;
|
|
20
|
+
readonly dasharray: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Dasharray per category — sphere-specific visual encoding. Sourced
|
|
24
|
+
* from colors.json `relationshipCategoryDash` block (added in #14). */
|
|
25
|
+
const DASHARRAY: Record<string, string> = {
|
|
26
|
+
lifecycle: 'none',
|
|
27
|
+
mapping: '1 2',
|
|
28
|
+
hierarchical: '6 3 1 3',
|
|
29
|
+
associative: 'none',
|
|
30
|
+
comparative: '2 2',
|
|
31
|
+
definitional: '8 4',
|
|
32
|
+
spatiotemporal: '4 2 1 2',
|
|
33
|
+
lexical: '3 1',
|
|
34
|
+
designation: '1 3',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Build the sphere-category list from the taxonomy SSOT. */
|
|
38
|
+
export const SPHERE_RELATION_CATEGORIES: readonly SphereRelationCategory[] =
|
|
39
|
+
RELATIONSHIP_CATEGORIES.map(cat => ({
|
|
40
|
+
key: cat.id,
|
|
41
|
+
label: cat.label,
|
|
42
|
+
color: colorPairForCategory(cat.id).light,
|
|
43
|
+
dasharray: DASHARRAY[cat.id] ?? 'none',
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
export function categorizeRelationForSphere(type: string): string {
|
|
47
|
+
return categorizeRelationship(type).id;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function sphereCategoryForType(type: string): SphereRelationCategory {
|
|
51
|
+
const cat = categorizeRelationship(type);
|
|
52
|
+
return SPHERE_RELATION_CATEGORIES.find(c => c.key === cat.id)
|
|
53
|
+
?? SPHERE_RELATION_CATEGORIES[3];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function colorForTypeInMode(type: string, isDark: boolean): string {
|
|
57
|
+
const pair = colorPairForType(type);
|
|
58
|
+
return isDark ? pair.dark : pair.light;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function relationLabel(type: string): string {
|
|
62
|
+
return relationshipLabel(type);
|
|
63
|
+
}
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* RELATIONSHIP_CATEGORIES, INVERSE_RELATIONSHIPS, and the category lookup
|
|
5
5
|
* map are all computed from `taxonomies.json` at module load time.
|
|
6
6
|
* Adding a new relationship type requires only a taxonomy edit — no code changes.
|
|
7
|
+
*
|
|
8
|
+
* Color pairs (light + dark) come from `data/colors.json` via the color-theme
|
|
9
|
+
* module, merged with per-deployment overrides from `site-config.json`.
|
|
7
10
|
*/
|
|
8
11
|
import { ontology } from '../adapters/ontology-registry';
|
|
9
12
|
import type { TaxonomyCategory } from '../adapters/ontology-registry';
|
|
13
|
+
import { createColorTheme, type ColorPair, type ColorTheme } from './color-theme';
|
|
10
14
|
|
|
11
15
|
export interface RelationshipCategory {
|
|
12
16
|
id: string;
|
|
@@ -95,3 +99,29 @@ export function relationshipLabel(type: string): string {
|
|
|
95
99
|
export function relationshipDefinition(type: string): string | null {
|
|
96
100
|
return ontology.getDefinition('relationshipType', type);
|
|
97
101
|
}
|
|
102
|
+
|
|
103
|
+
// ── Color pairs (light + dark) ────────────────────────────────────────────
|
|
104
|
+
//
|
|
105
|
+
// `createColorTheme(undefined)` returns the default theme (no per-deployment
|
|
106
|
+
// overrides). Components that need overrides should construct their own theme
|
|
107
|
+
// via `useSiteConfig()` + `createColorTheme(config.colors)`.
|
|
108
|
+
|
|
109
|
+
const defaultTheme: ColorTheme = createColorTheme(undefined);
|
|
110
|
+
|
|
111
|
+
/** Returns the color pair for a relationship type, optionally given a known
|
|
112
|
+
* category id (skips a taxonomy lookup when the caller already knows it). */
|
|
113
|
+
export function colorPairForType(type: string, categoryId?: string): ColorPair {
|
|
114
|
+
const concept = ontology.getConcept('relationshipType', type);
|
|
115
|
+
const cat = categoryId ?? concept?.category;
|
|
116
|
+
return defaultTheme.relationshipTypeColor(type, cat);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Returns the color pair for a category id (e.g. "lifecycle"). */
|
|
120
|
+
export function colorPairForCategory(categoryId: string): ColorPair {
|
|
121
|
+
return defaultTheme.relationshipCategoryColor(categoryId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Construct a per-deployment theme that overrides defaults. */
|
|
125
|
+
export function colorThemeForOverrides(siteColors: Parameters<typeof createColorTheme>[0]): ColorTheme {
|
|
126
|
+
return createColorTheme(siteColors);
|
|
127
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SAFE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
2
|
+
|
|
3
|
+
const DANGEROUS_TOKEN_RE = /[\s<>"]/;
|
|
4
|
+
|
|
5
|
+
export function isSafeUrl(url: string): boolean {
|
|
6
|
+
if (!url || typeof url !== 'string') return false;
|
|
7
|
+
if (DANGEROUS_TOKEN_RE.test(url)) return false;
|
|
8
|
+
|
|
9
|
+
const trimmed = url.trim();
|
|
10
|
+
if (!trimmed) return false;
|
|
11
|
+
|
|
12
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(trimmed);
|
|
15
|
+
return SAFE_URL_PROTOCOLS.has(u.protocol);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sanitizeUrl(url: string): string {
|
|
29
|
+
return isSafeUrl(url) ? url : '';
|
|
30
|
+
}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
|
|
3
3
|
import { useRouter } from 'vue-router';
|
|
4
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
5
|
+
import { conceptUri } from '../adapters/model-bridge';
|
|
5
6
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
7
|
+
import RelationSphere from '../components/RelationSphere.vue';
|
|
6
8
|
import ShortcutsModal from '../components/ShortcutsModal.vue';
|
|
7
9
|
import { useI18n } from '../i18n';
|
|
8
10
|
|
|
@@ -50,6 +52,24 @@ const concept = computed(() => store.currentConcept);
|
|
|
50
52
|
const manifest = computed(() => store.currentManifest);
|
|
51
53
|
const edges = computed(() => store.conceptEdges);
|
|
52
54
|
const adjacent = ref({ prev: null as string | null, next: null as string | null });
|
|
55
|
+
const viewMode = ref<'detail' | 'sphere'>('detail');
|
|
56
|
+
|
|
57
|
+
/* When the user clicks a card in the sphere, we store the navigation
|
|
58
|
+
payload here. The concept loads via store.viewConcept (without
|
|
59
|
+
router.push). When the user switches to Detail, we commit the URL. */
|
|
60
|
+
const sphereFocusPayload = ref<{ registerId: string; conceptId: string } | null>(null);
|
|
61
|
+
const permalinkCopied = ref(false);
|
|
62
|
+
|
|
63
|
+
async function copyPermalink() {
|
|
64
|
+
try {
|
|
65
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
66
|
+
permalinkCopied.value = true;
|
|
67
|
+
setTimeout(() => { permalinkCopied.value = false; }, 1800);
|
|
68
|
+
} catch {
|
|
69
|
+
/* Clipboard API not available — fall back to URL prompt */
|
|
70
|
+
window.prompt('Copy this URL:', window.location.href);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
53
73
|
|
|
54
74
|
async function loadAdjacent() {
|
|
55
75
|
const adapter = store.datasets.get(props.registerId);
|
|
@@ -66,6 +86,45 @@ function goAdjacent(id: string) {
|
|
|
66
86
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
67
87
|
}
|
|
68
88
|
|
|
89
|
+
function onSphereNavigate(payload: { registerId: string; conceptId: string }) {
|
|
90
|
+
if (!payload.registerId || !payload.conceptId) return;
|
|
91
|
+
/* DON'T router.push — that sets conceptLoading=true and unmounts the
|
|
92
|
+
sphere. Instead, load the concept directly via the store. This
|
|
93
|
+
updates store.currentConcept + store.conceptEdges, which flow as
|
|
94
|
+
props to RelationSphere without any loading flash. The sphere's
|
|
95
|
+
watch on props.concept fires → rebuilds the graph → animates. */
|
|
96
|
+
sphereFocusPayload.value = { registerId: payload.registerId, conceptId: payload.conceptId };
|
|
97
|
+
(async () => {
|
|
98
|
+
try {
|
|
99
|
+
const adapter = store.datasets.get(payload.registerId);
|
|
100
|
+
if (!adapter?.index) {
|
|
101
|
+
await store.loadDataset(payload.registerId);
|
|
102
|
+
}
|
|
103
|
+
await store.viewConcept(payload.registerId, payload.conceptId);
|
|
104
|
+
loadAdjacent();
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.warn('Sphere navigation failed:', e);
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function switchToSphere() {
|
|
112
|
+
viewMode.value = 'sphere';
|
|
113
|
+
sphereFocusPayload.value = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function switchToDetail() {
|
|
117
|
+
viewMode.value = 'detail';
|
|
118
|
+
/* Commit the URL if the sphere navigated to a different concept.
|
|
119
|
+
This triggers loadConcept → the Detail view shows the right concept. */
|
|
120
|
+
if (sphereFocusPayload.value) {
|
|
121
|
+
const { registerId, conceptId } = sphereFocusPayload.value;
|
|
122
|
+
if (registerId !== props.registerId || conceptId !== props.conceptId) {
|
|
123
|
+
router.push({ name: 'concept', params: { registerId, conceptId } });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
69
128
|
function onKeydown(e: KeyboardEvent) {
|
|
70
129
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
71
130
|
|
|
@@ -78,6 +137,17 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
78
137
|
showShortcuts.value = false;
|
|
79
138
|
return;
|
|
80
139
|
}
|
|
140
|
+
/* View mode toggle: 's' for sphere, 'd' for detail */
|
|
141
|
+
if (e.key === 's' && concept.value) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
switchToSphere();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (e.key === 'd') {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
switchToDetail();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
81
151
|
if (e.key === 'j' && adjacent.value.prev) {
|
|
82
152
|
e.preventDefault();
|
|
83
153
|
goAdjacent(adjacent.value.prev);
|
|
@@ -92,7 +162,68 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
|
92
162
|
</script>
|
|
93
163
|
|
|
94
164
|
<template>
|
|
95
|
-
<div class="
|
|
165
|
+
<div :class="['concept-view', { 'sphere-mode': viewMode === 'sphere' }]">
|
|
166
|
+
<!-- View mode toolbar — slim sub-header with segmented control + permalink.
|
|
167
|
+
Sits ABOVE the content (not floating, doesn't block anything). -->
|
|
168
|
+
<div
|
|
169
|
+
v-if="!conceptLoading && !localError && concept"
|
|
170
|
+
class="flex-shrink-0 w-full max-w-7xl mx-auto mb-4 flex items-center justify-between gap-4 pb-3 border-b border-ink-100 dark:border-ink-700"
|
|
171
|
+
>
|
|
172
|
+
<nav aria-label="View mode" class="inline-flex gap-1 p-1 rounded-lg bg-surface-alt dark:bg-ink-800" role="tablist">
|
|
173
|
+
<button
|
|
174
|
+
role="tab"
|
|
175
|
+
:aria-selected="viewMode === 'detail'"
|
|
176
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-[13px] font-medium rounded-md cursor-pointer transition-all border-none font-inherit"
|
|
177
|
+
:class="viewMode === 'detail'
|
|
178
|
+
? 'bg-surface-raised dark:bg-ink-600 text-ink-800 dark:text-ink-50 shadow-sm'
|
|
179
|
+
: 'bg-transparent text-ink-500 dark:text-ink-400 hover:text-ink-700 dark:hover:text-ink-200'"
|
|
180
|
+
@click="switchToDetail"
|
|
181
|
+
title="Detail view (d)"
|
|
182
|
+
>
|
|
183
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
184
|
+
<path d="M4 6h16M4 12h16M4 18h10" stroke-linecap="round"/>
|
|
185
|
+
</svg>
|
|
186
|
+
<span>{{ t('concept.detailView') }}</span>
|
|
187
|
+
<kbd class="ml-1 px-1 py-0.5 font-mono text-[9px] font-semibold rounded bg-ink-100 dark:bg-ink-700 text-ink-500 dark:text-ink-400 tracking-wide">d</kbd>
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
role="tab"
|
|
191
|
+
:aria-selected="viewMode === 'sphere'"
|
|
192
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-[13px] font-medium rounded-md cursor-pointer transition-all border-none font-inherit"
|
|
193
|
+
:class="viewMode === 'sphere'
|
|
194
|
+
? 'bg-surface-raised dark:bg-ink-600 text-ink-800 dark:text-ink-50 shadow-sm'
|
|
195
|
+
: 'bg-transparent text-ink-500 dark:text-ink-400 hover:text-ink-700 dark:hover:text-ink-200'"
|
|
196
|
+
@click="switchToSphere"
|
|
197
|
+
title="Relation sphere view (s)"
|
|
198
|
+
>
|
|
199
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
200
|
+
<circle cx="12" cy="12" r="9"/>
|
|
201
|
+
<ellipse cx="12" cy="12" rx="9" ry="3.5"/>
|
|
202
|
+
<ellipse cx="12" cy="12" rx="3.5" ry="9"/>
|
|
203
|
+
</svg>
|
|
204
|
+
<span>{{ t('concept.relationSphere') }}</span>
|
|
205
|
+
<kbd class="ml-1 px-1 py-0.5 font-mono text-[9px] font-semibold rounded bg-ink-100 dark:bg-ink-700 text-ink-500 dark:text-ink-400 tracking-wide">s</kbd>
|
|
206
|
+
</button>
|
|
207
|
+
</nav>
|
|
208
|
+
|
|
209
|
+
<div class="flex items-center gap-2.5">
|
|
210
|
+
<button
|
|
211
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-[13px] font-medium text-ink-500 dark:text-ink-400 bg-transparent border border-ink-100 dark:border-ink-700 rounded-md cursor-pointer transition-all font-inherit hover:text-ink-800 dark:hover:text-ink-100 hover:bg-surface-raised dark:hover:bg-ink-700 hover:border-ink-200 dark:hover:border-ink-600"
|
|
212
|
+
title="Copy permalink to this concept"
|
|
213
|
+
@click="copyPermalink"
|
|
214
|
+
>
|
|
215
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
216
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101"/>
|
|
217
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M10.172 13.828a4 4 0 005.656 0l4-4a4 4 0 10-5.656-5.656l-1.1 1.1"/>
|
|
218
|
+
</svg>
|
|
219
|
+
<span>{{ t('concept.permalink') }}</span>
|
|
220
|
+
</button>
|
|
221
|
+
<Transition name="fade">
|
|
222
|
+
<span v-if="permalinkCopied" class="text-xs font-semibold text-green-600 dark:text-green-400">{{ t('concept.copied') }}</span>
|
|
223
|
+
</Transition>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
96
227
|
<div v-if="conceptLoading" class="max-w-5xl mx-auto py-8 space-y-5">
|
|
97
228
|
<!-- Breadcrumb skeleton -->
|
|
98
229
|
<div class="flex items-center gap-1.5">
|
|
@@ -141,15 +272,58 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
|
141
272
|
</router-link>
|
|
142
273
|
</div>
|
|
143
274
|
</div>
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
275
|
+
<template v-else-if="concept && manifest">
|
|
276
|
+
<div class="concept-content" :class="{ 'sphere-content': viewMode === 'sphere' }">
|
|
277
|
+
<!-- Sphere mode — receives concept + edges directly, no URI matching -->
|
|
278
|
+
<RelationSphere
|
|
279
|
+
v-if="viewMode === 'sphere'"
|
|
280
|
+
:concept="concept"
|
|
281
|
+
:manifest="manifest"
|
|
282
|
+
:register-id="registerId"
|
|
283
|
+
:edges="edges"
|
|
284
|
+
@navigate="onSphereNavigate"
|
|
285
|
+
/>
|
|
286
|
+
<!-- Detail mode -->
|
|
287
|
+
<ConceptDetail
|
|
288
|
+
v-else
|
|
289
|
+
:concept="concept"
|
|
290
|
+
:manifest="manifest"
|
|
291
|
+
:edges="edges"
|
|
292
|
+
:adjacent="adjacent"
|
|
293
|
+
:register-id="registerId"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</template>
|
|
152
297
|
|
|
153
298
|
<ShortcutsModal v-if="showShortcuts" @close="showShortcuts = false" />
|
|
154
299
|
</div>
|
|
155
300
|
</template>
|
|
301
|
+
|
|
302
|
+
<style scoped>
|
|
303
|
+
.concept-view {
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
position: relative;
|
|
307
|
+
padding: 1rem;
|
|
308
|
+
min-height: calc(100vh - 56px);
|
|
309
|
+
}
|
|
310
|
+
.concept-view.sphere-mode {
|
|
311
|
+
height: calc(100vh - 56px);
|
|
312
|
+
overflow: hidden;
|
|
313
|
+
}
|
|
314
|
+
.concept-content {
|
|
315
|
+
flex: 1;
|
|
316
|
+
min-height: 0;
|
|
317
|
+
width: 100%;
|
|
318
|
+
max-width: 80rem;
|
|
319
|
+
margin: 0 auto;
|
|
320
|
+
}
|
|
321
|
+
.concept-content.sphere-content {
|
|
322
|
+
position: relative;
|
|
323
|
+
overflow: hidden;
|
|
324
|
+
display: flex;
|
|
325
|
+
flex-direction: column;
|
|
326
|
+
}
|
|
327
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s; }
|
|
328
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
329
|
+
</style>
|
|
@@ -7,6 +7,7 @@ import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
|
7
7
|
import { FORMAT_LABELS } from '../config/types';
|
|
8
8
|
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
9
9
|
import ConceptCard from '../components/ConceptCard.vue';
|
|
10
|
+
import DatasetSeriesCard from '../components/DatasetSeriesCard.vue';
|
|
10
11
|
import { useI18n, locale } from '../i18n';
|
|
11
12
|
import { useSiteConfig } from '../config/use-site-config';
|
|
12
13
|
import type { SectionNode, ConceptSummary } from '../adapters/types';
|
|
@@ -321,6 +322,11 @@ function clearSection() {
|
|
|
321
322
|
</div>
|
|
322
323
|
</div>
|
|
323
324
|
|
|
325
|
+
<!-- Edition series sidebar (only renders if this dataset belongs to a multi-edition series) -->
|
|
326
|
+
<div v-if="manifest" class="mb-6">
|
|
327
|
+
<DatasetSeriesCard :register-id="registerId" />
|
|
328
|
+
</div>
|
|
329
|
+
|
|
324
330
|
<!-- Downloads -->
|
|
325
331
|
<div v-if="bulkDownloads.length" class="card p-4 mb-6">
|
|
326
332
|
<h3 class="text-xs font-semibold text-ink-400 uppercase tracking-wide mb-3">{{ t('dataset.download') }}</h3>
|
package/src/views/HomeView.vue
CHANGED
|
@@ -5,6 +5,7 @@ import { useRouter } from 'vue-router';
|
|
|
5
5
|
import { useDsStyle } from '../utils/dataset-style';
|
|
6
6
|
import { useSiteConfig } from '../config/use-site-config';
|
|
7
7
|
import { useI18n } from '../i18n';
|
|
8
|
+
import HomeSeriesSection from '../components/HomeSeriesSection.vue';
|
|
8
9
|
|
|
9
10
|
const store = useVocabularyStore();
|
|
10
11
|
const router = useRouter();
|
|
@@ -74,6 +75,10 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
74
75
|
<p class="text-base text-ink-400 max-w-lg leading-relaxed">
|
|
75
76
|
{{ localizedDescription || 'Explore standardized terminology datasets from ISO and IEC technical committees. Browse concepts, definitions, and cross-references across multilingual vocabularies.' }}
|
|
76
77
|
</p>
|
|
78
|
+
|
|
79
|
+
<!-- Edition-series section (renders only if multi-edition series exist) -->
|
|
80
|
+
<HomeSeriesSection />
|
|
81
|
+
|
|
77
82
|
<div class="flex flex-wrap gap-3 mt-7">
|
|
78
83
|
<button @click="goToSearch" class="btn-primary flex items-center gap-2">
|
|
79
84
|
<svg class="w-4 h-4" 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>
|