@glossarist/concept-browser 0.7.68 → 0.7.71

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 (60) hide show
  1. package/cli/index.mjs +28 -9
  2. package/package.json +14 -8
  3. package/public/favicon.svg +1 -4
  4. package/scripts/bridge-to-astro.mjs +155 -0
  5. package/scripts/generate-data.mjs +13 -0
  6. package/scripts/load-site-config.mjs +73 -1
  7. package/src/__tests__/tc204-deployment.test.ts +21 -0
  8. package/src/adapters/DatasetAdapter.ts +1 -0
  9. package/src/adapters/model-bridge.ts +50 -12
  10. package/src/adapters/non-verbal/glossarist-augment.d.ts +5 -1
  11. package/src/adapters/types.ts +1 -0
  12. package/src/assets/glossarist-logo-full.svg +1 -0
  13. package/src/astro-islands/app.ts +3 -0
  14. package/src/components/AppFooter.vue +3 -2
  15. package/src/components/AppHeader.vue +4 -7
  16. package/src/components/AppSidebar.vue +8 -5
  17. package/src/components/ConceptDetail.vue +12 -0
  18. package/src/components/DesignationList.vue +15 -4
  19. package/src/components/LanguageDetail.vue +6 -0
  20. package/src/components/RelationSphere.vue +36 -32
  21. package/src/components/site/SeoMeta.astro +23 -0
  22. package/src/components/site/SiteFooter.astro +14 -0
  23. package/src/components/site/SiteHeader.astro +25 -0
  24. package/src/components/site/SiteSidebar.astro +56 -0
  25. package/src/content.config.ts +85 -0
  26. package/src/i18n/locales/eng.yml +1 -0
  27. package/src/i18n/locales/fra.yml +1 -0
  28. package/src/islands/AppIsland.vue +7 -0
  29. package/src/islands/BibliographyIsland.vue +26 -0
  30. package/src/islands/ConceptDetailIsland.vue +27 -0
  31. package/src/islands/ConceptDiffIsland.vue +214 -0
  32. package/src/islands/ConceptIsland.vue +102 -0
  33. package/src/islands/GraphIsland.vue +15 -0
  34. package/src/islands/GroupRendererIsland.vue +15 -0
  35. package/src/islands/HeaderIsland.vue +10 -0
  36. package/src/islands/NonVerbalIsland.vue +75 -0
  37. package/src/islands/RelationSphereIsland.vue +20 -0
  38. package/src/islands/SearchIsland.vue +23 -0
  39. package/src/islands/SeriesCardIsland.vue +12 -0
  40. package/src/islands/ShortcutsIsland.vue +13 -0
  41. package/src/islands/SidebarIsland.vue +15 -0
  42. package/src/islands/SupersessionDiffIsland.vue +267 -0
  43. package/src/islands/app-entry.ts +8 -0
  44. package/src/islands/app.ts +4 -0
  45. package/src/layouts/Default.astro +70 -0
  46. package/src/lib/site-config.ts +26 -0
  47. package/src/pages/404.astro +9 -0
  48. package/src/pages/[...path].astro +29 -0
  49. package/src/pages/dataset/[id]/concept/[cid].astro +23 -0
  50. package/src/pages/dataset/[id]/index.astro +16 -0
  51. package/src/pages/graph.astro +5 -0
  52. package/src/pages/group/[id]/index.astro +16 -0
  53. package/src/pages/index.astro +10 -0
  54. package/src/pages/search.astro +5 -0
  55. package/src/styles/tailwind.css +503 -0
  56. package/src/views/ConceptView.vue +18 -23
  57. package/src/views/StatsView.vue +1 -1
  58. package/tsconfig.json +2 -1
  59. package/vite.config.ts +3 -1
  60. package/postcss.config.js +0 -13
