@glossarist/concept-browser 0.7.78 → 0.7.81

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.
Files changed (57) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +109 -39
  3. package/scripts/generate-data.mjs +63 -27
  4. package/src/__tests__/build-edges-redundancy.test.ts +101 -0
  5. package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
  6. package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
  7. package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
  8. package/src/__tests__/config/page-types.test.ts +82 -0
  9. package/src/__tests__/model-bridge-bridges.test.ts +3 -2
  10. package/src/__tests__/model-bridge-citation.test.ts +1 -1
  11. package/src/__tests__/ontology-registry.test.ts +2 -2
  12. package/src/__tests__/partitive-relation-styling.test.ts +82 -0
  13. package/src/__tests__/partitive-relation.test.ts +285 -0
  14. package/src/adapters/GraphDataSource.ts +16 -6
  15. package/src/adapters/dataset-stats.ts +13 -0
  16. package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
  17. package/src/adapters/types.ts +27 -17
  18. package/src/components/AppSidebar.vue +14 -57
  19. package/src/components/ConceptDetail.vue +29 -0
  20. package/src/components/ConceptDiffModal.vue +120 -0
  21. package/src/components/ConceptDiffView.vue +192 -0
  22. package/src/components/ConceptEditionRail.vue +61 -36
  23. package/src/components/PartitiveRelationList.vue +139 -0
  24. package/src/components/groups/DatasetGroupRenderer.vue +14 -4
  25. package/src/components/groups/DefaultGroupSidebar.vue +28 -11
  26. package/src/components/groups/LineageGroupSidebar.vue +17 -29
  27. package/src/composables/use-concept-diff.ts +231 -0
  28. package/src/composables/use-concept-edges.ts +66 -45
  29. package/src/composables/use-dataset-stats.ts +52 -0
  30. package/src/config/page-types.ts +67 -0
  31. package/src/config/types.ts +6 -1
  32. package/src/config/use-site-config.ts +13 -29
  33. package/src/data/taxonomies.json +24 -1
  34. package/src/i18n/locales/eng.yml +6 -0
  35. package/src/main.ts +1 -1
  36. package/src/style.css +1 -1
  37. package/src/utils/partitive-relation-styling.ts +62 -0
  38. package/src/views/DatasetView.vue +5 -3
  39. package/src/views/StatsView.vue +9 -22
  40. package/src/__tests__/hyperedge-styling.test.ts +0 -57
  41. package/src/__tests__/partitive-hyperedge.test.ts +0 -199
  42. package/src/components/PartitiveHyperedgeList.vue +0 -112
  43. package/src/islands/BibliographyIsland.vue +0 -26
  44. package/src/islands/ConceptDetailIsland.vue +0 -27
  45. package/src/islands/ConceptDiffIsland.vue +0 -214
  46. package/src/islands/ConceptIsland.vue +0 -102
  47. package/src/islands/GraphIsland.vue +0 -15
  48. package/src/islands/GroupRendererIsland.vue +0 -15
  49. package/src/islands/HeaderIsland.vue +0 -10
  50. package/src/islands/NonVerbalIsland.vue +0 -75
  51. package/src/islands/RelationSphereIsland.vue +0 -20
  52. package/src/islands/SearchIsland.vue +0 -23
  53. package/src/islands/SeriesCardIsland.vue +0 -12
  54. package/src/islands/ShortcutsIsland.vue +0 -13
  55. package/src/islands/SidebarIsland.vue +0 -15
  56. package/src/islands/SupersessionDiffIsland.vue +0 -267
  57. package/src/utils/hyperedge-styling.ts +0 -48
