@glossarist/concept-browser 0.1.9 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
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": {
@@ -678,46 +678,8 @@ function processNewsPage(config, page) {
678
678
  console.log(`Generated news index: ${index.length} posts, ${postFiles.length} files copied to public/news/`);
679
679
  }
680
680
 
681
- function processContributorsPage(config, page) {
682
- const contributors = { register: config.id, contributors: [] };
683
-
684
- for (const ds of config.datasets) {
685
- const infoYamlPath = path.join(ROOT, '.datasets', ds.id, 'info.yaml');
686
- if (!fs.existsSync(infoYamlPath)) continue;
687
-
688
- try {
689
- const info = readYaml(infoYamlPath);
690
- if (info.header) {
691
- contributors.owner = info.header['register-owner'];
692
- contributors.manager = info.header['register-manager'];
693
- }
694
- if (info.languages) {
695
- for (const [lang, data] of Object.entries(info.languages)) {
696
- contributors.contributors.push({
697
- language: lang,
698
- registerName: data['register-name'] || '',
699
- organization: data['submitting-organisation-name'] || '',
700
- contact: data['submitting-organisation-contact'] || '',
701
- email: data['submitting-organisation-contact-email'] || '',
702
- uri: data['uniform-resource-identifier-uri'] || '',
703
- country: data['operating-language-country'] || '',
704
- });
705
- }
706
- }
707
- } catch (e) {
708
- console.warn(` Skipping contributors for ${ds.id}: ${e.message}`);
709
- }
710
- }
711
-
712
- if (contributors.contributors.length > 0 || contributors.owner) {
713
- writeJson(path.join(PUBLIC, 'contributors.json'), contributors);
714
- console.log(`Generated contributors: ${contributors.contributors.length} languages`);
715
- }
716
- }
717
-
718
681
  const pageProcessors = {
719
682
  news: processNewsPage,
720
- contributors: processContributorsPage,
721
683
  };
722
684
 
