@glossarist/concept-browser 0.4.15 → 0.4.18
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -108,8 +108,22 @@ describe('ConceptCard', () => {
|
|
|
108
108
|
expect(router.currentRoute.value.params.conceptId).toBe('3.1.1.1');
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it('shows language count from
|
|
111
|
+
it('shows language count from designations', () => {
|
|
112
112
|
const wrapper = mountCard();
|
|
113
|
-
expect(wrapper.text()).toContain('
|
|
113
|
+
expect(wrapper.text()).toContain('1 lang');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('shows selected language designation as title', () => {
|
|
117
|
+
const wrapper = mountCard(makeEntry({
|
|
118
|
+
designations: { eng: 'test term', fra: 'terme test' },
|
|
119
|
+
eng: 'test term',
|
|
120
|
+
}), 'test');
|
|
121
|
+
expect(wrapper.text()).toContain('test term');
|
|
122
|
+
|
|
123
|
+
const wrapperFra = mount(ConceptCard, {
|
|
124
|
+
global: { plugins: [pinia, router] },
|
|
125
|
+
props: { entry: makeEntry({ designations: { eng: 'test term', fra: 'terme test' }, eng: 'test term' }), registerId: 'test', displayLang: 'fra' },
|
|
126
|
+
});
|
|
127
|
+
expect(wrapperFra.text()).toContain('terme test');
|
|
114
128
|
});
|
|
115
129
|
});
|
|
@@ -238,6 +238,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
const related = (doc['gl:references'] ?? []).map(mapRelatedFromJsonLd);
|
|
241
|
+
const tags = Array.isArray(doc['gl:tags']) ? [...doc['gl:tags']] : [];
|
|
241
242
|
|
|
242
243
|
return Concept.fromJSON({
|
|
243
244
|
id,
|
|
@@ -245,6 +246,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
|
|
|
245
246
|
uri: doc['@id'] ?? null,
|
|
246
247
|
localizations,
|
|
247
248
|
related,
|
|
249
|
+
tags,
|
|
248
250
|
status: null,
|
|
249
251
|
});
|
|
250
252
|
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
8
8
|
const props = defineProps<{
|
|
9
9
|
entry: ConceptSummary;
|
|
10
10
|
registerId: string;
|
|
11
|
+
displayLang?: string | null;
|
|
11
12
|
}>();
|
|
12
13
|
|
|
13
14
|
const router = useRouter();
|
|
@@ -29,6 +30,17 @@ function statusColor(status: string): string {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.languages ?? []);
|
|
33
|
+
|
|
34
|
+
const displayTitle = computed(() => {
|
|
35
|
+
if (props.displayLang && props.entry.designations?.[props.displayLang]) {
|
|
36
|
+
return props.entry.designations[props.displayLang];
|
|
37
|
+
}
|
|
38
|
+
return props.entry.eng || props.entry.id;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const langCount = computed(() => {
|
|
42
|
+
return Object.keys(props.entry.designations ?? {}).length;
|
|
43
|
+
});
|
|
32
44
|
</script>
|
|
33
45
|
|
|
34
46
|
<template>
|
|
@@ -41,7 +53,7 @@ const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.
|
|
|
41
53
|
<div class="flex items-start justify-between gap-2">
|
|
42
54
|
<div class="min-w-0">
|
|
43
55
|
<h3 class="font-medium text-ink-800 truncate group-hover:text-ink-900 transition-colors leading-snug text-[15px]">
|
|
44
|
-
{{
|
|
56
|
+
{{ displayTitle }}
|
|
45
57
|
</h3>
|
|
46
58
|
<p class="text-[11px] text-ink-300 mt-1 font-mono tabular-nums">{{ entry.id }}</p>
|
|
47
59
|
</div>
|
|
@@ -53,14 +65,14 @@ const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.
|
|
|
53
65
|
</span>
|
|
54
66
|
</div>
|
|
55
67
|
<!-- Language coverage -->
|
|
56
|
-
<div class="flex items-center gap-1.5 mt-2.5"
|
|
57
|
-
<span class="text-[11px] text-ink-300">{{
|
|
68
|
+
<div class="flex items-center gap-1.5 mt-2.5">
|
|
69
|
+
<span class="text-[11px] text-ink-300">{{ langCount }} lang</span>
|
|
58
70
|
<div class="flex gap-0.5">
|
|
59
71
|
<span
|
|
60
72
|
v-for="lang in manifestLanguages"
|
|
61
73
|
:key="lang"
|
|
62
74
|
class="w-1.5 h-1.5 rounded-full"
|
|
63
|
-
:style="{ backgroundColor: getColor(registerId) + '
|
|
75
|
+
:style="{ backgroundColor: (lang in (entry.designations ?? {})) ? getColor(registerId) : getColor(registerId) + '20' }"
|
|
64
76
|
:aria-label="lang"
|
|
65
77
|
></span>
|
|
66
78
|
</div>
|
|
@@ -83,6 +83,9 @@ const conceptDates = computed(() => props.concept.dates);
|
|
|
83
83
|
// Managed concept sources (distinct from localized sources)
|
|
84
84
|
const conceptSources = computed(() => props.concept.sources);
|
|
85
85
|
|
|
86
|
+
// Managed concept tags
|
|
87
|
+
const conceptTags = computed(() => props.concept.tags ?? []);
|
|
88
|
+
|
|
86
89
|
// Cross-reference resolver: generates clickable links for inline refs
|
|
87
90
|
|
|
88
91
|
const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
|
|
@@ -683,6 +686,14 @@ const nonVerbalReps = computed(() => {
|
|
|
683
686
|
</div>
|
|
684
687
|
</div>
|
|
685
688
|
|
|
689
|
+
<!-- Tags -->
|
|
690
|
+
<div v-if="conceptTags.length" class="card p-5">
|
|
691
|
+
<div class="section-label">Tags</div>
|
|
692
|
+
<div class="flex flex-wrap gap-1.5 mt-3">
|
|
693
|
+
<span v-for="tag in conceptTags" :key="tag" class="badge badge-gray text-[10px]">{{ tag }}</span>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
686
697
|
<!-- Managed concept dates -->
|
|
687
698
|
<div v-if="conceptDates.length" class="card p-5">
|
|
688
699
|
<div class="section-label">Lifecycle dates</div>
|
|
@@ -4,6 +4,7 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
4
4
|
import { useDsStyle } from '../utils/dataset-style';
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
6
|
import { FORMAT_LABELS } from '../config/types';
|
|
7
|
+
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
7
8
|
import ConceptCard from '../components/ConceptCard.vue';
|
|
8
9
|
|
|
9
10
|
const props = defineProps<{ registerId: string }>();
|
|
@@ -38,6 +39,26 @@ const totalConceptCount = computed(() => adapter.value?.getConceptCount() ?? 0);
|
|
|
38
39
|
const filter = ref('');
|
|
39
40
|
const filterInput = ref<HTMLInputElement | null>(null);
|
|
40
41
|
const allChunksLoaded = ref(false);
|
|
42
|
+
const selectedLang = ref<string | null>(null);
|
|
43
|
+
|
|
44
|
+
interface LangOption {
|
|
45
|
+
code: string;
|
|
46
|
+
name: string;
|
|
47
|
+
label: string;
|
|
48
|
+
termCount: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const languageOptions = computed<LangOption[]>(() => {
|
|
52
|
+
const m = manifest.value;
|
|
53
|
+
if (!m) return [];
|
|
54
|
+
const sorted = sortLanguages(m.languages, m.languageOrder);
|
|
55
|
+
return sorted.map(code => ({
|
|
56
|
+
code,
|
|
57
|
+
name: langName(code),
|
|
58
|
+
label: langLabel(code),
|
|
59
|
+
termCount: m.languageStats?.[code]?.terms ?? 0,
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
41
62
|
|
|
42
63
|
function onGlobalKeydown(e: KeyboardEvent) {
|
|
43
64
|
if (e.key === '/' && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
|
|
@@ -65,6 +86,17 @@ watch(filter, async (q) => {
|
|
|
65
86
|
}
|
|
66
87
|
});
|
|
67
88
|
|
|
89
|
+
// When language filter changes, reset page and load all chunks
|
|
90
|
+
watch(selectedLang, async (lang) => {
|
|
91
|
+
page.value = 1;
|
|
92
|
+
if (lang && !allChunksLoaded.value && adapter.value) {
|
|
93
|
+
chunkLoading.value = true;
|
|
94
|
+
await adapter.value.ensureAllChunksLoaded();
|
|
95
|
+
allChunksLoaded.value = true;
|
|
96
|
+
chunkLoading.value = false;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
68
100
|
// Dense array: only loaded (non-undefined) entries
|
|
69
101
|
const loadedConcepts = computed(() => {
|
|
70
102
|
const arr = adapter.value?.getConcepts();
|
|
@@ -74,8 +106,10 @@ const loadedConcepts = computed(() => {
|
|
|
74
106
|
|
|
75
107
|
const filtered = computed(() => {
|
|
76
108
|
const q = filter.value.trim().toLowerCase();
|
|
77
|
-
|
|
109
|
+
const lang = selectedLang.value;
|
|
78
110
|
return loadedConcepts.value.filter(c => {
|
|
111
|
+
if (lang && !(lang in (c.designations ?? {}))) return false;
|
|
112
|
+
if (!q) return true;
|
|
79
113
|
return (c.eng || '').toLowerCase().includes(q) || c.id.toLowerCase().includes(q);
|
|
80
114
|
});
|
|
81
115
|
});
|
|
@@ -91,8 +125,8 @@ const pageLoaded = computed(() => {
|
|
|
91
125
|
});
|
|
92
126
|
|
|
93
127
|
const paged = computed(() => {
|
|
94
|
-
// When filtering, paginate over filtered dense results (all chunks loaded)
|
|
95
|
-
if (filter.value.trim()) {
|
|
128
|
+
// When filtering (text or language), paginate over filtered dense results (all chunks loaded)
|
|
129
|
+
if (filter.value.trim() || selectedLang.value) {
|
|
96
130
|
const start = (page.value - 1) * perPage;
|
|
97
131
|
return filtered.value.slice(start, start + perPage);
|
|
98
132
|
}
|
|
@@ -104,7 +138,7 @@ const paged = computed(() => {
|
|
|
104
138
|
});
|
|
105
139
|
|
|
106
140
|
const totalPages = computed(() => {
|
|
107
|
-
if (filter.value.trim()) {
|
|
141
|
+
if (filter.value.trim() || selectedLang.value) {
|
|
108
142
|
return Math.max(1, Math.ceil(filtered.value.length / perPage));
|
|
109
143
|
}
|
|
110
144
|
return Math.max(1, Math.ceil(totalConceptCount.value / perPage));
|
|
@@ -112,7 +146,7 @@ const totalPages = computed(() => {
|
|
|
112
146
|
|
|
113
147
|
// Load chunks needed for current page
|
|
114
148
|
watch(page, async () => {
|
|
115
|
-
if (!adapter.value || filter.value.trim()) return;
|
|
149
|
+
if (!adapter.value || filter.value.trim() || selectedLang.value) return;
|
|
116
150
|
const start = (page.value - 1) * perPage;
|
|
117
151
|
if (!adapter.value.isRangeLoaded(start, perPage)) {
|
|
118
152
|
chunkLoading.value = true;
|
|
@@ -229,7 +263,10 @@ function goToPage(p: number) {
|
|
|
229
263
|
</svg>
|
|
230
264
|
</div>
|
|
231
265
|
<span class="text-sm text-ink-400">
|
|
232
|
-
<template v-if="
|
|
266
|
+
<template v-if="selectedLang">
|
|
267
|
+
{{ filtered.length.toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts in {{ langName(selectedLang) }}
|
|
268
|
+
</template>
|
|
269
|
+
<template v-else-if="filter.trim()">
|
|
233
270
|
{{ filtered.length.toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts
|
|
234
271
|
</template>
|
|
235
272
|
<template v-else-if="totalPages > 1">
|
|
@@ -241,6 +278,39 @@ function goToPage(p: number) {
|
|
|
241
278
|
</span>
|
|
242
279
|
</div>
|
|
243
280
|
|
|
281
|
+
<!-- Language filter -->
|
|
282
|
+
<div v-if="languageOptions.length > 1" class="flex flex-wrap gap-1.5 mb-5">
|
|
283
|
+
<button
|
|
284
|
+
@click="selectedLang = null"
|
|
285
|
+
:class="[
|
|
286
|
+
selectedLang === null
|
|
287
|
+
? 'bg-ink-800 text-white'
|
|
288
|
+
: 'bg-surface-raised text-ink-600 hover:bg-ink-50 border border-ink-100'
|
|
289
|
+
]"
|
|
290
|
+
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
|
291
|
+
>
|
|
292
|
+
All {{ totalConceptCount.toLocaleString() }}
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
v-for="lang in languageOptions"
|
|
296
|
+
:key="lang.code"
|
|
297
|
+
@click="selectedLang = selectedLang === lang.code ? null : lang.code"
|
|
298
|
+
:class="[
|
|
299
|
+
selectedLang === lang.code
|
|
300
|
+
? 'bg-ink-800 text-white'
|
|
301
|
+
: 'bg-surface-raised text-ink-600 hover:bg-ink-50 border border-ink-100'
|
|
302
|
+
]"
|
|
303
|
+
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5"
|
|
304
|
+
>
|
|
305
|
+
<span
|
|
306
|
+
class="text-[10px] font-semibold px-1.5 py-0.5 rounded"
|
|
307
|
+
:class="selectedLang === lang.code ? 'bg-ink-700 text-ink-200' : 'bg-ink-50 text-ink-500'"
|
|
308
|
+
>{{ lang.label }}</span>
|
|
309
|
+
{{ lang.name }}
|
|
310
|
+
<span class="text-[10px] opacity-60">{{ lang.termCount }}</span>
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
244
314
|
<!-- Chunk loading skeleton -->
|
|
245
315
|
<div v-if="chunkLoading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
246
316
|
<div v-for="i in 6" :key="i" class="skeleton h-20"></div>
|
|
@@ -253,6 +323,7 @@ function goToPage(p: number) {
|
|
|
253
323
|
:key="entry.id"
|
|
254
324
|
:entry="entry"
|
|
255
325
|
:register-id="registerId"
|
|
326
|
+
:display-lang="selectedLang"
|
|
256
327
|
class="animate-entrance"
|
|
257
328
|
:style="{ animationDelay: `${Math.min(idx, 20) * 30}ms` }"
|
|
258
329
|
/>
|