@glossarist/concept-browser 0.1.0

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 (68) hide show
  1. package/README.md +319 -0
  2. package/cli/index.mjs +119 -0
  3. package/env.d.ts +7 -0
  4. package/index.html +16 -0
  5. package/package.json +78 -0
  6. package/postcss.config.js +6 -0
  7. package/scripts/build-edges.js +112 -0
  8. package/scripts/fetch-datasets.mjs +195 -0
  9. package/scripts/generate-404.js +15 -0
  10. package/scripts/generate-data.mjs +606 -0
  11. package/scripts/load-site-config.mjs +56 -0
  12. package/src/App.vue +98 -0
  13. package/src/__tests__/data-integration.test.ts +135 -0
  14. package/src/__tests__/data-integrity.test.ts +101 -0
  15. package/src/__tests__/dataset-adapter.test.ts +336 -0
  16. package/src/__tests__/dataset-style.test.ts +37 -0
  17. package/src/__tests__/graph.test.ts +187 -0
  18. package/src/__tests__/lang.test.ts +29 -0
  19. package/src/__tests__/math.test.ts +113 -0
  20. package/src/__tests__/reference-resolver.test.ts +122 -0
  21. package/src/__tests__/site-config.test.ts +52 -0
  22. package/src/__tests__/uri-router.test.ts +76 -0
  23. package/src/adapters/DatasetAdapter.ts +270 -0
  24. package/src/adapters/ReferenceResolver.ts +95 -0
  25. package/src/adapters/UriRouter.ts +41 -0
  26. package/src/adapters/factory.ts +78 -0
  27. package/src/adapters/types.ts +162 -0
  28. package/src/components/AppHeader.vue +99 -0
  29. package/src/components/AppSidebar.vue +133 -0
  30. package/src/components/ConceptCard.vue +65 -0
  31. package/src/components/ConceptDetail.vue +540 -0
  32. package/src/components/ConceptTimeline.vue +410 -0
  33. package/src/components/FormatDownloads.vue +46 -0
  34. package/src/components/GraphPanel.vue +499 -0
  35. package/src/components/LanguageDetail.vue +211 -0
  36. package/src/components/NavIcon.vue +20 -0
  37. package/src/components/SearchBar.vue +241 -0
  38. package/src/composables/use-dataset-loader.ts +27 -0
  39. package/src/config/types.ts +130 -0
  40. package/src/config/use-site-config.ts +144 -0
  41. package/src/graph/GraphEngine.ts +137 -0
  42. package/src/graph/index.ts +1 -0
  43. package/src/main.ts +11 -0
  44. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  45. package/src/router/index.ts +43 -0
  46. package/src/router/page-routes.ts +35 -0
  47. package/src/stores/ui.ts +59 -0
  48. package/src/stores/vocabulary.ts +309 -0
  49. package/src/style.css +314 -0
  50. package/src/utils/asciidoc-lite.ts +123 -0
  51. package/src/utils/concept-formats.ts +157 -0
  52. package/src/utils/dataset-style.ts +54 -0
  53. package/src/utils/index.ts +1 -0
  54. package/src/utils/lang.ts +32 -0
  55. package/src/utils/math.ts +100 -0
  56. package/src/views/AboutView.vue +122 -0
  57. package/src/views/ConceptView.vue +119 -0
  58. package/src/views/ContributorsView.vue +110 -0
  59. package/src/views/DatasetView.vue +249 -0
  60. package/src/views/GraphView.vue +65 -0
  61. package/src/views/HomeView.vue +168 -0
  62. package/src/views/NewsView.vue +146 -0
  63. package/src/views/ResolveView.vue +63 -0
  64. package/src/views/SearchView.vue +33 -0
  65. package/src/views/StatsView.vue +121 -0
  66. package/tailwind.config.js +43 -0
  67. package/tsconfig.json +24 -0
  68. package/vite.config.ts +27 -0