@@ -1,199 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import type { PartitiveHyperedge as JsonLdPartitiveHyperedge } from '../adapters/types';
3
-
4
- // Re-implements the extractor under test so we can test it in isolation
5
- // without spinning up the full build-edges.js pipeline. The production
6
- // extractor in scripts/build-edges.js uses the same logic.
7
- function extractPartitiveHyperedges(
8
- concept: { '@id': string; 'gl:partitiveHyperedges'?: any[] },
9
- registerId: string,
10
- uriBase: string,
11
- urnMap: Map<string, string>,
12
- ): JsonLdPartitiveHyperedge[] {
13
- const hyperedges: JsonLdPartitiveHyperedge[] = [];
14
- const sourceUri = concept['@id'];
15
-
16
- const resolveConceptUri = (ref: any): string | null => {
17
- const source = ref?.['gl:source'] || ref?.source;
18
- const id = ref?.['gl:id'] || ref?.id;
19
- if (!source || !id) return null;
20
- const reg = urnMap.get(source) || source;
21
- return `${uriBase}/${reg}/concept/${id}`;
22
- };
23
-
24
- for (const he of concept['gl:partitiveHyperedges'] || []) {
25
- const comprehensive = resolveConceptUri(he['gl:comprehensive']);
26
- if (!comprehensive) continue;
27
- const parts = (he['gl:hasPart'] || [])
28
- .map(resolveConceptUri)
29
- .filter((p: string | null): p is string => !!p && p !== sourceUri);
30
- if (parts.length === 0) continue;
31
- const rawMarkers = he['gl:hasPluralityMarker'] || [];
32
- for (const m of rawMarkers) {
33
- if (m !== 'double' && m !== 'dashed') {
34
- throw new Error(`Invalid partitive hyperedge marker: "${m}". Allowed: double, dashed`);
35
- }
36
- }
37
- hyperedges.push({
38
- source: sourceUri,
39
- comprehensive,
40
- parts,
41
- enumeration: he['gl:enumeration'] || 'closed',
42
- markers: rawMarkers as ('double' | 'dashed')[],
43
- label: he['gl:content'] || undefined,
44
- register: registerId,
45
- });
46
- }
47
- return hyperedges;
48
- }
49
-
50
- describe('extractPartitiveHyperedges', () => {
51
- const uriBase = 'https://example.org';
52
- const urnMap = new Map([['urn:vim:pub:v:2:2012', 'vim-2012']]);
53
- const registerId = 'vim-2012';
54
- const sourceUri = `${uriBase}/${registerId}/concept/2.9`;
55
-
56
- it('extracts a closed hyperedge with double marker', () => {
57
- const concept = {
58
- '@id': sourceUri,
59
- 'gl:partitiveHyperedges': [
60
- {
61
- 'gl:comprehensive': {
62
- 'gl:source': 'urn:vim:pub:v:2:2012',
63
- 'gl:id': '2.9',
64
- },
65
- 'gl:hasPart': [
66
- { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' },
67
- { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.26' },
68
- ],
69
- 'gl:enumeration': 'closed',
70
- 'gl:hasPluralityMarker': ['double'],
71
- 'gl:content': 'value + uncertainty',
72
- },
73
- ],
74
- };
75
-
76
- const result = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
77
- expect(result).toHaveLength(1);
78
- expect(result[0].source).toBe(sourceUri);
79
- expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/2.9`);
80
- expect(result[0].parts).toEqual([
81
- `${uriBase}/${registerId}/concept/2.10`,
82
- `${uriBase}/${registerId}/concept/2.26`,
83
- ]);
84
- expect(result[0].enumeration).toBe('closed');
85
- expect(result[0].markers).toEqual(['double']);
86
- expect(result[0].label).toBe('value + uncertainty');
87
- });
88
-
89
- it('defaults enumeration to closed when omitted', () => {
90
- const concept = {
91
- '@id': sourceUri,
92
- 'gl:partitiveHyperedges': [
93
- {
94
- 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
95
- 'gl:hasPart': [{ 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' }],
96
- },
97
- ],
98
- };
99
- const [he] = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
100
- expect(he.enumeration).toBe('closed');
101
- });
102
-
103
- it('preserves open enumeration', () => {
104
- const concept = {
105
- '@id': sourceUri,
106
- 'gl:partitiveHyperedges': [
107
- {
108
- 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
109
- 'gl:hasPart': [{ 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }],
110
- 'gl:enumeration': 'open',
111
- },
112
- ],
113
- };
114
- const [he] = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
115
- expect(he.enumeration).toBe('open');
116
- });
117
-
118
- it('preserves both markers', () => {
119
- const concept = {
120
- '@id': sourceUri,
121
- 'gl:partitiveHyperedges': [
122
- {
123
- 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
124
- 'gl:hasPart': [{ 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }],
125
- 'gl:enumeration': 'open',
126
- 'gl:hasPluralityMarker': ['double', 'dashed'],
127
- },
128
- ],
129
- };
130
- const [he] = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
131
- expect(he.markers).toEqual(['double', 'dashed']);
132
- });
133
-
134
- it('throws on invalid marker values', () => {
135
- const concept = {
136
- '@id': sourceUri,
137
- 'gl:partitiveHyperedges': [
138
- {
139
- 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
140
- 'gl:hasPart': [{ 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }],
141
- 'gl:hasPluralityMarker': ['double', 'dotted'],
142
- },
143
- ],
144
- };
145
- expect(() => extractPartitiveHyperedges(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*marker.*dotted/);
146
- });
147
-
148
- it('skips a hyperedge whose comprehensive cannot be resolved', () => {
149
- const concept = {
150
- '@id': sourceUri,
151
- 'gl:partitiveHyperedges': [
152
- {
153
- 'gl:comprehensive': { 'gl:id': '1.3' }, // missing source
154
- 'gl:hasPart': [{ 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }],
155
- },
156
- ],
157
- };
158
- const result = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
159
- expect(result).toHaveLength(0);
160
- });
161
-
162
- it('skips a hyperedge with no resolvable parts', () => {
163
- const concept = {
164
- '@id': sourceUri,
165
- 'gl:partitiveHyperedges': [
166
- {
167
- 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
168
- 'gl:hasPart': [{ 'gl:id': '1.4' }], // missing source
169
- },
170
- ],
171
- };
172
- const result = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
173
- expect(result).toHaveLength(0);
174
- });
175
-
176
- it('excludes the source concept from parts list (self-loop guard)', () => {
177
- const concept = {
178
- '@id': sourceUri,
179
- 'gl:partitiveHyperedges': [
180
- {
181
- 'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
182
- 'gl:hasPart': [
183
- { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' },
184
- { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' }, // self-ref
185
- ],
186
- },
187
- ],
188
- };
189
- const [he] = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
190
- expect(he.parts).toHaveLength(1);
191
- expect(he.parts[0]).toBe(`${uriBase}/${registerId}/concept/2.10`);
192
- });
193
-
194
- it('returns empty array when concept has no partitiveHyperedges', () => {
195
- const concept = { '@id': sourceUri };
196
- const result = extractPartitiveHyperedges(concept, registerId, uriBase, urnMap);
197
- expect(result).toEqual([]);
198
- });
199
- });
@@ -1,112 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * PartitiveHyperedgeList — renders one-to-many partitive decompositions
4
- * as grouped cards. Each hyperedge shows:
5
- * - the comprehensive (whole) concept as the card header
6
- * - the parts as indented children
7
- * - enumeration badge (closed / open)
8
- * - marker badges (double / dashed) when present
9
- * - optional content (label / note)
10
- *
11
- * Independent of RelationshipList.vue, which renders binary edges.
12
- * Both consume the same edges/hyperedges stores but render in
13
- * separate sections.
14
- *
15
- * See concept-model/TODO.hyperedge/00-design-overview.md.
16
- */
17
- import type { PartitiveHyperedge, Manifest } from '../adapters/types';
18
- import { useRouter } from 'vue-router';
19
- import { useVocabularyStore } from '../stores/vocabulary';
20
- import { getFactory } from '../adapters/factory';
21
- import { useI18n } from '../i18n';
22
- import { hyperedgeStyle, markerColor, enumerationLabel } from '../utils/hyperedge-styling';
23
- import { conceptIdFromUri } from '../utils/concept-helpers';
24
-
25
- const props = defineProps<{
26
- hyperedges: PartitiveHyperedge[];
27
- manifest: Manifest;
28
- registerId: string;
29
- }>();
30
-
31
- const emit = defineEmits<{
32
- (e: 'navigate', registerId: string, conceptId: string): void;
33
- }>();
34
-
35
- const router = useRouter();
36
- const store = useVocabularyStore();
37
- const factory = getFactory();
38
- const { t } = useI18n();
39
-
40
- function labelFor(uri: string): string {
41
- const parsed = factory.resolve(uri);
42
- if (parsed.type === 'internal') {
43
- return conceptIdFromUri(uri) || uri;
44
- }
45
- return uri;
46
- }
47
-
48
- function navigate(uri: string) {
49
- const parsed = factory.resolve(uri);
50
- if (parsed.type === 'internal') {
51
- store.viewConcept(parsed.registerId, parsed.conceptId);
52
- router.push({
53
- name: 'concept',
54
- params: { registerId: parsed.registerId, conceptId: parsed.conceptId },
55
- });
56
- }
57
- }
58
-
59
- function enumerationBadge(e: 'closed' | 'open'): string {
60
- return e === 'open'
61
- ? t('partitive.enumeration.open') || 'open'
62
- : t('partitive.enumeration.closed') || 'closed';
63
- }
64
- </script>
65
-
66
- <template>
67
- <div v-if="hyperedges.length > 0" class="space-y-3">
68
- <div class="section-label flex items-center gap-1.5 mb-2">
69
- <span class="inline-block w-2 h-2 rounded-full bg-emerald-600"></span>
70
- {{ t('partitive.hyperedges') || 'Partitive groups' }}
71
- </div>
72
- <div
73
- v-for="(he, i) in hyperedges"
74
- :key="i"
75
- class="rounded-lg border border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20 overflow-hidden"
76
- >
77
- <div class="px-3 py-2 flex items-center gap-2 border-b border-emerald-200 dark:border-emerald-800">
78
- <span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-200 dark:bg-emerald-800 text-emerald-900 dark:text-emerald-100 uppercase">
79
- {{ enumerationBadge(he.enumeration) }}
80
- </span>
81
- <span
82
- v-for="marker in he.markers"
83
- :key="marker"
84
- class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-100 uppercase"
85
- :title="marker === 'double' ? 'close-set double line: several parts of same type' : 'dashed line: plurality uncertain'"
86
- >
87
- {{ marker }}
88
- </span>
89
- <button
90
- @click="navigate(he.comprehensive)"
91
- class="ml-auto text-sm font-medium text-emerald-900 dark:text-emerald-100 hover:underline truncate"
92
- >
93
- {{ labelFor(he.comprehensive) }}
94
- </button>
95
- </div>
96
- <div v-if="he.label" class="px-3 py-1 text-xs text-emerald-800 dark:text-emerald-200 italic border-b border-emerald-200 dark:border-emerald-800">
97
- {{ he.label }}
98
- </div>
99
- <ul class="divide-y divide-emerald-100 dark:divide-emerald-900/30">
100
- <li v-for="(part, j) in he.parts" :key="j">
101
- <button
102
- @click="navigate(part)"
103
- class="block w-full text-left px-3 py-1.5 text-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/30 text-emerald-900 dark:text-emerald-100"
104
- >
105
- <span class="text-emerald-600 dark:text-emerald-400 mr-2">└</span>
106
- {{ labelFor(part) }}
107
- </button>
108
- </li>
109
- </ul>
110
- </div>
111
- </div>
112
- </template>
@@ -1,26 +0,0 @@
1
- <template>
2
- <section v-if="entries.length" class="bibliography-island">
3
- <h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">Bibliography</h2>
4
- <ul class="space-y-1">
5
- <li v-for="entry in entries" :key="entry.id" class="text-sm flex items-start gap-2">
6
- <span class="text-ink-400 font-mono text-xs shrink-0">[{{ entry.id }}]</span>
7
- <div>
8
- <span class="text-ink-600 dark:text-ink-300">{{ entry.reference }}</span>
9
- <a v-if="entry.link" :href="entry.link" target="_blank" class="text-blue-600 hover:underline text-xs ml-2">↗</a>
10
- </div>
11
- </li>
12
- </ul>
13
- </section>
14
- </template>
15
-
16
- <script setup lang="ts">
17
- defineProps<{
18
- entries: Array<{
19
- id: string;
20
- reference: string;
21
- title?: string;
22
- link?: string;
23
- type?: string;
24
- }>;
25
- }>();
26
- </script>
@@ -1,27 +0,0 @@
1
- <template>
2
- <!-- Full concept detail using the REAL ConceptDetail.vue -->
3
- <ErrorBoundary title="Concept view failed" :retry-key="conceptUri">
4
- <ConceptDetail
5
- :concept="concept"
6
- :register-id="registerId"
7
- :concept-uri="conceptUri"
8
- />
9
- </ErrorBoundary>
10
- </template>
11
-
12
- <script setup lang="ts">
13
- import { onErrorCaptured } from 'vue';
14
- import ConceptDetail from '../components/ConceptDetail.vue';
15
- import ErrorBoundary from '../components/ErrorBoundary.vue';
16
-
17
- defineProps<{
18
- concept: any;
19
- registerId: string;
20
- conceptUri: string;
21
- }>();
22
-
23
- onErrorCaptured((err) => {
24
- console.error('ConceptDetail island error:', err);
25
- return false;
26
- });
27
- </script>
@@ -1,214 +0,0 @@
1
- <template>
2
- <div class="concept-diff-island">
3
- <!-- Edition selector -->
4
- <div v-if="editions.length > 1" class="mb-4">
5
- <label class="text-sm font-semibold text-ink-500 uppercase tracking-wider mr-2">Compare with:</label>
6
- <select v-model="compareEdition" class="text-sm border border-ink-200 dark:border-ink-700 rounded px-2 py-1 bg-surface dark:bg-ink-800 text-ink-600 dark:text-ink-300">
7
- <option value="">Select edition...</option>
8
- <option v-for="ed in otherEditions" :key="ed.id" :value="ed.id">
9
- {{ ed.title }} ({{ ed.year ?? '?' }})
10
- </option>
11
- </select>
12
- </div>
13
-
14
- <!-- Diff rendering -->
15
- <div v-if="diffResult && compareEdition" class="space-y-4">
16
- <!-- Similarity score -->
17
- <div v-if="diffResult.similarity != null" class="flex items-center gap-3">
18
- <div class="text-sm text-ink-500">Similarity:</div>
19
- <div class="flex-1 max-w-xs bg-ink-100 dark:bg-ink-800 rounded-full h-3 overflow-hidden">
20
- <div
21
- class="h-full rounded-full transition-all duration-300"
22
- :class="similarityColor(diffResult.similarity)"
23
- :style="{ width: `${Math.round(diffResult.similarity * 100)}%` }"
24
- />
25
- </div>
26
- <div class="text-sm font-medium" :class="similarityTextColor(diffResult.similarity)">
27
- {{ Math.round(diffResult.similarity * 100) }}%
28
- </div>
29
- </div>
30
-
31
- <!-- Per-field diffs -->
32
- <div v-for="(section, field) in sections" :key="field" class="card p-4">
33
- <h3 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">{{ fieldLabel(field) }}</h3>
34
- <div v-if="section.type === 'added'" class="text-emerald-600 dark:text-emerald-400">
35
- <span class="text-xs font-medium bg-emerald-100 dark:bg-emerald-900/30 px-1.5 py-0.5 rounded mr-2">ADDED</span>
36
- <span dir="auto">{{ section.value }}</span>
37
- </div>
38
- <div v-else-if="section.type === 'removed'" class="text-red-500 dark:text-red-400 line-through opacity-70">
39
- <span class="text-xs font-medium bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded mr-2 no-underline">REMOVED</span>
40
- <span dir="auto">{{ section.value }}</span>
41
- </div>
42
- <div v-else-if="section.type === 'changed'" class="space-y-1">
43
- <div class="text-red-500 dark:text-red-400 line-through opacity-70" dir="auto">{{ section.oldValue }}</div>
44
- <div class="text-emerald-600 dark:text-emerald-400" dir="auto">{{ section.newValue }}</div>
45
- </div>
46
- <div v-else-if="section.hunks" class="font-mono text-xs space-y-0.5">
47
- <span
48
- v-for="(hunk, i) in section.hunks"
49
- :key="i"
50
- :class="{
51
- 'bg-emerald-100 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300': hunk.type === 'added',
52
- 'bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300 line-through': hunk.type === 'removed',
53
- 'text-ink-500': hunk.type === 'equal',
54
- }"
55
- dir="auto"
56
- >{{ hunk.text }}</span>
57
- </div>
58
- <div v-else class="text-xs text-ink-400 italic">No changes</div>
59
- </div>
60
- </div>
61
-
62
- <!-- No changes -->
63
- <div v-else-if="diffResult && diffResult.hasChanges === false" class="text-center py-8">
64
- <p class="text-ink-400">No differences between these editions.</p>
65
- </div>
66
-
67
- <!-- Loading -->
68
- <div v-else-if="loading" class="text-center py-8">
69
- <div class="inline-block w-8 h-8 border-4 border-blue-200 border-t-blue-500 rounded-full animate-spin mb-2"></div>
70
- <p class="text-ink-400 text-sm">Comparing editions...</p>
71
- </div>
72
-
73
- <!-- Hint -->
74
- <div v-else-if="!compareEdition" class="text-center py-4">
75
- <p class="text-sm text-ink-400">Select an edition above to see what changed.</p>
76
- </div>
77
- </div>
78
- </template>
79
-
80
- <script setup lang="ts">
81
- import { ref, computed, watch } from 'vue';
82
- import { diffConcepts } from 'glossarist/diff';
83
- import type { ConceptDiff } from 'glossarist/diff';
84
-
85
- const props = defineProps<{
86
- currentConcept: any;
87
- registerId: string;
88
- editions: Array<{
89
- id: string;
90
- title: string;
91
- year?: number;
92
- conceptData?: any;
93
- }>;
94
- }>();
95
-
96
- const compareEdition = ref('');
97
- const diffResult = ref<ConceptDiff | null>(null);
98
- const loading = ref(false);
99
-
100
- const otherEditions = computed(() =>
101
- props.editions.filter(e => e.id !== props.registerId)
102
- );
103
-
104
- watch(compareEdition, async (edId) => {
105
- if (!edId) {
106
- diffResult.value = null;
107
- return;
108
- }
109
- const edition = props.editions.find(e => e.id === edId);
110
- if (!edition?.conceptData) {
111
- diffResult.value = null;
112
- return;
113
- }
114
- loading.value = true;
115
- try {
116
- const oldConcept = toConceptLike(edition.conceptData, edId);
117
- const newConcept = toConceptLike(props.currentConcept, props.registerId);
118
- diffResult.value = diffConcepts(oldConcept, newConcept);
119
- } catch (e) {
120
- console.error('Diff failed:', e);
121
- diffResult.value = null;
122
- } finally {
123
- loading.value = false;
124
- }
125
- });
126
-
127
- function toConceptLike(data: any, registerId: string) {
128
- const langs = data.languages ?? (data.localizations ? Object.keys(data.localizations) : []);
129
- return {
130
- id: data.conceptId ?? data.id,
131
- termid: String(data.conceptId ?? data.id),
132
- status: data.status,
133
- languages: langs,
134
- localization(lang: string) {
135
- const loc = (data.localizations ?? {})[lang];
136
- if (!loc) return null;
137
- return {
138
- languageCode: loc.languageCode ?? lang,
139
- terms: loc.terms ?? [],
140
- definitions: loc.definitions ?? [],
141
- notes: loc.notes ?? [],
142
- examples: loc.examples ?? [],
143
- };
144
- },
145
- };
146
- }
147
-
148
- const sections = computed(() => {
149
- if (!diffResult.value) return {};
150
- const out: Record<string, any> = {};
151
- const diff = diffResult.value as any;
152
- const loc = diff.localization?.('eng') ?? diff.localizations?.eng;
153
- if (!loc) return out;
154
-
155
- const defChanged = loc.definitions?.changed?.[0];
156
- if (defChanged?.textDiff?.hunks?.length) {
157
- out.definition = { hunks: defChanged.textDiff.hunks };
158
- } else if (loc.definitions?.added?.length) {
159
- out.definition = { type: 'added', value: loc.definitions.added[0].value?.content ?? loc.definitions.added[0].value };
160
- } else if (loc.definitions?.removed?.length) {
161
- out.definition = { type: 'removed', value: loc.definitions.removed[0].value?.content ?? loc.definitions.removed[0].value };
162
- }
163
-
164
- const termChanged = loc.designations?.changed?.[0];
165
- if (termChanged?.textDiff?.hunks?.length) {
166
- out.terms = { hunks: termChanged.textDiff.hunks };
167
- } else if (loc.designations?.added?.length || loc.designations?.removed?.length) {
168
- out.terms = {
169
- type: 'changed',
170
- oldValue: (loc.designations.removed ?? []).map((d: any) => d.value?.designation ?? d.value?.text ?? String(d.value ?? '')).join('; '),
171
- newValue: (loc.designations.added ?? []).map((d: any) => d.value?.designation ?? d.value?.text ?? String(d.value ?? '')).join('; '),
172
- };
173
- }
174
-
175
- if (loc.notes?.added?.length || loc.notes?.removed?.length) {
176
- out.notes = {
177
- type: 'changed',
178
- oldValue: (loc.notes.removed ?? []).map((n: any) => n.value?.content ?? n.value?.text ?? String(n.value ?? '')).join('; '),
179
- newValue: (loc.notes.added ?? []).map((n: any) => n.value?.content ?? n.value?.text ?? String(n.value ?? '')).join('; '),
180
- };
181
- }
182
-
183
- if (loc.examples?.added?.length || loc.examples?.removed?.length) {
184
- out.examples = {
185
- type: 'changed',
186
- oldValue: (loc.examples.removed ?? []).map((e: any) => e.value?.content ?? e.value?.text ?? String(e.value ?? '')).join('; '),
187
- newValue: (loc.examples.added ?? []).map((e: any) => e.value?.content ?? e.value?.text ?? String(e.value ?? '')).join('; '),
188
- };
189
- }
190
-
191
- return out;
192
- });
193
-
194
- function fieldLabel(field: string): string {
195
- const labels: Record<string, string> = {
196
- definition: 'Definition',
197
- terms: 'Terms',
198
- notes: 'Notes',
199
- examples: 'Examples',
200
- };
201
- return labels[field] ?? field;
202
- }
203
-
204
- function similarityColor(score: number): string {
205
- if (score >= 0.8) return 'bg-emerald-500';
206
- if (score >= 0.5) return 'bg-amber-500';
207
- return 'bg-red-500';
208
- }
209
- function similarityTextColor(score: number): string {
210
- if (score >= 0.8) return 'text-emerald-600 dark:text-emerald-400';
211
- if (score >= 0.5) return 'text-amber-600 dark:text-amber-400';
212
- return 'text-red-600 dark:text-red-400';
213
- }
214
- </script>
@@ -1,102 +0,0 @@
1
- <template>
2
- <div class="concept-island">
3
- <!-- View mode toggle -->
4
- <div class="flex items-center justify-between mb-4">
5
- <div class="flex items-center gap-2">
6
- <button @click="viewMode = 'detail'" :class="['px-3 py-1 text-sm rounded transition-colors', viewMode === 'detail' ? 'bg-blue-500 text-white' : 'text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800']">Detail</button>
7
- <button @click="viewMode = 'sphere'" :class="['px-3 py-1 text-sm rounded transition-colors', viewMode === 'sphere' ? 'bg-blue-500 text-white' : 'text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800']">Sphere</button>
8
- </div>
9
- <button @click="toggleRdf()" class="px-3 py-1 text-sm rounded text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800 transition-colors">RDF Source {{ rdfOpen ? '▾' : '▸' }}</button>
10
- </div>
11
-
12
- <!-- Detail view using REAL SPA components -->
13
- <div v-if="viewMode === 'detail'" class="space-y-6">
14
- <!-- Designations from real DesignationList -->
15
- <DesignationList v-if="hasDesignations" :designations="parsedDesignations" :language="activeLang" />
16
-
17
- <!-- Definition -->
18
- <section v-if="concept.definition?.eng">
19
- <h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-2">Definition</h2>
20
- <p class="text-ink-700 dark:text-ink-200 leading-relaxed" dir="auto">{{ concept.definition.eng }}</p>
21
- </section>
22
-
23
- <!-- Non-verbal representations from real NonVerbalRepDisplay -->
24
- <NonVerbalRepDisplay v-if="concept.nonVerbalRep?.length" :reps="concept.nonVerbalRep" :register-id="registerId" />
25
-
26
- <!-- Format downloads from real FormatDownloads -->
27
- <FormatDownloads v-if="concept.formats?.length" :formats="concept.formats" :register-id="registerId" />
28
-
29
- <!-- Groups / Sections -->
30
- <section v-if="concept.groups?.length">
31
- <h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-2">Sections</h2>
32
- <div class="flex flex-wrap gap-2">
33
- <span v-for="g in concept.groups" :key="g" class="text-xs px-2 py-1 rounded bg-ink-100 dark:bg-ink-800 text-ink-600 dark:text-ink-300">{{ g }}</span>
34
- </div>
35
- </section>
36
- </div>
37
-
38
- <!-- Sphere view placeholder -->
39
- <div v-else class="border border-ink-200 dark:border-ink-700 rounded-lg p-8 text-center">
40
- <p class="text-ink-400">3D sphere visualization loads on interaction</p>
41
- </div>
42
-
43
- <!-- RDF panel — lazy-loaded client-side only (glossarist/rdf is Node-only) -->
44
- <component v-if="rdfOpen && RdfComponent" :is="RdfComponent" :concept="parsedConcept" :register-id="registerId" :concept-uri-value="conceptUri" />
45
- </div>
46
- </template>
47
-
48
- <script setup lang="ts">
49
- import { ref, computed, shallowRef, onMounted } from 'vue';
50
- import DesignationList from '../components/DesignationList.vue';
51
- import NonVerbalRepDisplay from '../components/NonVerbalRepDisplay.vue';
52
- import FormatDownloads from '../components/FormatDownloads.vue';
53
-
54
- const props = defineProps<{
55
- concept: any;
56
- registerId: string;
57
- conceptUri: string;
58
- }>();
59
-
60
- const viewMode = ref<'detail' | 'sphere'>('detail');
61
- const rdfOpen = ref(false);
62
- const activeLang = ref('eng');
63
- const RdfComponent = shallowRef<any>(null);
64
-
65
- // Lazy-load ConceptRdfView only when RDF panel is opened
66
- // (it imports glossarist/rdf which has Node-only deps — can't SSR)
67
- async function toggleRdf() {
68
- rdfOpen.value = !rdfOpen.value;
69
- if (rdfOpen.value && !RdfComponent.value) {
70
- try {
71
- const mod = await import('../components/ConceptRdfView.vue');
72
- RdfComponent.value = mod.default;
73
- } catch (e) {
74
- console.error('Failed to load RDF view:', e);
75
- }
76
- }
77
- }
78
-
79
- const hasDesignations = computed(() => {
80
- return Object.keys(props.concept?.designations || {}).length > 0;
81
- });
82
-
83
- const parsedDesignations = computed(() => {
84
- // Convert the content collection format to what DesignationList expects
85
- const out: any[] = [];
86
- for (const [lang, text] of Object.entries(props.concept?.designations || {})) {
87
- out.push({ designation: text, type: 'expression', normative_status: 'preferred', language_code: lang });
88
- }
89
- return out;
90
- });
91
-
92
- const parsedConcept = computed(() => {
93
- // Create a minimal Concept-like object for ConceptRdfView
94
- return {
95
- id: props.concept?.conceptId,
96
- uri: props.conceptUri,
97
- status: props.concept?.status,
98
- localizations: {},
99
- ...props.concept,
100
- };
101
- });
102
- </script>