723
685
  function synthesizePages(config) {
@@ -766,6 +728,7 @@ writeJson(path.join(PUBLIC, 'site-config.json'), {
766
728
  defaults: config.defaults,
767
729
  email: config.email,
768
730
  pages: processedPages.length > 0 ? processedPages : undefined,
731
+ contributors: config.contributors || undefined,
769
732
  });
770
733
  console.log('Generated site-config.json');
771
734
 
package/src/App.vue CHANGED
@@ -2,14 +2,12 @@
2
2
  import { onMounted, onUnmounted, ref } from 'vue';
3
3
  import { useVocabularyStore } from './stores/vocabulary';
4
4
  import { useSiteConfig } from './config/use-site-config';
5
- import { buildPageRoutes } from './router/page-routes';
6
- import router from './router';
7
5
  import AppHeader from './components/AppHeader.vue';
8
6
  import AppSidebar from './components/AppSidebar.vue';
9
7
  import AppFooter from './components/AppFooter.vue';
10
8
 
11
9
  const store = useVocabularyStore();
12
- const { loadConfig, config, globalPages, datasetPages } = useSiteConfig();
10
+ const { loadConfig, config } = useSiteConfig();
13
11
  const appReady = ref(false);
14
12
  const showScrollTop = ref(false);
15
13
  let mainEl: HTMLElement | null = null;
@@ -27,12 +25,6 @@ onMounted(async () => {
27
25
  if (cfg?.title) {
28
26
  document.title = cfg.title;
29
27
  }
30
- const allPages = [...globalPages.value, ...datasetPages.value];
31
- if (allPages.length > 0) {
32
- for (const route of buildPageRoutes(allPages)) {
33
- router.addRoute(route);
34
- }
35
- }
36
28
  appReady.value = true;
37
29
  // Watch scroll on main content area
38
30
  mainEl = document.querySelector('main');
@@ -48,8 +48,10 @@ function pageRoute(page: { route: string; datasetScoped?: boolean }): string {
48
48
  }
49
49
 
50
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;
51
+ if (!page.route) {
52
+ if (page.datasetScoped) return route.name === 'dataset' || route.name === 'concept';
53
+ return route.name === 'home';
54
+ }
53
55
  return route.name === page.route;
54
56
  }
55
57
  </script>
@@ -93,6 +93,16 @@ export interface DatasetConfig {
93
93
  downloads?: string[];
94
94
  }
95
95
 
96
+ // === Contributors ===
97
+
98
+ export interface Contributor {
99
+ name: string;
100
+ role?: string;
101
+ organization?: string;
102
+ url?: string;
103
+ email?: string;
104
+ }
105
+
96
106
  // === Downloads ===
97
107
 
98
108
  export interface BulkFormatInfo {
@@ -144,4 +154,5 @@ export interface SiteConfig {
144
154
  };
145
155
  email?: string;
146
156
  pages?: PageConfig[];
157
+ contributors?: Contributor[];
147
158
  }
@@ -29,6 +29,7 @@ export interface RuntimeSiteConfig {
29
29
  defaults?: { language?: string; languageOrder?: string[] };
30
30
  email?: string;
31
31
  pages?: PageConfig[];
32
+ contributors?: { name: string; role?: string; organization?: string; url?: string; email?: string }[];
32
33
  }
33
34
 
34
35
  const siteConfig = ref<RuntimeSiteConfig | null>(null);
@@ -103,42 +104,51 @@ async function loadConfig(): Promise<RuntimeSiteConfig | null> {
103
104
  return siteConfig.value;
104
105
  }
105
106
 
106
- const BUILTIN_GLOBAL_PAGES: PageConfig[] = [
107
- { type: 'custom', route: '', title: 'Home', icon: 'home' },
108
- { type: 'custom', route: 'search', title: 'Search', icon: 'search' },
109
- { type: 'custom', route: 'graph', title: 'Graph', icon: 'graph' },
110
- ];
107
+ function synthesizeGlobalPages(features?: Record<string, unknown>, pages?: PageConfig[]): PageConfig[] {
108
+ if (pages && pages.length > 0) return pages.filter(p => !p.datasetScoped);
111
109
 
112
- const BUILTIN_DATASET_PAGES: PageConfig[] = [
113
- { type: 'custom', route: '', title: 'Concepts', icon: 'list' },
114
- { type: 'stats', route: 'stats', title: 'Statistics', icon: 'chart' },
115
- { type: 'about', route: 'about', title: 'About', icon: 'info' },
116
- ];
117
-
118
- function synthesizePages(features?: Record<string, unknown>, pages?: PageConfig[]) {
119
- if (pages && pages.length > 0) return pages;
120
-
121
- const result = [...BUILTIN_GLOBAL_PAGES];
110
+ const result: PageConfig[] = [
111
+ { type: 'custom', route: '', title: 'Home', icon: 'home' },
112
+ ];
113
+ if (features?.search !== false) {
114
+ result.push({ type: 'custom', route: 'search', title: 'Search', icon: 'search' });
115
+ }
116
+ if (features?.graph !== false) {
117
+ result.push({ type: 'custom', route: 'graph', title: 'Graph', icon: 'graph' });
118
+ }
122
119
  if (features?.news) {
123
120
  result.push({ type: 'news', route: 'news', title: 'News', icon: 'newspaper' });
124
121
  }
125
122
  return result;
126
123
  }
127
124
 
125
+ function synthesizeDatasetPages(features?: Record<string, unknown>, pages?: PageConfig[]): PageConfig[] {
126
+ const declared = pages?.filter(p => p.datasetScoped) ?? [];
127
+ if (declared.length > 0) return declared;
128
+
129
+ const result: PageConfig[] = [
130
+ { type: 'custom', route: '', title: 'Concepts', icon: 'list', datasetScoped: true },
131
+ ];
132
+ if (features?.stats !== false) {
133
+ result.push({ type: 'stats', route: 'stats', title: 'Statistics', icon: 'chart', datasetScoped: true });
134
+ }
135
+ if (features?.about !== false) {
136
+ result.push({ type: 'about', route: 'about', title: 'About', icon: 'info', datasetScoped: true });
137
+ }
138
+ return result;
139
+ }
140
+
128
141
  export function useSiteConfig() {
129
142
  const config = computed(() => siteConfig.value);
130
143
  const visibleDatasets = computed(() => siteConfig.value?.datasets ?? []);
131
144
 
132
145
  const globalPages = computed<PageConfig[]>(() =>
133
- synthesizePages(siteConfig.value?.features, siteConfig.value?.pages)
134
- .filter(p => !p.datasetScoped),
146
+ synthesizeGlobalPages(siteConfig.value?.features, siteConfig.value?.pages),
135
147
  );
136
148
 
137
- const datasetPages = computed<PageConfig[]>(() => {
138
- const declared = siteConfig.value?.pages?.filter(p => p.datasetScoped) ?? [];
139
- if (declared.length > 0) return declared;
140
- return BUILTIN_DATASET_PAGES;
141
- });
149
+ const datasetPages = computed<PageConfig[]>(() =>
150
+ synthesizeDatasetPages(siteConfig.value?.features, siteConfig.value?.pages),
151
+ );
142
152
 
143
153
  return { config, visibleDatasets, loadConfig, globalPages, datasetPages };
144
154
  }
@@ -18,6 +18,18 @@ const routes: RouteRecordRaw[] = [
18
18
  component: () => import('../views/ConceptView.vue'),
19
19
  props: true,
20
20
  },
21
+ {
22
+ path: '/dataset/:registerId/stats',
23
+ name: 'stats',
24
+ component: () => import('../views/StatsView.vue'),
25
+ props: true,
26
+ },
27
+ {
28
+ path: '/dataset/:registerId/about',
29
+ name: 'about',
30
+ component: () => import('../views/AboutView.vue'),
31
+ props: true,
32
+ },
21
33
  {
22
34
  path: '/search',
23
35
  name: 'search',
@@ -28,6 +40,16 @@ const routes: RouteRecordRaw[] = [
28
40
  name: 'graph',
29
41
  component: () => import('../views/GraphView.vue'),
30
42
  },
43
+ {
44
+ path: '/news',
45
+ name: 'news',
46
+ component: () => import('../views/NewsView.vue'),
47
+ },
48
+ {
49
+ path: '/contributors',
50
+ name: 'contributors',
51
+ component: () => import('../views/ContributorsView.vue'),
52
+ },
31
53
  {
32
54
  path: '/resolve/:uri(.*)',
33
55
  name: 'resolve',
@@ -1,38 +1,16 @@
1
1
  <script setup lang="ts">
2
- import { ref, onMounted } from 'vue';
3
2
  import { useSiteConfig } from '../config/use-site-config';
4
3
 
5
4
  interface Contributor {
6
- language: string;
7
- registerName: string;
8
- organization: string;
9
- contact: string;
10
- email: string;
11
- uri: string;
12
- country: string;
13
- }
14
-
15
- interface ContributorsData {
16
- register: string;
17
- owner: string;
18
- manager: string;
19
- contributors: Contributor[];
5
+ name: string;
6
+ role?: string;
7
+ organization?: string;
8
+ url?: string;
9
+ email?: string;
20
10
  }
21
11
 
22
12
  const { config } = useSiteConfig();
23
- const data = ref<ContributorsData | null>(null);
24
- const loading = ref(true);
25
- const error = ref<string | null>(null);
26
-
27
- onMounted(async () => {
28
- try {
29
- const resp = await fetch('/contributors.json');
30
- if (resp.ok) data.value = await resp.json();
31
- } catch (e: any) {
32
- error.value = e.message;
33
- }
34
- loading.value = false;
35
- });
13
+ const contributors = config.value?.contributors as Contributor[] | undefined;
36
14
  </script>
37
15
 
38
16
  <template>
@@ -44,62 +22,32 @@ onMounted(async () => {
44
22
  </nav>
45
23
  <h1 class="font-serif text-3xl text-ink-800 mb-2">Contributors</h1>
46
24
  <p class="text-ink-400 mb-8">
47
- Organizations and individuals contributing to {{ config?.branding?.ownerName || config?.title || 'this glossary' }}.
25
+ People and organizations contributing to {{ config?.branding?.ownerName || config?.title || 'this glossary' }}.
48
26
  </p>
49
27
 
50
- <template v-if="loading">
51
- <div class="animate-pulse space-y-6">
52
- <div class="card p-6 space-y-3">
53
- <div class="h-4 bg-ink-100 rounded w-40"></div>
54
- <div class="h-6 bg-ink-100 rounded w-3/4"></div>
55
- <div class="h-4 bg-ink-100 rounded w-1/2"></div>
56
- </div>
57
- </div>
58
- </template>
59
-
60
- <template v-else-if="error">
61
- <div class="card p-8 text-center">
62
- <p class="text-ink-500">Failed to load contributors.</p>
63
- </div>
64
- </template>
65
-
66
- <template v-else-if="data">
67
- <!-- Register metadata -->
68
- <div v-if="data.owner" class="card p-6 mb-6">
69
- <h2 class="section-label">Register Information</h2>
70
- <dl class="space-y-3 mt-3">
71
- <div v-if="data.owner" class="flex items-start gap-4">
72
- <dt class="text-ink-400 text-sm w-40 flex-shrink-0 pt-0.5">Owner</dt>
73
- <dd class="text-ink-800 font-medium">{{ data.owner }}</dd>
74
- </div>
75
- <div v-if="data.manager" class="flex items-start gap-4">
76
- <dt class="text-ink-400 text-sm w-40 flex-shrink-0 pt-0.5">Manager</dt>
77
- <dd class="text-ink-800">{{ data.manager }}</dd>
78
- </div>
79
- </dl>
80
- </div>
81
-
82
- <!-- Per-language contributors -->
83
- <div v-if="data.contributors.length" class="card overflow-x-auto">
28
+ <template v-if="contributors?.length">
29
+ <div class="card overflow-x-auto">
84
30
  <table class="w-full text-sm">
85
31
  <thead>
86
32
  <tr class="border-b border-ink-100/60 bg-ink-50/50">
87
- <th class="text-left px-5 py-3 text-ink-500 font-medium">Language</th>
33
+ <th class="text-left px-5 py-3 text-ink-500 font-medium">Name</th>
34
+ <th class="text-left px-5 py-3 text-ink-500 font-medium">Role</th>
88
35
  <th class="text-left px-5 py-3 text-ink-500 font-medium">Organization</th>
89
36
  <th class="text-left px-5 py-3 text-ink-500 font-medium">Contact</th>
90
37
  </tr>
91
38
  </thead>
92
39
  <tbody>
93
- <tr v-for="c in data.contributors" :key="c.language" class="border-b border-ink-50 last:border-0">
94
- <td class="px-5 py-3">
95
- <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ c.language.toUpperCase() }}</span>
96
- <span v-if="c.registerName" class="ml-2 text-ink-600 text-xs">{{ c.registerName }}</span>
40
+ <tr v-for="c in contributors" :key="c.name" class="border-b border-ink-50 last:border-0">
41
+ <td class="px-5 py-3 text-ink-800 font-medium">
42
+ <a v-if="c.url" :href="c.url" target="_blank" class="concept-link">{{ c.name }}</a>
43
+ <span v-else>{{ c.name }}</span>
97
44
  </td>
98
- <td class="px-5 py-3 text-ink-700">
99
- <span v-if="c.uri"><a :href="c.uri" target="_blank" class="concept-link">{{ c.organization }}</a></span>
100
- <span v-else>{{ c.organization }}</span>
45
+ <td class="px-5 py-3 text-ink-600">{{ c.role || '—' }}</td>
46
+ <td class="px-5 py-3 text-ink-700">{{ c.organization || '—' }}</td>
47
+ <td class="px-5 py-3">
48
+ <a v-if="c.email" :href="`mailto:${c.email}`" class="concept-link text-xs">{{ c.email }}</a>
49
+ <span v-else class="text-ink-300">—</span>
101
50
  </td>
102
- <td class="px-5 py-3 text-ink-500 text-xs">{{ c.contact }}</td>
103
51
  </tr>
104
52
  </tbody>
105
53
  </table>
@@ -1,35 +0,0 @@
1
- import type { RouteRecordRaw } from 'vue-router';
2
- import type { PageConfig } from '../config/types';
3
-
4
- const pageComponents: Record<string, () => Promise<any>> = {
5
- news: () => import('../views/NewsView.vue'),
6
- contributors: () => import('../views/ContributorsView.vue'),
7
- about: () => import('../views/AboutView.vue'),
8
- stats: () => import('../views/StatsView.vue'),
9
- };
10
-
11
- export function buildPageRoutes(pages: PageConfig[]): RouteRecordRaw[] {
12
- const routes: RouteRecordRaw[] = [];
13
-
14
- for (const page of pages) {
15
- const component = pageComponents[page.type];
16
- if (!component) continue;
17
-
18
- if (page.datasetScoped) {
19
- routes.push({
20
- path: `/dataset/:registerId/${page.route}`,
21
- name: page.route,
22
- component,
23
- props: true,
24
- });
25
- } else {
26
- routes.push({
27
- path: `/${page.route}`,
28
- name: page.route,
29
- component,
30
- });
31
- }
32
- }
33
-
34
- return routes;
35
- }