@glossarist/concept-browser 0.5.0 → 0.5.1
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/README.md +32 -0
- package/cli/index.mjs +4 -0
- package/package.json +1 -1
- package/scripts/generate-data.mjs +2 -1
- package/scripts/generate-ontology-schema.mjs +312 -10
- package/src/__tests__/concept-card.test.ts +16 -2
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -0
- package/src/adapters/ontology-schema.ts +89 -4
- package/src/adapters/types.ts +1 -0
- package/src/components/AppSidebar.vue +281 -43
- package/src/components/ConceptCard.vue +16 -4
- package/src/components/ConceptDetail.vue +30 -26
- package/src/components/ConceptRdfView.vue +3 -3
- package/src/components/ConceptTimeline.vue +2 -14
- package/src/components/GraphPanel.vue +19 -0
- package/src/composables/use-ontology-nav.ts +183 -13
- package/src/composables/use-render-options.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/config/use-site-config.ts +2 -2
- package/src/data/ontology-schema.json +1721 -153
- package/src/router/index.ts +10 -0
- package/src/stores/vocabulary.ts +1 -1
- package/src/utils/lang.ts +13 -0
- package/src/views/AboutView.vue +1 -1
- package/src/views/ContributorsView.vue +1 -1
- package/src/views/DatasetView.vue +77 -6
- package/src/views/NewsView.vue +2 -2
- package/src/views/OntologySchemaView.vue +331 -14
- package/src/views/PageView.vue +4 -3
- package/src/views/ResolveView.vue +1 -1
- package/src/views/StatsView.vue +1 -1
package/src/router/index.ts
CHANGED
|
@@ -55,6 +55,16 @@ export const routes: RouteRecordRaw[] = [
|
|
|
55
55
|
name: 'ontology-taxonomy',
|
|
56
56
|
component: () => import('../views/OntologySchemaView.vue'),
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
path: '/ontology/shape/:shapeId',
|
|
60
|
+
name: 'ontology-shape',
|
|
61
|
+
component: () => import('../views/OntologySchemaView.vue'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: '/ontology/property/:propertyId',
|
|
65
|
+
name: 'ontology-property',
|
|
66
|
+
component: () => import('../views/OntologySchemaView.vue'),
|
|
67
|
+
},
|
|
58
68
|
{
|
|
59
69
|
path: '/news',
|
|
60
70
|
name: 'news',
|
package/src/stores/vocabulary.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
52
52
|
loading.value = true;
|
|
53
53
|
error.value = null;
|
|
54
54
|
try {
|
|
55
|
-
const adapters = await factory.discoverDatasets(
|
|
55
|
+
const adapters = await factory.discoverDatasets(`${import.meta.env.BASE_URL}datasets.json`);
|
|
56
56
|
for (const adapter of adapters) {
|
|
57
57
|
datasets.value.set(adapter.registerId, adapter);
|
|
58
58
|
if (adapter.manifest) {
|
package/src/utils/lang.ts
CHANGED
|
@@ -29,4 +29,17 @@ export function langLabel(code: string): string {
|
|
|
29
29
|
return code.toUpperCase();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const FALLBACK_LANG_ORDER = ['eng', 'fra'];
|
|
33
|
+
|
|
34
|
+
export function sortLanguages(languages: string[], order?: string[]): string[] {
|
|
35
|
+
const priority = order ?? FALLBACK_LANG_ORDER;
|
|
36
|
+
const index = new Map(priority.map((l, i) => [l, i]));
|
|
37
|
+
return [...languages].sort((a, b) => {
|
|
38
|
+
const ai = index.get(a) ?? priority.length;
|
|
39
|
+
const bi = index.get(b) ?? priority.length;
|
|
40
|
+
if (ai !== bi) return ai - bi;
|
|
41
|
+
return a.localeCompare(b);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export const DEFAULT_LANG = 'eng';
|
package/src/views/AboutView.vue
CHANGED
|
@@ -15,7 +15,7 @@ const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<div class="max-w-
|
|
18
|
+
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
19
19
|
<!-- Breadcrumb -->
|
|
20
20
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
21
21
|
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
|
|
@@ -14,7 +14,7 @@ const contributors = config.value?.contributors as Contributor[] | undefined;
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<div class="max-w-
|
|
17
|
+
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
18
18
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
19
19
|
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
|
|
20
20
|
<span class="text-ink-200">/</span>
|
|
@@ -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
|
/>
|
package/src/views/NewsView.vue
CHANGED
|
@@ -23,7 +23,7 @@ const activeLoading = ref(false);
|
|
|
23
23
|
|
|
24
24
|
onMounted(async () => {
|
|
25
25
|
try {
|
|
26
|
-
const resp = await fetch(
|
|
26
|
+
const resp = await fetch(`${import.meta.env.BASE_URL}news.json`);
|
|
27
27
|
if (resp.ok) posts.value = await resp.json();
|
|
28
28
|
} catch (e: any) {
|
|
29
29
|
error.value = e.message;
|
|
@@ -78,7 +78,7 @@ function formatDate(dateStr: string) {
|
|
|
78
78
|
</script>
|
|
79
79
|
|
|
80
80
|
<template>
|
|
81
|
-
<div class="max-w-
|
|
81
|
+
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
82
82
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
83
83
|
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
|
|
84
84
|
<span class="text-ink-200">/</span>
|