@@ -0,0 +1,214 @@
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>
@@ -0,0 +1,102 @@
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>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <ErrorBoundary title="Graph view failed" retry-key="graph">
3
+ <GraphPanel :nodes="nodes" :edges="edges" />
4
+ </ErrorBoundary>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import GraphPanel from '../components/GraphPanel.vue';
9
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
10
+
11
+ defineProps<{
12
+ nodes: Array<{ id: string; label: string; group?: string }>;
13
+ edges: Array<{ source: string; target: string; type?: string }>;
14
+ }>();
15
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <ErrorBoundary title="Group renderer failed">
3
+ <DatasetGroupRenderer :group="group" :datasets="datasets" />
4
+ </ErrorBoundary>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import DatasetGroupRenderer from '../components/DatasetGroupRenderer.vue';
9
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
10
+
11
+ defineProps<{
12
+ group: any;
13
+ datasets: any[];
14
+ }>();
15
+ </script>
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <ErrorBoundary title="Header failed">
3
+ <AppHeader />
4
+ </ErrorBoundary>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import AppHeader from '../components/AppHeader.vue';
9
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
10
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div class="nvr-island">
3
+ <section v-if="figures.length" class="mb-6">
4
+ <h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">Figures</h2>
5
+ <div class="space-y-4">
6
+ <figure v-for="(fig, i) in figures" :key="i" class="card p-4">
7
+ <figcaption class="text-sm text-ink-600 dark:text-ink-300 mb-2" dir="auto">{{ fig.caption || `Figure ${i + 1}` }}</figcaption>
8
+ <div v-if="fig.images?.length" class="grid grid-cols-2 gap-2">
9
+ <img
10
+ v-for="(img, j) in fig.images"
11
+ :key="j"
12
+ :src="`/data/${registerId}/images/${img.src}`"
13
+ :alt="fig.altText || fig.caption || `Figure ${i + 1}`"
14
+ class="rounded border border-ink-200 dark:border-ink-700 max-w-full"
15
+ loading="lazy"
16
+ />
17
+ </div>
18
+ <p v-if="fig.description" class="text-xs text-ink-400 mt-2" dir="auto">{{ fig.description }}</p>
19
+ </figure>
20
+ </div>
21
+ </section>
22
+
23
+ <section v-if="tables.length" class="mb-6">
24
+ <h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">Tables</h2>
25
+ <div class="space-y-4">
26
+ <div v-for="(tbl, i) in tables" :key="i" class="card p-4">
27
+ <p class="text-sm text-ink-600 dark:text-ink-300 mb-2" dir="auto">{{ tbl.caption || `Table ${i + 1}` }}</p>
28
+ <div v-if="tbl.rows?.length" class="overflow-x-auto">
29
+ <table class="w-full text-sm">
30
+ <tbody>
31
+ <tr v-for="(row, r) in tbl.rows" :key="r" class="border-b border-ink-100 dark:border-ink-700">
32
+ <td v-for="(cell, c) in row" :key="c" class="py-1 px-2 text-ink-600 dark:text-ink-300" dir="auto">{{ cell }}</td>
33
+ </tr>
34
+ </tbody>
35
+ </table>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </section>
40
+
41
+ <section v-if="formulas.length" class="mb-6">
42
+ <h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">Formulas</h2>
43
+ <div class="space-y-3">
44
+ <div v-for="(fml, i) in formulas" :key="i" class="card p-4">
45
+ <p v-if="fml.caption" class="text-sm text-ink-600 dark:text-ink-300 mb-2" dir="auto">{{ fml.caption }}</p>
46
+ <code class="text-sm font-mono text-ink-700 dark:text-ink-200 bg-ink-50 dark:bg-ink-800 px-2 py-1 rounded" dir="auto">{{ fml.content || fml.latex || fml.text || '' }}</code>
47
+ </div>
48
+ </div>
49
+ </section>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { computed } from 'vue';
55
+
56
+ const props = defineProps<{
57
+ nonVerbalReps?: Array<{
58
+ type?: string;
59
+ caption?: string;
60
+ description?: string;
61
+ images?: Array<{ src: string }>;
62
+ altText?: string;
63
+ rows?: string[][];
64
+ content?: string;
65
+ latex?: string;
66
+ text?: string;
67
+ }>;
68
+ registerId: string;
69
+ }>();
70
+
71
+ const all = computed(() => props.nonVerbalReps || []);
72
+ const figures = computed(() => all.value.filter(r => r.type === 'figure'));
73
+ const tables = computed(() => all.value.filter(r => r.type === 'table'));
74
+ const formulas = computed(() => all.value.filter(r => r.type === 'formula'));
75
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <ErrorBoundary title="Sphere view failed" :retry-key="conceptUri">
3
+ <RelationSphere
4
+ :concept="concept"
5
+ :edges="edges"
6
+ :concept-uri="conceptUri"
7
+ />
8
+ </ErrorBoundary>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import RelationSphere from '../components/RelationSphere.vue';
13
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
14
+
15
+ defineProps<{
16
+ concept: any;
17
+ conceptUri: string;
18
+ edges?: any[];
19
+ }>();
20
+ </script>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <ErrorBoundary title="Search failed" retry-key="search">
3
+ <div class="search-island">
4
+ <SearchBar />
5
+ <SearchResults />
6
+ </div>
7
+ </ErrorBoundary>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import SearchBar from '../components/SearchBar.vue';
12
+ import SearchResults from '../components/SearchResults.vue';
13
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
14
+
15
+ defineProps<{
16
+ concepts?: Array<{
17
+ conceptId: string;
18
+ registerId: string;
19
+ term: string;
20
+ status: string;
21
+ }>;
22
+ }>();
23
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <ErrorBoundary title="Series card failed">
3
+ <DatasetSeriesCard :register-id="registerId" />
4
+ </ErrorBoundary>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import DatasetSeriesCard from '../components/DatasetSeriesCard.vue';
9
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
10
+
11
+ defineProps<{ registerId: string }>();
12
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <ErrorBoundary title="Shortcuts failed">
3
+ <ShortcutsModal :open="open" @close="$emit('close')" />
4
+ </ErrorBoundary>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import ShortcutsModal from '../components/ShortcutsModal.vue';
9
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
10
+
11
+ defineProps<{ open: boolean }>();
12
+ defineEmits<{ close: [] }>();
13
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <ErrorBoundary title="Sidebar failed">
3
+ <AppSidebar />
4
+ </ErrorBoundary>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import AppSidebar from '../components/AppSidebar.vue';
9
+ import ErrorBoundary from '../components/ErrorBoundary.vue';
10
+
11
+ defineProps<{
12
+ activeDataset?: string;
13
+ activeGroup?: string;
14
+ }>();
15
+ </script>