@@ -0,0 +1,99 @@
1
+ <script setup lang="ts">
2
+ import { useRouter } from 'vue-router';
3
+ import { useUiStore } from '../stores/ui';
4
+ import { useVocabularyStore } from '../stores/vocabulary';
5
+ import { useSiteConfig } from '../config/use-site-config';
6
+ import { ref } from 'vue';
7
+
8
+ const router = useRouter();
9
+ const ui = useUiStore();
10
+ const store = useVocabularyStore();
11
+ const { config: siteConfig } = useSiteConfig();
12
+ const searchInput = ref('');
13
+
14
+ function doSearch() {
15
+ const q = searchInput.value.trim();
16
+ if (q) {
17
+ ui.searchQuery = q;
18
+ router.push({ name: 'search', query: { q } });
19
+ }
20
+ }
21
+
22
+ function goHome() {
23
+ router.push({ name: 'home' });
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <header class="bg-surface-raised border-b border-ink-100/80 z-30 relative">
29
+ <div class="px-4 lg:px-5 h-14 flex items-center gap-3">
30
+ <!-- Mobile hamburger -->
31
+ <button
32
+ @click="ui.toggleSidebar()"
33
+ :aria-label="ui.sidebarOpen ? 'Close navigation menu' : 'Open navigation menu'"
34
+ class="lg:hidden p-2 -ml-1 rounded-lg text-ink-600 hover:bg-ink-50 transition-colors flex-shrink-0"
35
+ >
36
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"/>
38
+ </svg>
39
+ </button>
40
+
41
+ <!-- Logo -->
42
+ <button @click="goHome" class="flex items-center gap-2 hover:opacity-80 transition flex-shrink-0 group">
43
+ <div v-if="siteConfig?.branding?.logo" class="h-8 flex items-center">
44
+ <img
45
+ :src="siteConfig.branding.logo.path"
46
+ :alt="siteConfig.branding.logo.alt"
47
+ class="h-8 max-w-[48px] object-contain rounded"
48
+ />
49
+ </div>
50
+ <div v-else class="w-8 h-8 rounded-lg flex items-center justify-center transition-colors" style="background-color: var(--brand-dark)">
51
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
52
+ <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
53
+ <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
54
+ <line x1="9" y1="7" x2="17" y2="7"/>
55
+ <line x1="9" y1="11" x2="15" y2="11"/>
56
+ </svg>
57
+ </div>
58
+ <span class="font-serif text-lg text-ink-800 leading-none hidden sm:inline">{{ siteConfig?.title || 'Glossarist' }}</span>
59
+ </button>
60
+
61
+ <!-- Search -->
62
+ <form @submit.prevent="doSearch" class="flex-1 max-w-lg mx-2 sm:mx-4">
63
+ <div class="relative">
64
+ <input
65
+ v-model="searchInput"
66
+ type="text"
67
+ aria-label="Search concepts"
68
+ placeholder="Search..."
69
+ class="w-full pl-9 pr-3 py-2 text-sm bg-surface border border-ink-100 rounded-lg focus:ring-2 focus:ring-ink-200 focus:border-ink-400 outline-none placeholder:text-ink-300 transition-all"
70
+ />
71
+ <svg class="absolute left-3 top-2.5 w-4 h-4 text-ink-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
73
+ </svg>
74
+ </div>
75
+ </form>
76
+
77
+ <!-- Stats -->
78
+ <div class="text-xs text-ink-400 flex-shrink-0 hidden md:block">
79
+ {{ store.datasetList.length }} datasets
80
+ </div>
81
+
82
+ <!-- Theme toggle -->
83
+ <button
84
+ @click="ui.toggleTheme()"
85
+ :aria-label="ui.isDark ? 'Switch to light mode' : 'Switch to dark mode'"
86
+ class="p-2 rounded-lg text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors flex-shrink-0"
87
+ >
88
+ <!-- Sun icon (shown in dark mode) -->
89
+ <svg v-if="ui.isDark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
90
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
91
+ </svg>
92
+ <!-- Moon icon (shown in light mode) -->
93
+ <svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
95
+ </svg>
96
+ </button>
97
+ </div>
98
+ </header>
99
+ </template>
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { useVocabularyStore } from '../stores/vocabulary';
4
+ import { useUiStore } from '../stores/ui';
5
+ import { useRoute, useRouter } from 'vue-router';
6
+ import { useDsStyle } from '../utils/dataset-style';
7
+ import { useSiteConfig } from '../config/use-site-config';
8
+ import NavIcon from './NavIcon.vue';
9
+
10
+ const store = useVocabularyStore();
11
+ const ui = useUiStore();
12
+ const router = useRouter();
13
+ const route = useRoute();
14
+ const { getColor } = useDsStyle();
15
+ const { globalPages, datasetPages } = useSiteConfig();
16
+
17
+ const currentDataset = computed(() => (route.params as any).registerId ?? '');
18
+
19
+ const datasetEntries = computed(() => {
20
+ const entries: { id: string; title: string; loaded: boolean; conceptCount: number }[] = [];
21
+ for (const [id, adapter] of store.datasets) {
22
+ const m = store.manifests.get(id);
23
+ entries.push({
24
+ id,
25
+ title: m?.title ?? id.toUpperCase(),
26
+ loaded: !!m,
27
+ conceptCount: m?.conceptCount ?? 0,
28
+ });
29
+ }
30
+ return entries;
31
+ });
32
+
33
+ const currentManifest = computed(() => store.manifests.get(currentDataset.value));
34
+
35
+ function closeMobile() { ui.sidebarOpen = false; }
36
+
37
+ function goToDataset(id: string) {
38
+ router.push({ name: 'dataset', params: { registerId: id } });
39
+ closeMobile();
40
+ }
41
+
42
+ function pageRoute(page: { route: string; datasetScoped?: boolean }): string {
43
+ if (!page.route) return '/';
44
+ if (page.datasetScoped) {
45
+ return `/dataset/${currentDataset.value}/${page.route}`;
46
+ }
47
+ return `/${page.route}`;
48
+ }
49
+
50
+ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
51
+ if (!page.route) return route.name === 'home';
52
+ if (page.datasetScoped) return route.name === page.route;
53
+ return route.name === page.route;
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <!-- Mobile backdrop -->
59
+ <div v-if="ui.sidebarOpen" @click="closeMobile" class="lg:hidden fixed inset-0 bg-ink-800/30 z-40"></div>
60
+
61
+ <!-- Sidebar -->
62
+ <aside
63
+ :class="ui.sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
64
+ class="fixed lg:static inset-y-0 left-0 z-50 w-60 bg-surface-raised border-r border-ink-100/80 overflow-y-auto flex-shrink-0 transition-transform duration-200 lg:transition-none"
65
+ style="top: 56px;"
66
+ >
67
+ <div class="p-4">
68
+ <!-- Navigation -->
69
+ <div class="section-label">Navigation</div>
70
+ <nav class="space-y-0.5 mb-6">
71
+ <router-link
72
+ v-for="page in globalPages"
73
+ :key="page.route || 'home'"
74
+ :to="pageRoute(page)"
75
+ class="btn-ghost w-full text-left flex items-center gap-2"
76
+ :class="isActive(page) ? 'active' : ''"
77
+ @click="closeMobile"
78
+ >
79
+ <NavIcon :name="page.icon" />
80
+ {{ page.title }}
81
+ </router-link>
82
+ </nav>
83
+
84
+ <!-- Dataset-level navigation (shown when viewing a dataset) -->
85
+ <div v-if="currentManifest" class="mb-6">
86
+ <div class="section-label">{{ currentManifest.title }}</div>
87
+ <nav class="space-y-0.5">
88
+ <router-link
89
+ v-for="page in datasetPages"
90
+ :key="page.route || 'concepts'"
91
+ :to="pageRoute(page)"
92
+ class="btn-ghost w-full text-left flex items-center gap-2"
93
+ :class="isActive(page) ? 'active' : ''"
94
+ @click="closeMobile"
95
+ >
96
+ <NavIcon :name="page.icon" />
97
+ {{ page.title }}
98
+ </router-link>
99
+ </nav>
100
+ </div>
101
+
102
+ <!-- Datasets -->
103
+ <div class="section-label">Datasets</div>
104
+ <nav class="space-y-1">
105
+ <button
106
+ v-for="ds in datasetEntries"
107
+ :key="ds.id"
108
+ @click="goToDataset(ds.id)"
109
+ class="w-full text-left px-3 py-2.5 rounded-lg text-sm transition-all duration-150 border-l-2"
110
+ :class="[
111
+ currentDataset === ds.id
112
+ ? 'bg-surface text-ink-800'
113
+ : 'border-transparent text-ink-600 hover:bg-ink-50 hover:text-ink-800'
114
+ ]"
115
+ :style="currentDataset === ds.id ? { borderLeftColor: getColor(ds.id), borderLeftWidth: '2px' } : {}"
116
+ >
117
+ <div class="font-medium truncate leading-snug">{{ ds.title }}</div>
118
+ <div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400' : 'text-ink-300'">
119
+ {{ ds.conceptCount.toLocaleString() }} concepts
120
+ </div>
121
+ </button>
122
+ </nav>
123
+
124
+ <!-- Graph stats -->
125
+ <div class="mt-6 pt-4 border-t border-ink-100/60">
126
+ <div class="text-[11px] text-ink-300 space-y-0.5">
127
+ <div>{{ store.graph.nodeCount.toLocaleString() }} graph nodes</div>
128
+ <div>{{ store.graph.edgeCount.toLocaleString() }} edges</div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </aside>
133
+ </template>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type { ConceptSummary } from '../adapters/types';
3
+ import { computed } from 'vue';
4
+ import { useRouter } from 'vue-router';
5
+ import { useDsStyle } from '../utils/dataset-style';
6
+ import { useVocabularyStore } from '../stores/vocabulary';
7
+
8
+ const props = defineProps<{
9
+ entry: ConceptSummary;
10
+ registerId: string;
11
+ }>();
12
+
13
+ const router = useRouter();
14
+ const { getColor } = useDsStyle();
15
+ const store = useVocabularyStore();
16
+
17
+ function viewConcept() {
18
+ router.push({
19
+ name: 'concept',
20
+ params: { registerId: props.registerId, conceptId: props.entry.id },
21
+ });
22
+ }
23
+
24
+ function statusColor(status: string): string {
25
+ if (status === 'valid' || status === 'Standard') return 'bg-emerald-50 text-emerald-600';
26
+ if (status === 'superseded') return 'bg-red-50 text-red-600';
27
+ if (status === 'withdrawn') return 'bg-red-100 text-red-700';
28
+ return 'bg-amber-50 text-amber-600';
29
+ }
30
+
31
+ const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.languages ?? []);
32
+ </script>
33
+
34
+ <template>
35
+ <button
36
+ @click="viewConcept"
37
+ class="card-hover p-4 text-left w-full border-l-2 group"
38
+ :style="{ borderLeftColor: getColor(registerId) }"
39
+ >
40
+ <div class="flex items-start justify-between gap-2">
41
+ <div class="min-w-0">
42
+ <h3 class="font-medium text-ink-800 truncate group-hover:text-ink-900 transition-colors leading-snug text-[15px]">
43
+ {{ entry.eng || entry.id }}
44
+ </h3>
45
+ <p class="text-[11px] text-ink-300 mt-1 font-mono tabular-nums">{{ entry.id }}</p>
46
+ </div>
47
+ <span
48
+ class="text-[9px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded flex-shrink-0 mt-0.5"
49
+ :class="statusColor(entry.status)"
50
+ >
51
+ {{ entry.status === 'Standard' ? 'valid' : entry.status }}
52
+ </span>
53
+ </div>
54
+ <!-- Language coverage dots -->
55
+ <div class="flex gap-0.5 mt-2.5" :aria-label="`${manifestLanguages.length} languages`" role="img">
56
+ <span
57
+ v-for="lang in manifestLanguages"
58
+ :key="lang"
59
+ class="w-1.5 h-1.5 rounded-full"
60
+ :style="{ backgroundColor: getColor(registerId) + '60' }"
61
+ :aria-label="lang"
62
+ ></span>
63
+ </div>
64
+ </button>
65
+ </template>