@glossarist/concept-browser 0.7.11 → 0.7.13
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 +33 -0
- package/package.json +1 -1
- package/scripts/generate-data.mjs +1 -0
- package/src/components/AppSidebar.vue +135 -3
- package/src/config/types.ts +12 -0
- package/src/config/use-site-config.ts +3 -1
package/README.md
CHANGED
|
@@ -231,6 +231,39 @@ pages:
|
|
|
231
231
|
source: about-fra.md
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
+
### Dataset groups
|
|
235
|
+
|
|
236
|
+
When a site has many datasets, you can group them in the sidebar navigation. Datasets within a group are displayed under a collapsible header.
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
datasetGroups:
|
|
240
|
+
- id: viml
|
|
241
|
+
label: "VIML — International Vocabulary of Legal Metrology"
|
|
242
|
+
color: "#004996"
|
|
243
|
+
datasets: [viml-2022, viml-2013, viml-2000, viml-1968]
|
|
244
|
+
translations:
|
|
245
|
+
fra:
|
|
246
|
+
label: "VIML — Vocabulaire international de métrologie légale"
|
|
247
|
+
|
|
248
|
+
- id: vim
|
|
249
|
+
label: "VIM — International Vocabulary of Metrology"
|
|
250
|
+
color: "#005A9C"
|
|
251
|
+
datasets: [vim-2012, vim-2010, vim-2007, vim-1993]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Dataset group field reference
|
|
255
|
+
|
|
256
|
+
| Field | Required | Description |
|
|
257
|
+
|-------|----------|-------------|
|
|
258
|
+
| `id` | yes | Unique identifier for the group |
|
|
259
|
+
| `label` | yes | Display name shown as the collapsible group header |
|
|
260
|
+
| `description` | no | Short description of the group |
|
|
261
|
+
| `color` | no | Hex color for the group header text |
|
|
262
|
+
| `datasets` | yes | Ordered array of dataset IDs belonging to this group |
|
|
263
|
+
| `translations` | no | Localized label and description per language |
|
|
264
|
+
|
|
265
|
+
Datasets not assigned to any group appear at the bottom of the dataset list. If `datasetGroups` is omitted, all datasets are displayed as a flat list (the default behavior).
|
|
266
|
+
|
|
234
267
|
### Cross-reference mapping
|
|
235
268
|
|
|
236
269
|
```yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
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": {
|
|
@@ -1170,6 +1170,7 @@ writeJson(path.join(PUBLIC, 'site-config.json'), {
|
|
|
1170
1170
|
description: config.description,
|
|
1171
1171
|
translations: config.translations || undefined,
|
|
1172
1172
|
datasets: config.datasets.map(d => d.id),
|
|
1173
|
+
datasetGroups: config.datasetGroups || undefined,
|
|
1173
1174
|
datasetTranslations: Object.keys(datasetTranslations).length ? datasetTranslations : undefined,
|
|
1174
1175
|
defaultDataset: config.datasets.length === 1 ? config.datasets[0].id : undefined,
|
|
1175
1176
|
uiLanguages: config.uiLanguages || undefined,
|
|
@@ -5,16 +5,17 @@ import { useUiStore } from '../stores/ui';
|
|
|
5
5
|
import { useRoute, useRouter } from 'vue-router';
|
|
6
6
|
import { useDsStyle } from '../utils/dataset-style';
|
|
7
7
|
import { useSiteConfig } from '../config/use-site-config';
|
|
8
|
+
import type { DatasetGroup } from '../config/types';
|
|
8
9
|
import { useOntologyNav, compactToSlug } from '../composables/use-ontology-nav';
|
|
9
10
|
import NavIcon from './NavIcon.vue';
|
|
10
|
-
import { useI18n } from '../i18n';
|
|
11
|
+
import { useI18n, locale } from '../i18n';
|
|
11
12
|
|
|
12
13
|
const store = useVocabularyStore();
|
|
13
14
|
const ui = useUiStore();
|
|
14
15
|
const router = useRouter();
|
|
15
16
|
const route = useRoute();
|
|
16
17
|
const { getColor } = useDsStyle();
|
|
17
|
-
const { globalPages, datasetPages, config: siteConfig, localizedTitle, localizedDatasetField } = useSiteConfig();
|
|
18
|
+
const { globalPages, datasetPages, config: siteConfig, localizedTitle, localizedDatasetField, datasetGroups } = useSiteConfig();
|
|
18
19
|
const { t } = useI18n();
|
|
19
20
|
|
|
20
21
|
const currentDataset = computed(() => route.params.registerId as string ?? '');
|
|
@@ -85,6 +86,60 @@ const datasetEntries = computed(() => {
|
|
|
85
86
|
|
|
86
87
|
const datasetIds = computed(() => new Set(datasetEntries.value.map(d => d.id)));
|
|
87
88
|
|
|
89
|
+
const hasGroups = computed(() => (datasetGroups.value?.length ?? 0) > 0);
|
|
90
|
+
|
|
91
|
+
interface SidebarGroup {
|
|
92
|
+
id: string;
|
|
93
|
+
label: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
color?: string;
|
|
96
|
+
entries: { id: string; title: string; loaded: boolean; conceptCount: number }[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const groupedDatasetEntries = computed<SidebarGroup[]>(() => {
|
|
100
|
+
const groups = datasetGroups.value;
|
|
101
|
+
if (!groups?.length) return [];
|
|
102
|
+
|
|
103
|
+
const entryMap = new Map(datasetEntries.value.map(e => [e.id, e]));
|
|
104
|
+
const assigned = new Set<string>();
|
|
105
|
+
const result: SidebarGroup[] = [];
|
|
106
|
+
|
|
107
|
+
for (const g of groups) {
|
|
108
|
+
const entries = g.datasets
|
|
109
|
+
.map(id => entryMap.get(id))
|
|
110
|
+
.filter((e): e is typeof entryMap extends Map<string, infer V> ? V : never => !!e);
|
|
111
|
+
for (const e of entries) assigned.add(e.id);
|
|
112
|
+
const trLabel = g.translations?.[locale.value]?.label;
|
|
113
|
+
result.push({
|
|
114
|
+
id: g.id,
|
|
115
|
+
label: trLabel || g.label,
|
|
116
|
+
description: g.description,
|
|
117
|
+
color: g.color,
|
|
118
|
+
entries,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ungrouped = datasetEntries.value.filter(e => !assigned.has(e.id));
|
|
123
|
+
if (ungrouped.length) {
|
|
124
|
+
result.push({ id: '__ungrouped__', label: '', entries: ungrouped });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const collapsedGroups = ref<Set<string>>(new Set());
|
|
131
|
+
|
|
132
|
+
function toggleGroup(groupId: string) {
|
|
133
|
+
const s = new Set(collapsedGroups.value);
|
|
134
|
+
if (s.has(groupId)) s.delete(groupId);
|
|
135
|
+
else s.add(groupId);
|
|
136
|
+
collapsedGroups.value = s;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isGroupExpanded(groupId: string): boolean {
|
|
140
|
+
return !collapsedGroups.value.has(groupId);
|
|
141
|
+
}
|
|
142
|
+
|
|
88
143
|
// Hide dataset-prefixed pages (e.g. "viml-about") from global nav
|
|
89
144
|
const filteredGlobalPages = computed(() =>
|
|
90
145
|
globalPages.value.filter(p => {
|
|
@@ -473,7 +528,84 @@ function navTitle(page: { route: string }): string {
|
|
|
473
528
|
|
|
474
529
|
<!-- Datasets -->
|
|
475
530
|
<div class="section-label">{{ t('nav.datasets') }}</div>
|
|
476
|
-
|
|
531
|
+
|
|
532
|
+
<!-- Grouped datasets -->
|
|
533
|
+
<template v-if="hasGroups">
|
|
534
|
+
<div v-for="group in groupedDatasetEntries" :key="group.id" class="mb-2">
|
|
535
|
+
<!-- Group header (skip for ungrouped) -->
|
|
536
|
+
<button
|
|
537
|
+
v-if="group.label"
|
|
538
|
+
@click="toggleGroup(group.id)"
|
|
539
|
+
class="w-full flex items-start gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold transition-colors hover:bg-ink-50"
|
|
540
|
+
:style="group.color ? { color: group.color } : {}"
|
|
541
|
+
>
|
|
542
|
+
<span class="w-3 text-[10px] mt-0.5 flex-shrink-0">{{ isGroupExpanded(group.id) ? '▾' : '▸' }}</span>
|
|
543
|
+
<span class="flex-1 text-left leading-snug">{{ group.label }}</span>
|
|
544
|
+
</button>
|
|
545
|
+
|
|
546
|
+
<!-- Group entries -->
|
|
547
|
+
<div v-if="isGroupExpanded(group.id)" class="space-y-1" :class="group.label ? 'ml-1' : ''">
|
|
548
|
+
<div
|
|
549
|
+
v-for="ds in group.entries"
|
|
550
|
+
:key="ds.id"
|
|
551
|
+
class="rounded-lg transition-all duration-150"
|
|
552
|
+
:class="currentDataset === ds.id ? 'bg-surface' : ''"
|
|
553
|
+
>
|
|
554
|
+
<button
|
|
555
|
+
@click="goToDataset(ds.id)"
|
|
556
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm border-l-2"
|
|
557
|
+
:class="[
|
|
558
|
+
currentDataset === ds.id
|
|
559
|
+
? 'text-ink-800'
|
|
560
|
+
: 'border-transparent text-ink-600 hover:bg-ink-50 hover:text-ink-800'
|
|
561
|
+
]"
|
|
562
|
+
:style="currentDataset === ds.id ? { borderLeftColor: getColor(ds.id), borderLeftWidth: '2px' } : {}"
|
|
563
|
+
>
|
|
564
|
+
<div class="font-medium truncate leading-snug">{{ localizedDatasetField(ds.id, 'title', ds.title) }}</div>
|
|
565
|
+
<div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400' : 'text-ink-300'">
|
|
566
|
+
{{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
|
|
567
|
+
</div>
|
|
568
|
+
</button>
|
|
569
|
+
|
|
570
|
+
<!-- Expanded dataset: sub-pages + provenance -->
|
|
571
|
+
<div v-if="currentDataset === ds.id && (filteredDatasetPages.length || provenance.owner)" class="px-2 pb-2">
|
|
572
|
+
<nav v-if="filteredDatasetPages.length" class="space-y-0.5 mt-1">
|
|
573
|
+
<router-link
|
|
574
|
+
v-for="page in filteredDatasetPages"
|
|
575
|
+
:key="page.route || 'concepts'"
|
|
576
|
+
:to="pageRoute(page)"
|
|
577
|
+
class="btn-ghost w-full text-left flex items-center gap-2 text-sm"
|
|
578
|
+
:class="isActive(page) ? 'active' : ''"
|
|
579
|
+
@click="closeMobile"
|
|
580
|
+
>
|
|
581
|
+
<NavIcon :name="page.icon" />
|
|
582
|
+
{{ navTitle(page) }}
|
|
583
|
+
</router-link>
|
|
584
|
+
</nav>
|
|
585
|
+
|
|
586
|
+
<div v-if="provenance.owner" class="mt-3 pt-3 border-t border-ink-100/60">
|
|
587
|
+
<div class="text-[11px] text-ink-300 space-y-1.5 px-1">
|
|
588
|
+
<div v-if="provenance.ref" class="text-xs font-semibold text-ink-700">
|
|
589
|
+
{{ provenance.ref }}
|
|
590
|
+
</div>
|
|
591
|
+
<div class="flex items-center gap-1">
|
|
592
|
+
<span class="text-ink-400">{{ t('sidebar.publishedBy') }}</span>
|
|
593
|
+
<a v-if="provenance.ownerUrl" :href="provenance.ownerUrl" target="_blank" rel="noopener" class="concept-link font-medium">{{ provenance.owner }}</a>
|
|
594
|
+
<span v-else class="text-ink-600 font-medium">{{ provenance.owner }}</span>
|
|
595
|
+
</div>
|
|
596
|
+
<div v-if="provenance.sourceRepo">
|
|
597
|
+
<a :href="provenance.sourceRepo" target="_blank" rel="noopener" class="concept-link">{{ t('sidebar.viewSource') }}</a>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</template>
|
|
606
|
+
|
|
607
|
+
<!-- Flat dataset list (fallback when no groups) -->
|
|
608
|
+
<nav v-else class="space-y-1">
|
|
477
609
|
<div
|
|
478
610
|
v-for="ds in datasetEntries"
|
|
479
611
|
:key="ds.id"
|
package/src/config/types.ts
CHANGED
|
@@ -140,6 +140,17 @@ export interface PageConfig {
|
|
|
140
140
|
datasetScoped?: boolean;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// === Dataset Groups ===
|
|
144
|
+
|
|
145
|
+
export interface DatasetGroup {
|
|
146
|
+
id: string;
|
|
147
|
+
label: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
color?: string;
|
|
150
|
+
datasets: string[];
|
|
151
|
+
translations?: Record<string, { label?: string; description?: string }>;
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
// === Site Config ===
|
|
144
155
|
|
|
145
156
|
export interface SiteConfig {
|
|
@@ -152,6 +163,7 @@ export interface SiteConfig {
|
|
|
152
163
|
description?: string;
|
|
153
164
|
translations?: Record<string, { title?: string; subtitle?: string; description?: string }>;
|
|
154
165
|
datasets: DatasetConfig[];
|
|
166
|
+
datasetGroups?: DatasetGroup[];
|
|
155
167
|
routing: RoutingEntry[];
|
|
156
168
|
branding: SiteBranding;
|
|
157
169
|
analytics?: AnalyticsConfig;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref, computed } from 'vue';
|
|
2
2
|
import type { PageConfig } from './types';
|
|
3
|
+
import type { DatasetGroup } from './types';
|
|
3
4
|
import { locale } from '../i18n';
|
|
4
5
|
|
|
5
6
|
export interface RuntimeSiteConfig {
|
|
@@ -12,6 +13,7 @@ export interface RuntimeSiteConfig {
|
|
|
12
13
|
translations?: Record<string, { title?: string; subtitle?: string; description?: string }>;
|
|
13
14
|
datasetTranslations?: Record<string, Record<string, { title?: string; description?: string }>>;
|
|
14
15
|
datasets: string[];
|
|
16
|
+
datasetGroups?: DatasetGroup[];
|
|
15
17
|
defaultDataset?: string;
|
|
16
18
|
uiLanguages?: { code: string; label: string }[];
|
|
17
19
|
branding?: {
|
|
@@ -182,5 +184,5 @@ export function useSiteConfig() {
|
|
|
182
184
|
synthesizeDatasetPages(siteConfig.value?.features, siteConfig.value?.pages),
|
|
183
185
|
);
|
|
184
186
|
|
|
185
|
-
return { config, visibleDatasets, localizedTitle, localizedSubtitle, localizedDescription, localizedDatasetField, loadConfig, globalPages, datasetPages };
|
|
187
|
+
return { config, visibleDatasets, localizedTitle, localizedSubtitle, localizedDescription, localizedDatasetField, loadConfig, globalPages, datasetPages, datasetGroups: computed(() => siteConfig.value?.datasetGroups) };
|
|
186
188
|
}
|