@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.
@@ -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',
@@ -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('/datasets.json');
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';
@@ -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-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
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-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
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
- if (!q) return loadedConcepts.value;
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="filter.trim()">
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
  />
@@ -23,7 +23,7 @@ const activeLoading = ref(false);
23
23
 
24
24
  onMounted(async () => {
25
25
  try {
26
- const resp = await fetch('/news.json');
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-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
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>