@glossarist/concept-browser 0.2.1 → 0.2.2
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 +1 -1
- package/scripts/generate-data.mjs +95 -0
- package/src/composables/use-dataset-loader.ts +10 -5
- package/src/config/types.ts +1 -1
- package/src/router/index.ts +22 -0
- package/src/style.css +22 -0
- package/src/utils/markdown-lite.ts +109 -0
- package/src/views/AboutView.vue +6 -6
- package/src/views/PageView.vue +61 -0
- package/src/views/StatsView.vue +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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,8 +678,103 @@ 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
|
+
// --- Markdown-lite renderer (isomorphic, same logic as src/utils/markdown-lite.ts) ---
|
|
682
|
+
|
|
683
|
+
function renderMarkdown(input) {
|
|
684
|
+
const INLINE_PATTERNS = [
|
|
685
|
+
[/\*\*(.+?)\*\*/g, m => `<strong>${m[1]}</strong>`],
|
|
686
|
+
[/(?<!\*)\*([^*]+?)\*(?!\*)/g, m => `<em>${m[1]}</em>`],
|
|
687
|
+
[/`([^`]+?)`/g, m => `<code>${m[1]}</code>`],
|
|
688
|
+
[/\[([^\]]+)\]\(([^)]+)\)/g, m => `<a href="${m[2]}" target="_blank">${m[1]}</a>`],
|
|
689
|
+
];
|
|
690
|
+
function renderInline(text) {
|
|
691
|
+
for (const [re, fn] of INLINE_PATTERNS) {
|
|
692
|
+
text = text.replace(re, (...args) => fn(args));
|
|
693
|
+
}
|
|
694
|
+
return text;
|
|
695
|
+
}
|
|
696
|
+
function escapeHtml(s) {
|
|
697
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const blocks = [];
|
|
701
|
+
const lines = input.split('\n');
|
|
702
|
+
let i = 0;
|
|
703
|
+
while (i < lines.length) {
|
|
704
|
+
const line = lines[i];
|
|
705
|
+
if (line.trimStart().startsWith('```')) {
|
|
706
|
+
const lang = line.trim().slice(3);
|
|
707
|
+
const codeLines = [];
|
|
708
|
+
i++;
|
|
709
|
+
while (i < lines.length && !lines[i].trimStart().startsWith('```')) { codeLines.push(lines[i]); i++; }
|
|
710
|
+
i++;
|
|
711
|
+
blocks.push(`<pre><code${lang ? ` class="language-${lang}"` : ''}>${escapeHtml(codeLines.join('\n'))}</code></pre>`);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const hm = line.match(/^(#{1,4})\s+(.+)/);
|
|
715
|
+
if (hm) { blocks.push(`<h${hm[1].length + 1}>${renderInline(hm[2])}</h${hm[1].length + 1}>`); i++; continue; }
|
|
716
|
+
if (/^---+\s*$/.test(line)) { blocks.push('<hr>'); i++; continue; }
|
|
717
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
718
|
+
const items = [];
|
|
719
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) { items.push(`<li>${renderInline(lines[i].replace(/^\s*[-*]\s+/, ''))}</li>`); i++; }
|
|
720
|
+
blocks.push(`<ul>${items.join('')}</ul>`); continue;
|
|
721
|
+
}
|
|
722
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
723
|
+
const items = [];
|
|
724
|
+
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) { items.push(`<li>${renderInline(lines[i].replace(/^\s*\d+\.\s+/, ''))}</li>`); i++; }
|
|
725
|
+
blocks.push(`<ol>${items.join('')}</ol>`); continue;
|
|
726
|
+
}
|
|
727
|
+
if (/^>\s?/.test(line)) {
|
|
728
|
+
const ql = [];
|
|
729
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) { ql.push(lines[i].replace(/^>\s?/, '')); i++; }
|
|
730
|
+
blocks.push(`<blockquote>${renderInline(ql.join(' '))}</blockquote>`); continue;
|
|
731
|
+
}
|
|
732
|
+
if (!line.trim()) { i++; continue; }
|
|
733
|
+
const pl = [];
|
|
734
|
+
while (i < lines.length && lines[i].trim() && !/^#{1,4}\s/.test(lines[i]) && !/^\s*[-*]\s+/.test(lines[i]) && !/^\s*\d+\.\s+/.test(lines[i]) && !/^>\s?/.test(lines[i]) && !lines[i].trimStart().startsWith('```')) { pl.push(lines[i]); i++; }
|
|
735
|
+
if (pl.length) blocks.push(`<p>${renderInline(pl.join(' '))}</p>`);
|
|
736
|
+
}
|
|
737
|
+
return blocks.join('\n');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function processContentPage(config, page) {
|
|
741
|
+
if (!page.source) {
|
|
742
|
+
console.warn(` Skipping content page '${page.route}': no source file`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const srcPath = path.resolve(ROOT, page.source);
|
|
746
|
+
if (!fs.existsSync(srcPath)) {
|
|
747
|
+
console.warn(` Skipping content page '${page.route}': source not found (${srcPath})`);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const raw = fs.readFileSync(srcPath, 'utf8');
|
|
751
|
+
const ext = path.extname(srcPath).toLowerCase();
|
|
752
|
+
let html;
|
|
753
|
+
if (ext === '.html' || ext === '.htm') {
|
|
754
|
+
html = raw;
|
|
755
|
+
} else {
|
|
756
|
+
const stripped = stripFrontmatter(raw);
|
|
757
|
+
html = renderMarkdown(stripped);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const pagesDir = path.join(PUBLIC, 'pages');
|
|
761
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
762
|
+
writeJson(path.join(pagesDir, `${page.route}.json`), { title: page.title, html });
|
|
763
|
+
console.log(` Generated content page: ${page.route} (${ext})`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function stripFrontmatter(text) {
|
|
767
|
+
const lines = text.split('\n');
|
|
768
|
+
if (lines[0] !== '---') return text;
|
|
769
|
+
let end = -1;
|
|
770
|
+
for (let i = 1; i < lines.length; i++) { if (lines[i] === '---') { end = i; break; } }
|
|
771
|
+
if (end < 0) return text;
|
|
772
|
+
return lines.slice(end + 1).join('\n').trim();
|
|
773
|
+
}
|
|
774
|
+
|
|
681
775
|
const pageProcessors = {
|
|
682
776
|
news: processNewsPage,
|
|
777
|
+
page: processContentPage,
|
|
683
778
|
};
|
|
684
779
|
|
|
685
780
|
function synthesizePages(config) {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { ref, onMounted, watch } from 'vue';
|
|
1
|
+
import { ref, computed, onMounted, watch } from 'vue';
|
|
2
2
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
3
|
+
import { useSiteConfig } from '../config/use-site-config';
|
|
3
4
|
|
|
4
|
-
export function useDatasetLoader(registerId: () => string) {
|
|
5
|
+
export function useDatasetLoader(registerId: () => string | undefined) {
|
|
5
6
|
const store = useVocabularyStore();
|
|
7
|
+
const { config } = useSiteConfig();
|
|
6
8
|
const loading = ref(false);
|
|
7
9
|
const localError = ref<string | null>(null);
|
|
8
10
|
|
|
11
|
+
const resolvedId = computed(() => registerId() || config.value?.defaultDataset || '');
|
|
12
|
+
|
|
9
13
|
async function ensureLoaded() {
|
|
10
|
-
const id =
|
|
14
|
+
const id = resolvedId.value;
|
|
15
|
+
if (!id) return;
|
|
11
16
|
const adapter = store.datasets.get(id);
|
|
12
17
|
if (adapter?.index) return;
|
|
13
18
|
loading.value = true;
|
|
@@ -21,7 +26,7 @@ export function useDatasetLoader(registerId: () => string) {
|
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
onMounted(ensureLoaded);
|
|
24
|
-
watch(
|
|
29
|
+
watch(resolvedId, ensureLoaded);
|
|
25
30
|
|
|
26
|
-
return { loading, localError, ensureLoaded };
|
|
31
|
+
return { loading, localError, ensureLoaded, resolvedId };
|
|
27
32
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -121,7 +121,7 @@ export const FORMAT_LABELS: Record<string, string> = {
|
|
|
121
121
|
|
|
122
122
|
// === Pages ===
|
|
123
123
|
|
|
124
|
-
export type PageType = 'news' | 'contributors' | 'about' | 'stats' | 'custom';
|
|
124
|
+
export type PageType = 'page' | 'news' | 'contributors' | 'about' | 'stats' | 'custom';
|
|
125
125
|
|
|
126
126
|
export interface PageConfig {
|
|
127
127
|
type: PageType;
|
package/src/router/index.ts
CHANGED
|
@@ -50,11 +50,33 @@ const routes: RouteRecordRaw[] = [
|
|
|
50
50
|
name: 'contributors',
|
|
51
51
|
component: () => import('../views/ContributorsView.vue'),
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
path: '/about',
|
|
55
|
+
name: 'about-global',
|
|
56
|
+
component: () => import('../views/AboutView.vue'),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: '/stats',
|
|
60
|
+
name: 'stats-global',
|
|
61
|
+
component: () => import('../views/StatsView.vue'),
|
|
62
|
+
},
|
|
53
63
|
{
|
|
54
64
|
path: '/resolve/:uri(.*)',
|
|
55
65
|
name: 'resolve',
|
|
56
66
|
component: () => import('../views/ResolveView.vue'),
|
|
57
67
|
},
|
|
68
|
+
// Catch-all for custom content pages (lowest priority)
|
|
69
|
+
{
|
|
70
|
+
path: '/dataset/:registerId/:page',
|
|
71
|
+
name: 'dataset-page',
|
|
72
|
+
component: () => import('../views/PageView.vue'),
|
|
73
|
+
props: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
path: '/:slug',
|
|
77
|
+
name: 'page',
|
|
78
|
+
component: () => import('../views/PageView.vue'),
|
|
79
|
+
},
|
|
58
80
|
];
|
|
59
81
|
|
|
60
82
|
const router = createRouter({
|
package/src/style.css
CHANGED
|
@@ -160,51 +160,73 @@
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/* Prose content (news posts, etc.) */
|
|
163
|
+
.prose-page,
|
|
163
164
|
.prose-news {
|
|
164
165
|
@apply text-sm text-ink-700 leading-relaxed;
|
|
165
166
|
}
|
|
167
|
+
.prose-page h2,
|
|
166
168
|
.prose-news h2 {
|
|
167
169
|
@apply font-serif text-xl text-ink-800 mt-6 mb-3;
|
|
168
170
|
}
|
|
171
|
+
.prose-page h3,
|
|
169
172
|
.prose-news h3 {
|
|
170
173
|
@apply font-serif text-lg text-ink-800 mt-5 mb-2;
|
|
171
174
|
}
|
|
175
|
+
.prose-page h4,
|
|
172
176
|
.prose-news h4 {
|
|
173
177
|
@apply font-semibold text-ink-800 mt-4 mb-2;
|
|
174
178
|
}
|
|
179
|
+
.prose-page p,
|
|
175
180
|
.prose-news p {
|
|
176
181
|
@apply mb-4;
|
|
177
182
|
}
|
|
183
|
+
.prose-page ul, .prose-page ol,
|
|
178
184
|
.prose-news ul, .prose-news ol {
|
|
179
185
|
@apply mb-4 pl-5;
|
|
180
186
|
}
|
|
187
|
+
.prose-page ul,
|
|
181
188
|
.prose-news ul {
|
|
182
189
|
@apply list-disc;
|
|
183
190
|
}
|
|
191
|
+
.prose-page ol,
|
|
184
192
|
.prose-news ol {
|
|
185
193
|
@apply list-decimal;
|
|
186
194
|
}
|
|
195
|
+
.prose-page li,
|
|
187
196
|
.prose-news li {
|
|
188
197
|
@apply mb-1;
|
|
189
198
|
}
|
|
199
|
+
.prose-page a,
|
|
190
200
|
.prose-news a {
|
|
191
201
|
@apply concept-link;
|
|
192
202
|
}
|
|
203
|
+
.prose-page strong,
|
|
193
204
|
.prose-news strong {
|
|
194
205
|
@apply font-semibold text-ink-800;
|
|
195
206
|
}
|
|
207
|
+
.prose-page em,
|
|
196
208
|
.prose-news em {
|
|
197
209
|
@apply italic;
|
|
198
210
|
}
|
|
211
|
+
.prose-page code,
|
|
199
212
|
.prose-news code {
|
|
200
213
|
@apply bg-ink-50 text-ink-600 px-1 rounded text-xs font-mono;
|
|
201
214
|
}
|
|
215
|
+
.prose-page pre,
|
|
202
216
|
.prose-news pre {
|
|
203
217
|
@apply bg-ink-50 rounded-lg p-4 mb-4 overflow-x-auto text-xs font-mono;
|
|
204
218
|
}
|
|
219
|
+
.prose-page pre code,
|
|
205
220
|
.prose-news pre code {
|
|
206
221
|
@apply bg-transparent px-0 text-ink-700;
|
|
207
222
|
}
|
|
223
|
+
.prose-page blockquote,
|
|
224
|
+
.prose-news blockquote {
|
|
225
|
+
@apply border-l-4 border-ink-200 pl-4 italic text-ink-500 mb-4;
|
|
226
|
+
}
|
|
227
|
+
.prose-page hr {
|
|
228
|
+
@apply border-ink-100 my-6;
|
|
229
|
+
}
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
@layer utilities {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const INLINE_PATTERNS: [RegExp, (m: RegExpMatchArray) => string][] = [
|
|
2
|
+
[/\*\*(.+?)\*\*/g, m => `<strong>${m[1]}</strong>`],
|
|
3
|
+
[/(?<!\*)\*([^*]+?)\*(?!\*)/g, m => `<em>${m[1]}</em>`],
|
|
4
|
+
[/`([^`]+?)`/g, m => `<code>${m[1]}</code>`],
|
|
5
|
+
[/\[([^\]]+)\]\(([^)]+)\)/g, m => `<a href="${m[2]}" target="_blank">${m[1]}</a>`],
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function renderInline(text: string): string {
|
|
9
|
+
for (const [re, fn] of INLINE_PATTERNS) {
|
|
10
|
+
text = text.replace(re, (...args) => fn(args as any));
|
|
11
|
+
}
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderMarkdown(input: string): string {
|
|
16
|
+
const blocks: string[] = [];
|
|
17
|
+
const lines = input.split('\n');
|
|
18
|
+
let i = 0;
|
|
19
|
+
|
|
20
|
+
while (i < lines.length) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
|
|
23
|
+
// Code fence
|
|
24
|
+
if (line.trimStart().startsWith('```')) {
|
|
25
|
+
const lang = line.trim().slice(3);
|
|
26
|
+
const codeLines: string[] = [];
|
|
27
|
+
i++;
|
|
28
|
+
while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
|
|
29
|
+
codeLines.push(lines[i]);
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
i++; // skip closing fence
|
|
33
|
+
blocks.push(`<pre><code${lang ? ` class="language-${lang}"` : ''}>${escapeHtml(codeLines.join('\n'))}</code></pre>`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Heading
|
|
38
|
+
const headingMatch = line.match(/^(#{1,4})\s+(.+)/);
|
|
39
|
+
if (headingMatch) {
|
|
40
|
+
const level = headingMatch[1].length + 1; // h2-h5 (h1 reserved for page title)
|
|
41
|
+
blocks.push(`<h${level}>${renderInline(headingMatch[2])}</h${level}>`);
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Horizontal rule
|
|
47
|
+
if (/^---+\s*$/.test(line)) {
|
|
48
|
+
blocks.push('<hr>');
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Unordered list
|
|
54
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
55
|
+
const items: string[] = [];
|
|
56
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
|
|
57
|
+
items.push(`<li>${renderInline(lines[i].replace(/^\s*[-*]\s+/, ''))}</li>`);
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
blocks.push(`<ul>${items.join('')}</ul>`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ordered list
|
|
65
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
66
|
+
const items: string[] = [];
|
|
67
|
+
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
|
|
68
|
+
items.push(`<li>${renderInline(lines[i].replace(/^\s*\d+\.\s+/, ''))}</li>`);
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
blocks.push(`<ol>${items.join('')}</ol>`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Blockquote
|
|
76
|
+
if (/^>\s?/.test(line)) {
|
|
77
|
+
const quoteLines: string[] = [];
|
|
78
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
|
79
|
+
quoteLines.push(lines[i].replace(/^>\s?/, ''));
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
blocks.push(`<blockquote>${renderInline(quoteLines.join(' '))}</blockquote>`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Blank line
|
|
87
|
+
if (!line.trim()) {
|
|
88
|
+
i++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Paragraph
|
|
93
|
+
const paraLines: string[] = [];
|
|
94
|
+
while (i < lines.length && lines[i].trim() && !/^#{1,4}\s/.test(lines[i]) && !/^\s*[-*]\s+/.test(lines[i]) && !/^\s*\d+\.\s+/.test(lines[i]) && !/^>\s?/.test(lines[i]) && !line.trimStart().startsWith('```')) {
|
|
95
|
+
paraLines.push(lines[i]);
|
|
96
|
+
i++;
|
|
97
|
+
if (i >= lines.length) break;
|
|
98
|
+
}
|
|
99
|
+
if (paraLines.length) {
|
|
100
|
+
blocks.push(`<p>${renderInline(paraLines.join(' '))}</p>`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return blocks.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function escapeHtml(s: string): string {
|
|
108
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
109
|
+
}
|
package/src/views/AboutView.vue
CHANGED
|
@@ -5,13 +5,13 @@ import { useDsStyle } from '../utils/dataset-style';
|
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
6
|
import { langName, langLabel } from '../utils/lang';
|
|
7
7
|
|
|
8
|
-
const props = defineProps<{ registerId
|
|
8
|
+
const props = defineProps<{ registerId?: string }>();
|
|
9
9
|
|
|
10
10
|
const store = useVocabularyStore();
|
|
11
11
|
const { getColor } = useDsStyle();
|
|
12
|
-
const { loading, localError, ensureLoaded } = useDatasetLoader(() => props.registerId);
|
|
12
|
+
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
13
13
|
|
|
14
|
-
const manifest = computed(() => store.manifests.get(
|
|
14
|
+
const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
@@ -20,7 +20,7 @@ const manifest = computed(() => store.manifests.get(props.registerId));
|
|
|
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>
|
|
22
22
|
<span class="text-ink-200">/</span>
|
|
23
|
-
<router-link :to="{ name: 'dataset', params: { registerId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title ||
|
|
23
|
+
<router-link :to="{ name: 'dataset', params: { registerId: resolvedId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || resolvedId }}</router-link>
|
|
24
24
|
<span class="text-ink-200">/</span>
|
|
25
25
|
<span class="text-ink-700">About</span>
|
|
26
26
|
</nav>
|
|
@@ -109,8 +109,8 @@ const manifest = computed(() => store.manifests.get(props.registerId));
|
|
|
109
109
|
:key="tag"
|
|
110
110
|
class="badge"
|
|
111
111
|
:style="{
|
|
112
|
-
backgroundColor: getColor(
|
|
113
|
-
color: getColor(
|
|
112
|
+
backgroundColor: getColor(resolvedId) + '15',
|
|
113
|
+
color: getColor(resolvedId),
|
|
114
114
|
}"
|
|
115
115
|
>
|
|
116
116
|
{{ tag }}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, computed } from 'vue';
|
|
3
|
+
import { useRoute } from 'vue-router';
|
|
4
|
+
|
|
5
|
+
interface PageData {
|
|
6
|
+
title: string;
|
|
7
|
+
html: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const route = useRoute();
|
|
11
|
+
const slug = computed(() => (route.params.slug || route.params.page || '') as string);
|
|
12
|
+
const data = ref<PageData | null>(null);
|
|
13
|
+
const loading = ref(true);
|
|
14
|
+
const notFound = ref(false);
|
|
15
|
+
|
|
16
|
+
onMounted(async () => {
|
|
17
|
+
try {
|
|
18
|
+
const resp = await fetch(`/pages/${slug.value}.json`);
|
|
19
|
+
if (resp.ok) {
|
|
20
|
+
data.value = await resp.json();
|
|
21
|
+
} else {
|
|
22
|
+
notFound.value = true;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
notFound.value = true;
|
|
26
|
+
}
|
|
27
|
+
loading.value = false;
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
33
|
+
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
34
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
|
|
35
|
+
<span class="text-ink-200">/</span>
|
|
36
|
+
<span class="text-ink-700">{{ data?.title || slug }}</span>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<template v-if="loading">
|
|
40
|
+
<div class="animate-pulse space-y-6">
|
|
41
|
+
<div class="h-8 bg-ink-100 rounded w-48"></div>
|
|
42
|
+
<div class="h-4 bg-ink-100 rounded w-full"></div>
|
|
43
|
+
<div class="h-4 bg-ink-100 rounded w-5/6"></div>
|
|
44
|
+
<div class="h-4 bg-ink-100 rounded w-4/6"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<template v-else-if="notFound">
|
|
49
|
+
<div class="card p-8 text-center">
|
|
50
|
+
<h1 class="font-serif text-2xl text-ink-800 mb-2">Page Not Found</h1>
|
|
51
|
+
<p class="text-ink-500 mb-4">The page "{{ slug }}" does not exist.</p>
|
|
52
|
+
<router-link :to="{ name: 'home' }" class="btn-primary">Go Home</router-link>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<template v-else-if="data">
|
|
57
|
+
<h1 class="font-serif text-3xl text-ink-800 mb-6">{{ data.title }}</h1>
|
|
58
|
+
<div class="prose-page" v-html="data.html"></div>
|
|
59
|
+
</template>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
package/src/views/StatsView.vue
CHANGED
|
@@ -5,13 +5,13 @@ import { useDsStyle } from '../utils/dataset-style';
|
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
6
|
import { langName, langLabel } from '../utils/lang';
|
|
7
7
|
|
|
8
|
-
const props = defineProps<{ registerId
|
|
8
|
+
const props = defineProps<{ registerId?: string }>();
|
|
9
9
|
|
|
10
10
|
const store = useVocabularyStore();
|
|
11
11
|
const { getColor } = useDsStyle();
|
|
12
|
-
const { loading, localError, ensureLoaded } = useDatasetLoader(() => props.registerId);
|
|
12
|
+
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
13
13
|
|
|
14
|
-
const manifest = computed(() => store.manifests.get(
|
|
14
|
+
const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
15
15
|
|
|
16
16
|
interface LangStat {
|
|
17
17
|
lang: string;
|
|
@@ -49,7 +49,7 @@ const maxTerms = computed(() => Math.max(...stats.value.langs.map(l => l.terms),
|
|
|
49
49
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
50
50
|
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
|
|
51
51
|
<span class="text-ink-200">/</span>
|
|
52
|
-
<router-link :to="{ name: 'dataset', params: { registerId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title ||
|
|
52
|
+
<router-link :to="{ name: 'dataset', params: { registerId: resolvedId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || resolvedId }}</router-link>
|
|
53
53
|
<span class="text-ink-200">/</span>
|
|
54
54
|
<span class="text-ink-700">Statistics</span>
|
|
55
55
|
</nav>
|
|
@@ -105,7 +105,7 @@ const maxTerms = computed(() => Math.max(...stats.value.langs.map(l => l.terms),
|
|
|
105
105
|
class="h-full rounded-full transition-all duration-500"
|
|
106
106
|
:style="{
|
|
107
107
|
width: (s.terms / maxTerms * 100) + '%',
|
|
108
|
-
backgroundColor: getColor(
|
|
108
|
+
backgroundColor: getColor(resolvedId),
|
|
109
109
|
}"
|
|
110
110
|
></div>
|
|
111
111
|
</div>
|