@glossarist/concept-browser 0.7.54 → 0.7.56
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 +112 -20
- package/package.json +1 -1
- package/scripts/process-about-pages.mjs +145 -0
- package/src/adapters/types.ts +2 -2
- package/src/components/AppSidebar.vue +5 -2
- package/src/composables/use-color-theme.ts +1 -1
- package/src/router/index.ts +12 -0
- package/src/style.css +0 -8
- package/src/utils/color-theme.ts +4 -6
- package/src/utils/dataset-style.ts +1 -1
- package/src/views/GroupView.vue +135 -0
- package/src/components/SidebarSeriesSection.vue +0 -239
package/README.md
CHANGED
|
@@ -14,8 +14,13 @@ A statically deployable single-page application for browsing terminology dataset
|
|
|
14
14
|
- **Multi-dataset browsing** — Concepts from multiple terminology registers in one place
|
|
15
15
|
- **Full multilingual support** — Definitions, notes, and examples in all available languages with i18n UI
|
|
16
16
|
- **Concept history timeline** — Review dates, decisions, and change notes per language
|
|
17
|
+
- **Relation sphere** — 3D sphere visualization of a concept's neighborhood with d3 force simulation, per-relation-type colors, and degree filtering
|
|
18
|
+
- **Edition series** — Sidebar timeline for vocabulary edition groups (e.g. VIML 1968/2000/2013/2022) with current-edition markers and supersession chain navigation
|
|
17
19
|
- **Cross-reference graph** — D3 force-directed graph showing concept relationships with dataset filtering
|
|
18
|
-
- **
|
|
20
|
+
- **Dataset groups** — Organize datasets into lineage series, topic bundles, publication families, or curated collections. Each group kind has distinct sidebar rendering
|
|
21
|
+
- **Light/dark mode colors** — Per-dataset `{ light, dark }` color pairs. Per-relation-type colors configurable via site-config
|
|
22
|
+
- **RDF / SHACL outputs** — Every concept emits SHACL-conformant Turtle + JSON-LD. Dataset-level `dcat:Dataset`, group-level `dcat:DatasetSeries`/`dcat:Catalog`, vocabulary SKOS ConceptSchemes, bibliography `dcterms:BibliographicResource`, build provenance `prov:Activity`
|
|
23
|
+
- **Rich sidebar provenance** — Publication reference, owner, status, concept/language counts, sections tree from manifest data
|
|
19
24
|
- **Math rendering** — KaTeX rendering for AsciiMath notation in definitions (`stem:[...]`)
|
|
20
25
|
- **Responsive design** — Mobile-first layout with light/dark mode
|
|
21
26
|
- **Static deployment** — No server required. Deploy to any static host
|
|
@@ -56,10 +61,12 @@ concept-browser <command> [options]
|
|
|
56
61
|
|
|
57
62
|
Commands:
|
|
58
63
|
fetch Fetch/update datasets (from GCR packages, local paths, or source repos)
|
|
59
|
-
generate Convert harmonized YAML concepts to JSON-LD static files
|
|
64
|
+
generate Convert harmonized YAML concepts to JSON-LD static files + RDF artifacts
|
|
60
65
|
edges Build cross-reference edges from generated concept data
|
|
61
66
|
build Full pipeline: fetch + generate + edges + vite build
|
|
62
67
|
site Same as build (alias)
|
|
68
|
+
doctor Run diagnostic checks (node version, deps, shapes, data integrity)
|
|
69
|
+
normalize NFC-normalize YAML concept files in-place
|
|
63
70
|
|
|
64
71
|
Options:
|
|
65
72
|
--site <id> Site config ID (looks for site-config.yml in CWD)
|
|
@@ -78,13 +85,19 @@ Environment:
|
|
|
78
85
|
site-config.yml
|
|
79
86
|
└─> scripts/fetch-datasets.mjs (fetch from GCR, localPath, or sourceRepo)
|
|
80
87
|
└─> .datasets/{id}/concepts/*.yaml
|
|
81
|
-
└─> scripts/generate-data.mjs (YAML → JSON-LD)
|
|
88
|
+
└─> scripts/generate-data.mjs (YAML → JSON-LD + RDF artifacts)
|
|
82
89
|
└─> public/data/{id}/
|
|
83
90
|
├── manifest.json Dataset metadata (ref, owner, stats)
|
|
84
91
|
├── index.json Concept listing (chunked for large sets)
|
|
85
92
|
├── edges.json Pre-computed cross-reference + domain edges
|
|
86
93
|
├── domain-nodes.json Domain classification nodes
|
|
94
|
+
├── {register}.ttl Dataset-level RDF (dcat:Dataset + skos:ConceptScheme + sections)
|
|
95
|
+
├── bib.ttl Bibliography graph (dcterms:BibliographicResource)
|
|
87
96
|
└── concepts/*.json Individual concept documents
|
|
97
|
+
└─> public/data/_vocab.ttl Vocabulary graph (SKOS ConceptSchemes)
|
|
98
|
+
└─> public/data/activity/ Build provenance (prov:Activity per build)
|
|
99
|
+
└─> public/data/agents.ttl Contributor records (foaf:Person)
|
|
100
|
+
└─> public/data/versions.ttl Version chain (prov:Entity)
|
|
88
101
|
└─> scripts/build-edges.js (extract graph + domain edges)
|
|
89
102
|
```
|
|
90
103
|
|
|
@@ -151,7 +164,7 @@ datasets:
|
|
|
151
164
|
| `description` | no | Shown on home page and about page |
|
|
152
165
|
| `owner` | no | Organization name shown in sidebar provenance |
|
|
153
166
|
| `ref` | no | Publication reference shown in sidebar provenance (e.g., "OIML V 1:2022") |
|
|
154
|
-
| `color` | no |
|
|
167
|
+
| `color` | no | Accent color — single hex (`"#004996"`) or `{ light, dark }` pair |
|
|
155
168
|
| `tags` | no | Array of labels shown on dataset card |
|
|
156
169
|
| `languageOrder` | no | Array of ISO 639-2 codes controlling display order |
|
|
157
170
|
| `translations` | no | Localized title and description per language |
|
|
@@ -233,13 +246,17 @@ pages:
|
|
|
233
246
|
|
|
234
247
|
### Dataset groups
|
|
235
248
|
|
|
236
|
-
When a site has many datasets, you can group them in the sidebar navigation.
|
|
249
|
+
When a site has many datasets, you can group them in the sidebar navigation. Each group has a `kind` that determines its visual treatment and semantic assumptions.
|
|
237
250
|
|
|
238
251
|
```yaml
|
|
239
252
|
datasetGroups:
|
|
240
253
|
- id: viml
|
|
241
254
|
label: "VIML — International Vocabulary of Legal Metrology"
|
|
242
|
-
|
|
255
|
+
kind: lineage # edition series (same vocabulary, different years)
|
|
256
|
+
current: viml-2022 # current edition for the series
|
|
257
|
+
color:
|
|
258
|
+
light: "#004996"
|
|
259
|
+
dark: "#3B82F6"
|
|
243
260
|
datasets: [viml-2022, viml-2013, viml-2000, viml-1968]
|
|
244
261
|
translations:
|
|
245
262
|
fra:
|
|
@@ -247,22 +264,44 @@ datasetGroups:
|
|
|
247
264
|
|
|
248
265
|
- id: vim
|
|
249
266
|
label: "VIM — International Vocabulary of Metrology"
|
|
250
|
-
|
|
267
|
+
kind: lineage
|
|
268
|
+
current: vim-2012
|
|
269
|
+
color:
|
|
270
|
+
light: "#005A9C"
|
|
271
|
+
dark: "#2196F3"
|
|
251
272
|
datasets: [vim-2012, vim-2010, vim-2007, vim-1993]
|
|
252
273
|
```
|
|
253
274
|
|
|
254
|
-
####
|
|
275
|
+
#### Group kinds
|
|
276
|
+
|
|
277
|
+
| Kind | Glyph | Description | Sidebar rendering |
|
|
278
|
+
|------|-------|-------------|-------------------|
|
|
279
|
+
| `lineage` | ⏳ | Editions of the same vocabulary over time | Compact timeline with year badges + current-edition star (✦) |
|
|
280
|
+
| `topic` | ◆ | Different vocabularies on the same subject | Standard list entries |
|
|
281
|
+
| `family` | ✦ | Related vocabularies from the same publisher | Standard list entries |
|
|
282
|
+
| `collection` | ❖ | Curated bundle of datasets | Standard list entries |
|
|
283
|
+
| `default` | ▸ | No special semantics (backward compat) | Standard list entries |
|
|
284
|
+
|
|
285
|
+
For `lineage` groups:
|
|
286
|
+
- The `current` field identifies the newest valid edition. If omitted, the newest member by year is auto-detected.
|
|
287
|
+
- Members are displayed as a timeline with year + reference + status badges.
|
|
288
|
+
- When an edition is active, the full expansion (sub-pages, sections tree, provenance) appears below the timeline entry — same as non-series datasets.
|
|
289
|
+
- A supersession chain card (`ConceptEditionRail`) appears in the concept detail sidebar, showing how the concept evolved across editions.
|
|
290
|
+
|
|
291
|
+
#### Group field reference
|
|
255
292
|
|
|
256
293
|
| Field | Required | Description |
|
|
257
294
|
|-------|----------|-------------|
|
|
258
295
|
| `id` | yes | Unique identifier for the group |
|
|
259
296
|
| `label` | yes | Display name shown as the collapsible group header |
|
|
297
|
+
| `kind` | no | Group kind: `lineage`, `topic`, `family`, `collection`, `default`. Defaults to `default` |
|
|
298
|
+
| `current` | no | For `lineage`: dataset id of the current (newest valid) edition |
|
|
260
299
|
| `description` | no | Short description of the group |
|
|
261
|
-
| `color` | no |
|
|
300
|
+
| `color` | no | Accent color — single hex or `{ light, dark }` pair |
|
|
262
301
|
| `datasets` | yes | Ordered array of dataset IDs belonging to this group |
|
|
263
302
|
| `translations` | no | Localized label and description per language |
|
|
264
303
|
|
|
265
|
-
Datasets not assigned to any group appear at the bottom of the dataset list.
|
|
304
|
+
Datasets not assigned to any group appear at the bottom of the dataset list.
|
|
266
305
|
|
|
267
306
|
### Internationalization
|
|
268
307
|
|
|
@@ -288,6 +327,41 @@ datasetGroups:
|
|
|
288
327
|
|
|
289
328
|
This pattern applies everywhere localized text appears: site-level translations, dataset translations, group translations, and page translations. Do not use language suffixes like `_fra` — use nested language maps instead.
|
|
290
329
|
|
|
330
|
+
### Color system
|
|
331
|
+
|
|
332
|
+
Dataset and group colors accept either a single hex string (backward compat) or a `{ light, dark }` pair:
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
# Single hex (applied to both modes)
|
|
336
|
+
color: "#004996"
|
|
337
|
+
|
|
338
|
+
# Explicit light/dark pair
|
|
339
|
+
color:
|
|
340
|
+
light: "#004996"
|
|
341
|
+
dark: "#3B82F6"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Relationship-type colors default from `data/colors.json` but can be overridden per deployment:
|
|
345
|
+
|
|
346
|
+
```yaml
|
|
347
|
+
colors:
|
|
348
|
+
relationshipType:
|
|
349
|
+
supersedes:
|
|
350
|
+
light: "#FF0000"
|
|
351
|
+
dark: "#FF5555"
|
|
352
|
+
dataset:
|
|
353
|
+
viml-2022:
|
|
354
|
+
light: "#004996"
|
|
355
|
+
dark: "#3B82F6"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
CSS variables are emitted on document root by `useColorTheme()`:
|
|
359
|
+
- `--rel-{category}-{light|dark}` per relationship category
|
|
360
|
+
- `--rel-type-{type}-{light|dark}` per relationship type
|
|
361
|
+
- `--concept-status-{status}-{light|dark}` per concept status
|
|
362
|
+
- `--group-kind-{kind}-{light|dark}` per group kind
|
|
363
|
+
- `--ds-{light|dark}` per dataset (scoped via `[data-ds]`)
|
|
364
|
+
|
|
291
365
|
### Cross-reference mapping
|
|
292
366
|
|
|
293
367
|
```yaml
|
|
@@ -375,30 +449,39 @@ To add a new deployment here, open a PR against this README.
|
|
|
375
449
|
- **Pinia** (state management)
|
|
376
450
|
- **Vue Router** (SPA navigation)
|
|
377
451
|
- **Tailwind CSS 3** (utility-first styling)
|
|
378
|
-
- **D3.js** (force-directed graph)
|
|
452
|
+
- **D3.js** (force-directed graph + relation sphere)
|
|
379
453
|
- **KaTeX** (math rendering)
|
|
454
|
+
- **n3** + **rdf-validate-shacl** (RDF parsing + SHACL validation)
|
|
455
|
+
- **fast-check** (property-based fuzz testing)
|
|
456
|
+
- **Vitest** with happy-dom
|
|
380
457
|
|
|
381
458
|
### Project structure
|
|
382
459
|
|
|
383
460
|
```
|
|
384
461
|
src/
|
|
385
462
|
├── adapters/ Data access layer (DatasetAdapter, AdapterFactory, UriRouter)
|
|
386
|
-
├── components/ Vue components (AppSidebar,
|
|
463
|
+
├── components/ Vue components (AppSidebar, ConceptDetail, RelationSphere, etc.)
|
|
464
|
+
│ ├── concept-rdf/ RDF graph IR + emitters + writers (concept, dataset, group, vocab, etc.)
|
|
465
|
+
│ └── groups/ OCP group renderer dispatcher + per-kind sidebar components
|
|
466
|
+
├── composables/ Vue composables (useDatasetSeries, useColorTheme, useSphereProjection)
|
|
467
|
+
├── config/ Site config types, group-types registry, group-renderers registry
|
|
468
|
+
├── data/ Static data (taxonomies.json, colors.json, concept-model shapes)
|
|
387
469
|
├── graph/ Graph engine for concept relationships
|
|
388
470
|
├── stores/ Pinia stores (vocabulary, ui)
|
|
389
|
-
├── views/ Page-level components (HomeView, DatasetView, ConceptView,
|
|
471
|
+
├── views/ Page-level components (HomeView, DatasetView, ConceptView, GroupView)
|
|
390
472
|
├── i18n/ Internationalization (YAML locale files, auto-discovered)
|
|
391
|
-
|
|
392
|
-
└── style.css Global styles and Tailwind layers
|
|
473
|
+
└── utils/ Utilities (color-theme, dataset-style, relationship-categories, content-renderer)
|
|
393
474
|
|
|
394
475
|
cli/
|
|
395
|
-
└── index.mjs CLI entry point (fetch, generate, edges, build
|
|
476
|
+
└── index.mjs CLI entry point (fetch, generate, edges, build, doctor, normalize)
|
|
396
477
|
|
|
397
478
|
scripts/
|
|
398
|
-
├── fetch-datasets.mjs
|
|
399
|
-
├── generate-data.mjs
|
|
400
|
-
├── build-edges.js
|
|
401
|
-
|
|
479
|
+
├── fetch-datasets.mjs Clone + harmonize source repos
|
|
480
|
+
├── generate-data.mjs Convert YAML → JSON-LD + RDF artifacts
|
|
481
|
+
├── build-edges.js Extract cross-reference edges
|
|
482
|
+
├── process-about-pages.mjs Compile markdown/AsciiDoc about pages
|
|
483
|
+
├── validate-shacl.mjs SHACL validation gate
|
|
484
|
+
└── lib/ Build-time emitters (vocab, dataset, group, agents, version, etc.)
|
|
402
485
|
```
|
|
403
486
|
|
|
404
487
|
---
|
|
@@ -418,8 +501,17 @@ UI translations are YAML files in `src/i18n/locales/`, auto-discovered via `impo
|
|
|
418
501
|
```bash
|
|
419
502
|
npm test # Run all tests (Vitest with happy-dom)
|
|
420
503
|
npm run test:watch # Watch mode
|
|
504
|
+
npm run mutation:test # Stryker mutation testing (scoped to RDF emitters, ~3-5 min)
|
|
421
505
|
```
|
|
422
506
|
|
|
507
|
+
Test coverage includes:
|
|
508
|
+
- 1357+ tests across unit, round-trip, SHACL conformance, property-based fuzz, and build-integration layers
|
|
509
|
+
- 7-fixture concept corpus (minimal, multilingual, full-relationships, with-sources, with-non-verbal, with-dates, withdrawn)
|
|
510
|
+
- SHACL conformance gate: all fixtures validated against canonical shapes
|
|
511
|
+
- Property-based fuzz: 200 iterations × 4 invariants via fast-check
|
|
512
|
+
- 10,000-concept scale stress test (< 100ms)
|
|
513
|
+
- Stryker mutation testing scoped to concept/dataset/bibliography emitters
|
|
514
|
+
|
|
423
515
|
---
|
|
424
516
|
|
|
425
517
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.56",
|
|
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": {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// About page compiler — processes markdown/AsciiDoc about pages for
|
|
3
|
+
// datasets and groups into the public/pages/*.json format consumed
|
|
4
|
+
// by PageView.vue.
|
|
5
|
+
//
|
|
6
|
+
// Source layout:
|
|
7
|
+
// .datasets/<id>/about/about.{lang}.adoc → dataset about
|
|
8
|
+
// .datasets/<id>/about/about.{lang}.md → dataset about
|
|
9
|
+
// site-content/groups/<id>/about/about.{lang}.adoc → group about
|
|
10
|
+
//
|
|
11
|
+
// Output:
|
|
12
|
+
// public/pages/dataset-<id>-about.{lang}.json
|
|
13
|
+
// public/pages/group-<id>-about.{lang}.json
|
|
14
|
+
//
|
|
15
|
+
// Output shape: { title: string, html: string }
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
18
|
+
import { join, basename, extname } from 'node:path';
|
|
19
|
+
import { cwd } from 'node:process';
|
|
20
|
+
|
|
21
|
+
const ROOT = cwd();
|
|
22
|
+
const PUBLIC_PAGES = join(ROOT, 'public', 'pages');
|
|
23
|
+
const DATASETS_DIR = join(ROOT, '.datasets');
|
|
24
|
+
const GROUPS_CONTENT_DIR = join(ROOT, 'site-content', 'groups');
|
|
25
|
+
|
|
26
|
+
function renderMarkdown(text) {
|
|
27
|
+
// Minimal markdown → HTML. For production, use a real parser.
|
|
28
|
+
// This handles headings, paragraphs, bold, italic, links, lists.
|
|
29
|
+
const lines = text.split('\n');
|
|
30
|
+
const html = [];
|
|
31
|
+
let inList = false;
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed) {
|
|
36
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (/^#{1}\s/.test(trimmed)) {
|
|
40
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
41
|
+
html.push(`<h1>${inline(trimmed.replace(/^#\s/, ''))}</h1>`);
|
|
42
|
+
} else if (/^#{2}\s/.test(trimmed)) {
|
|
43
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
44
|
+
html.push(`<h2>${inline(trimmed.replace(/^##\s/, ''))}</h2>`);
|
|
45
|
+
} else if (/^#{3}\s/.test(trimmed)) {
|
|
46
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
47
|
+
html.push(`<h3>${inline(trimmed.replace(/^###\s/, ''))}</h3>`);
|
|
48
|
+
} else if (/^[-*]\s/.test(trimmed)) {
|
|
49
|
+
if (!inList) { html.push('<ul>'); inList = true; }
|
|
50
|
+
html.push(`<li>${inline(trimmed.replace(/^[-*]\s/, ''))}</li>`);
|
|
51
|
+
} else {
|
|
52
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
53
|
+
html.push(`<p>${inline(trimmed)}</p>`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (inList) html.push('</ul>');
|
|
57
|
+
return html.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inline(text) {
|
|
61
|
+
return text
|
|
62
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
63
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
64
|
+
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')
|
|
65
|
+
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function processAboutDir(sourceDir, outputPrefix) {
|
|
69
|
+
if (!existsSync(sourceDir)) return 0;
|
|
70
|
+
let count = 0;
|
|
71
|
+
|
|
72
|
+
for (const file of readdirSync(sourceDir)) {
|
|
73
|
+
const fullPath = join(sourceDir, file);
|
|
74
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
75
|
+
|
|
76
|
+
const ext = extname(file);
|
|
77
|
+
if (ext !== '.md' && ext !== '.adoc' && ext !== '.html') continue;
|
|
78
|
+
|
|
79
|
+
// Parse filename: about.{lang}.md or about.md
|
|
80
|
+
const base = basename(file, ext);
|
|
81
|
+
const langMatch = base.match(/^about\.(\w+)$/);
|
|
82
|
+
const lang = langMatch ? langMatch[1] : 'eng';
|
|
83
|
+
const outputName = lang === 'eng'
|
|
84
|
+
? `${outputPrefix}.json`
|
|
85
|
+
: `${outputPrefix}.${lang}.json`;
|
|
86
|
+
|
|
87
|
+
const raw = readFileSync(fullPath, 'utf8');
|
|
88
|
+
let html;
|
|
89
|
+
let title;
|
|
90
|
+
|
|
91
|
+
if (ext === '.html') {
|
|
92
|
+
html = raw;
|
|
93
|
+
const titleMatch = raw.match(/<h1[^>]*>(.+?)<\/h1>/);
|
|
94
|
+
title = titleMatch ? titleMatch[1] : 'About';
|
|
95
|
+
} else if (ext === '.md') {
|
|
96
|
+
html = renderMarkdown(raw);
|
|
97
|
+
const titleMatch = raw.match(/^#\s+(.+)$/m);
|
|
98
|
+
title = titleMatch ? titleMatch[1] : 'About';
|
|
99
|
+
} else {
|
|
100
|
+
// AsciiDoc — treat as plain text for now (production: use asciidoctor.js)
|
|
101
|
+
html = `<p>${raw.split('\n').filter(l => l.trim() && !l.startsWith('=')).join('</p>\n<p>')}</p>`;
|
|
102
|
+
const titleMatch = raw.match(/^=\s+(.+)$/m);
|
|
103
|
+
title = titleMatch ? titleMatch[1] : 'About';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const output = { title, html };
|
|
107
|
+
mkdirSync(PUBLIC_PAGES, { recursive: true });
|
|
108
|
+
writeFileSync(join(PUBLIC_PAGES, outputName), JSON.stringify(output));
|
|
109
|
+
count++;
|
|
110
|
+
console.log(` Compiled: ${file} → public/pages/${outputName}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function main() {
|
|
117
|
+
mkdirSync(PUBLIC_PAGES, { recursive: true });
|
|
118
|
+
let total = 0;
|
|
119
|
+
|
|
120
|
+
// Dataset about pages
|
|
121
|
+
if (existsSync(DATASETS_DIR)) {
|
|
122
|
+
for (const dsId of readdirSync(DATASETS_DIR)) {
|
|
123
|
+
const aboutDir = join(DATASETS_DIR, dsId, 'about');
|
|
124
|
+
if (existsSync(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
125
|
+
console.log(`Processing dataset: ${dsId}`);
|
|
126
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Group about pages
|
|
132
|
+
if (existsSync(GROUPS_CONTENT_DIR)) {
|
|
133
|
+
for (const groupId of readdirSync(GROUPS_CONTENT_DIR)) {
|
|
134
|
+
const aboutDir = join(GROUPS_CONTENT_DIR, groupId, 'about');
|
|
135
|
+
if (existsSync(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
136
|
+
console.log(`Processing group: ${groupId}`);
|
|
137
|
+
total += processAboutDir(aboutDir, `group-${groupId}-about`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(total > 0 ? `\nCompiled ${total} about page(s).` : '\nNo about pages found.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main();
|
package/src/adapters/types.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface Manifest {
|
|
|
64
64
|
lastUpdated: string;
|
|
65
65
|
sourceRepo: string;
|
|
66
66
|
chunkSize: number;
|
|
67
|
-
color?: string;
|
|
67
|
+
color?: string | { light: string; dark: string };
|
|
68
68
|
shortname?: string;
|
|
69
69
|
languageOrder?: string[];
|
|
70
70
|
ref?: string;
|
|
@@ -109,7 +109,7 @@ export interface DatasetSummary {
|
|
|
109
109
|
languages: string[];
|
|
110
110
|
owner: string;
|
|
111
111
|
tags: string[];
|
|
112
|
-
color?: string;
|
|
112
|
+
color?: string | { light: string; dark: string };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export interface DatasetRegistry {
|
|
@@ -12,7 +12,7 @@ import { toSectionTree } from '../utils/section-tree';
|
|
|
12
12
|
import { formatSectionLabel, sectionName as sectionLocalized } from '../utils/section-display';
|
|
13
13
|
|
|
14
14
|
const OntologySidebarSection = defineAsyncComponent(() => import('./OntologySidebarSection.vue'));
|
|
15
|
-
import { resolveGroupKind } from '../config/group-types';
|
|
15
|
+
import { resolveGroupKind, groupTypeMeta } from '../config/group-types';
|
|
16
16
|
import type { DatasetGroupKind } from '../config/types';
|
|
17
17
|
import { useDatasetSeries } from '../composables/useDatasetSeries';
|
|
18
18
|
const useDatasetSeriesRef = () => useDatasetSeries().series;
|
|
@@ -271,14 +271,17 @@ const activeSectionId = computed(() => {
|
|
|
271
271
|
<!-- Grouped datasets -->
|
|
272
272
|
<template v-if="hasGroups">
|
|
273
273
|
<div v-for="group in groupedDatasetEntries" :key="group.id" class="mb-2">
|
|
274
|
-
<!-- Group header (skip for ungrouped) -->
|
|
274
|
+
<!-- Group header (skip for ungrouped) — shows kind glyph + accent -->
|
|
275
275
|
<button
|
|
276
276
|
v-if="group.label"
|
|
277
277
|
@click="toggleGroup(group.id)"
|
|
278
278
|
class="sidebar-group-label w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold transition-colors hover:bg-ink-50 dark:hover:bg-ink-700/60"
|
|
279
|
+
:title="groupTypeMeta(group).description"
|
|
279
280
|
>
|
|
280
281
|
<span class="w-3 text-[10px] mt-0.5 flex-shrink-0 text-ink-300 dark:text-ink-400">{{ isGroupExpanded(group.id) ? '▾' : '▸' }}</span>
|
|
282
|
+
<span class="flex-shrink-0 text-sm" :style="{ color: `var(--group-kind-${group.kind}-light, var(--group-kind-default-light, #6B6E7D))` }">{{ groupTypeMeta(group).glyph }}</span>
|
|
281
283
|
<span class="flex-1 text-left leading-snug text-ink-700 dark:text-ink-200 font-serif">{{ group.label }}</span>
|
|
284
|
+
<span class="text-[9px] uppercase tracking-wide text-ink-300 dark:text-ink-500 font-sans">{{ groupTypeMeta(group).label }}</span>
|
|
282
285
|
</button>
|
|
283
286
|
|
|
284
287
|
<!-- Group entries -->
|
|
@@ -59,7 +59,7 @@ export function useColorTheme(): void {
|
|
|
59
59
|
const theme = createColorTheme(siteColors);
|
|
60
60
|
|
|
61
61
|
for (const ds of store.datasetList) {
|
|
62
|
-
const declared = ds.manifest?.color
|
|
62
|
+
const declared = ds.manifest?.color;
|
|
63
63
|
const pair = theme.datasetColor(ds.id, declared);
|
|
64
64
|
const scopeId = `ds-color-scope-${ds.id.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
65
65
|
let scope = document.getElementById(scopeId);
|
package/src/router/index.ts
CHANGED
|
@@ -31,6 +31,18 @@ export const routes: RouteRecordRaw[] = [
|
|
|
31
31
|
component: () => import('../views/PageView.vue'),
|
|
32
32
|
props: true,
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
path: '/group/:groupId',
|
|
36
|
+
name: 'group',
|
|
37
|
+
component: () => import('../views/GroupView.vue'),
|
|
38
|
+
props: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: '/group/:groupId/about',
|
|
42
|
+
name: 'group-about',
|
|
43
|
+
component: () => import('../views/PageView.vue'),
|
|
44
|
+
props: true,
|
|
45
|
+
},
|
|
34
46
|
{
|
|
35
47
|
path: '/search',
|
|
36
48
|
name: 'search',
|
package/src/style.css
CHANGED
|
@@ -480,14 +480,6 @@
|
|
|
480
480
|
.dark .bg-red-50 { background-color: rgba(239, 68, 68, 0.15) !important; }
|
|
481
481
|
.dark .text-red-600 { color: #fca5a5 !important; }
|
|
482
482
|
|
|
483
|
-
/* ── Sidebar group labels (brand colors are too dark in dark mode) ── */
|
|
484
|
-
.dark .sidebar-group-label {
|
|
485
|
-
filter: brightness(1.6);
|
|
486
|
-
}
|
|
487
|
-
.dark .sidebar-group-label:hover {
|
|
488
|
-
filter: brightness(1.4);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
483
|
/* ── Gold accent that adapts to theme ── */
|
|
492
484
|
:root {
|
|
493
485
|
--gold-accent: #B8935A;
|
package/src/utils/color-theme.ts
CHANGED
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
* Pure data + pure accessors — no Vue reactivity. Reactive consumption
|
|
6
6
|
* is via the `useColorTheme()` composable.
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
9
|
-
import { join, dirname } from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import colorsJson from '../../data/colors.json';
|
|
11
9
|
import type { DatasetColorSpec, SiteColors } from '../config/types';
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
const COLORS_PATH = join(__dirname, '..', '..', 'data', 'colors.json');
|
|
11
|
+
const COLORS_PATH_RESOLVED = true; /* marker so the import isn't tree-shaken */
|
|
15
12
|
|
|
16
13
|
export interface ColorPair {
|
|
17
14
|
readonly light: string;
|
|
@@ -29,7 +26,8 @@ let cachedDefaults: ColorDefaults | undefined;
|
|
|
29
26
|
|
|
30
27
|
function loadDefaults(): ColorDefaults {
|
|
31
28
|
if (cachedDefaults) return cachedDefaults;
|
|
32
|
-
const raw =
|
|
29
|
+
const raw = colorsJson as any;
|
|
30
|
+
void COLORS_PATH_RESOLVED; /* keep the import meaningful for reviewers */
|
|
33
31
|
cachedDefaults = {
|
|
34
32
|
relationshipCategory: raw.relationshipCategory,
|
|
35
33
|
relationshipType: raw.relationshipType,
|
|
@@ -64,7 +64,7 @@ export function useDsStyle() {
|
|
|
64
64
|
|
|
65
65
|
const store = useVocabularyStore();
|
|
66
66
|
const ds = store.datasetList.find(d => d.id === registerId);
|
|
67
|
-
const declared = ds?.manifest?.color
|
|
67
|
+
const declared = ds?.manifest?.color;
|
|
68
68
|
const fallback = paletteColor(store.datasetList.findIndex(d => d.id === registerId));
|
|
69
69
|
const style = makeDsStyle(declared, fallback);
|
|
70
70
|
cache.set(registerId, style);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* GroupView — overview page for a dataset group (lineage series,
|
|
4
|
+
* topic bundle, family, collection). Renders authored about content
|
|
5
|
+
* when available, otherwise generates from group metadata + members.
|
|
6
|
+
*
|
|
7
|
+
* Route: /group/:groupId
|
|
8
|
+
*/
|
|
9
|
+
import { ref, computed, onMounted, watch } from 'vue';
|
|
10
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
11
|
+
import { useSiteConfig } from '../config/use-site-config';
|
|
12
|
+
import { resolveGroupKind, groupTypeMeta } from '../config/group-types';
|
|
13
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
14
|
+
|
|
15
|
+
const route = useRoute();
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const { datasetGroups } = useSiteConfig();
|
|
18
|
+
const store = useVocabularyStore();
|
|
19
|
+
|
|
20
|
+
const groupId = computed(() => route.params.groupId as string);
|
|
21
|
+
|
|
22
|
+
const group = computed(() => {
|
|
23
|
+
return datasetGroups.value?.find(g => g.id === groupId.value);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const groupKind = computed(() => group.value ? resolveGroupKind(group.value) : 'default');
|
|
27
|
+
const groupMeta = computed(() => group.value ? groupTypeMeta(group.value) : groupTypeMeta({}));
|
|
28
|
+
|
|
29
|
+
const loading = ref(true);
|
|
30
|
+
const aboutHtml = ref<string | null>(null);
|
|
31
|
+
const aboutTitle = ref<string>('');
|
|
32
|
+
|
|
33
|
+
async function fetchAbout() {
|
|
34
|
+
loading.value = true;
|
|
35
|
+
aboutHtml.value = null;
|
|
36
|
+
|
|
37
|
+
const base = import.meta.env.BASE_URL;
|
|
38
|
+
const candidates = [
|
|
39
|
+
`${base}pages/group-${groupId.value}-about.json`,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const url of candidates) {
|
|
43
|
+
try {
|
|
44
|
+
const resp = await fetch(url);
|
|
45
|
+
if (resp.ok) {
|
|
46
|
+
const data = await resp.json();
|
|
47
|
+
aboutTitle.value = data.title || group.value?.label || groupId.value;
|
|
48
|
+
aboutHtml.value = data.html;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
} catch { /* try next */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
loading.value = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMounted(fetchAbout);
|
|
58
|
+
watch(groupId, fetchAbout);
|
|
59
|
+
|
|
60
|
+
const members = computed(() => {
|
|
61
|
+
if (!group.value) return [];
|
|
62
|
+
return group.value.datasets.map(id => {
|
|
63
|
+
const m = store.manifests.get(id);
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
title: m?.title ?? id,
|
|
67
|
+
ref: m?.ref ?? id,
|
|
68
|
+
conceptCount: m?.conceptCount ?? 0,
|
|
69
|
+
loaded: !!m,
|
|
70
|
+
status: m?.status ?? 'unknown',
|
|
71
|
+
year: m?.ref ? parseInt(m.ref.match(/(\d{4})$/)?.[1] ?? '0', 10) || undefined : undefined,
|
|
72
|
+
};
|
|
73
|
+
}).sort((a, b) => (a.year ?? 0) - (b.year ?? 0));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function openDataset(id: string) {
|
|
77
|
+
router.push({ name: 'dataset', params: { registerId: id } });
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
83
|
+
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
84
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700">Home</router-link>
|
|
85
|
+
<span class="text-ink-200">/</span>
|
|
86
|
+
<span class="text-ink-700">{{ group?.label ?? groupId }}</span>
|
|
87
|
+
</nav>
|
|
88
|
+
|
|
89
|
+
<template v-if="loading">
|
|
90
|
+
<div class="animate-pulse space-y-4">
|
|
91
|
+
<div class="h-8 bg-ink-100 rounded w-64"></div>
|
|
92
|
+
<div class="card p-6"><div class="h-48 bg-ink-50 rounded"></div></div>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<template v-else-if="group">
|
|
97
|
+
<div class="flex items-baseline gap-3 mb-2">
|
|
98
|
+
<span class="text-2xl">{{ groupMeta.glyph }}</span>
|
|
99
|
+
<h1 class="font-serif text-3xl text-ink-800 dark:text-ink-50">
|
|
100
|
+
{{ group.label }}
|
|
101
|
+
</h1>
|
|
102
|
+
</div>
|
|
103
|
+
<p v-if="group.description" class="text-ink-500 dark:text-ink-400 mb-6">{{ group.description }}</p>
|
|
104
|
+
|
|
105
|
+
<div v-if="aboutHtml" class="card p-6 mb-6 prose-page" v-html="aboutHtml"></div>
|
|
106
|
+
|
|
107
|
+
<h2 class="section-label mb-3">Members ({{ members.length }})</h2>
|
|
108
|
+
<div class="space-y-2">
|
|
109
|
+
<button
|
|
110
|
+
v-for="m in members"
|
|
111
|
+
:key="m.id"
|
|
112
|
+
type="button"
|
|
113
|
+
class="w-full text-left card p-4 flex items-center gap-4 hover:border-ink-300 transition-colors"
|
|
114
|
+
@click="openDataset(m.id)"
|
|
115
|
+
>
|
|
116
|
+
<div class="flex-1 min-w-0">
|
|
117
|
+
<div class="font-medium truncate">{{ m.title }}</div>
|
|
118
|
+
<div v-if="m.loaded" class="text-xs text-ink-400 mt-0.5">
|
|
119
|
+
{{ m.conceptCount.toLocaleString() }} concepts · {{ m.status }}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<span v-if="m.year" class="font-mono text-sm text-ink-400 flex-shrink-0">{{ m.year }}</span>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<template v-else>
|
|
128
|
+
<div class="card p-8 text-center">
|
|
129
|
+
<h1 class="font-serif text-2xl text-ink-800 mb-2">Group not found</h1>
|
|
130
|
+
<p class="text-ink-500 mb-4">No group with id "{{ groupId }}" is configured.</p>
|
|
131
|
+
<router-link :to="{ name: 'home' }" class="btn-primary">Go home</router-link>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SidebarSeriesSection — compact version of the dataset series list for the
|
|
4
|
-
* AppSidebar. Shows all multi-edition series as collapsible groups with their
|
|
5
|
-
* editions as clickable items.
|
|
6
|
-
*
|
|
7
|
-
* Designed to fit the sidebar's existing visual language: small text, gold
|
|
8
|
-
* accents, ink-100 borders. Uses Tailwind dark: classes for theme switching.
|
|
9
|
-
*/
|
|
10
|
-
import { computed, ref } from 'vue';
|
|
11
|
-
import { useRouter } from 'vue-router';
|
|
12
|
-
import { useDatasetSeries } from '../composables/useDatasetSeries';
|
|
13
|
-
|
|
14
|
-
const router = useRouter();
|
|
15
|
-
const { series } = useDatasetSeries();
|
|
16
|
-
|
|
17
|
-
const multiEditionSeries = computed(() =>
|
|
18
|
-
series.value.filter(s => s.members.length > 1)
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
/* Collapse state — start expanded for the active series, collapsed otherwise */
|
|
22
|
-
const collapsed = ref<Set<string>>(new Set());
|
|
23
|
-
function toggle(key: string) {
|
|
24
|
-
const next = new Set(collapsed.value);
|
|
25
|
-
if (next.has(key)) next.delete(key);
|
|
26
|
-
else next.add(key);
|
|
27
|
-
collapsed.value = next;
|
|
28
|
-
}
|
|
29
|
-
function isCollapsed(key: string) {
|
|
30
|
-
return collapsed.value.has(key);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function navigate(registerId: string) {
|
|
34
|
-
router.push({ name: 'dataset', params: { registerId } });
|
|
35
|
-
}
|
|
36
|
-
</script>
|
|
37
|
-
|
|
38
|
-
<template>
|
|
39
|
-
<div v-if="multiEditionSeries.length" class="sidebar-series">
|
|
40
|
-
<div class="section-label">Edition Series</div>
|
|
41
|
-
<div class="series-list">
|
|
42
|
-
<div v-for="s in multiEditionSeries" :key="s.key" class="series-block">
|
|
43
|
-
<button
|
|
44
|
-
class="series-header"
|
|
45
|
-
@click="toggle(s.key)"
|
|
46
|
-
>
|
|
47
|
-
<span class="series-chevron">{{ isCollapsed(s.key) ? '▸' : '▾' }}</span>
|
|
48
|
-
<span class="series-title">{{ s.title }}</span>
|
|
49
|
-
<span class="series-count">{{ s.members.length }}</span>
|
|
50
|
-
</button>
|
|
51
|
-
<ol v-if="!isCollapsed(s.key)" class="series-editions">
|
|
52
|
-
<li
|
|
53
|
-
v-for="member in [...s.members].reverse()"
|
|
54
|
-
:key="member.id"
|
|
55
|
-
:class="['edition-row', { active: member.isActive, current: member.isCurrent }]"
|
|
56
|
-
>
|
|
57
|
-
<button class="edition-button" @click="navigate(member.id)">
|
|
58
|
-
<span class="edition-year">{{ member.year ?? '—' }}</span>
|
|
59
|
-
<span class="edition-meta">
|
|
60
|
-
<span v-if="member.isActive" class="edition-mark">●</span>
|
|
61
|
-
<span v-else-if="member.isCurrent" class="edition-mark current">◆</span>
|
|
62
|
-
</span>
|
|
63
|
-
</button>
|
|
64
|
-
</li>
|
|
65
|
-
</ol>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</template>
|
|
70
|
-
|
|
71
|
-
<style scoped>
|
|
72
|
-
.sidebar-series {
|
|
73
|
-
margin-bottom: 1.5rem;
|
|
74
|
-
}
|
|
75
|
-
.section-label {
|
|
76
|
-
font-size: 10px;
|
|
77
|
-
font-weight: 700;
|
|
78
|
-
text-transform: uppercase;
|
|
79
|
-
letter-spacing: 0.18em;
|
|
80
|
-
color: theme('colors.ink.300');
|
|
81
|
-
margin-bottom: 0.5rem;
|
|
82
|
-
}
|
|
83
|
-
:global(.dark) .section-label {
|
|
84
|
-
color: theme('colors.ink.400');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.series-list {
|
|
88
|
-
display: flex;
|
|
89
|
-
flex-direction: column;
|
|
90
|
-
gap: 0.25rem;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.series-block {
|
|
94
|
-
border-radius: 6px;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.series-header {
|
|
98
|
-
display: flex;
|
|
99
|
-
align-items: center;
|
|
100
|
-
gap: 0.375rem;
|
|
101
|
-
width: 100%;
|
|
102
|
-
padding: 0.375rem 0.5rem;
|
|
103
|
-
background: transparent;
|
|
104
|
-
border: none;
|
|
105
|
-
border-radius: 4px;
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
font-family: inherit;
|
|
108
|
-
text-align: left;
|
|
109
|
-
color: theme('colors.ink.700');
|
|
110
|
-
font-size: 12px;
|
|
111
|
-
font-weight: 600;
|
|
112
|
-
transition: background 0.15s;
|
|
113
|
-
}
|
|
114
|
-
.series-header:hover {
|
|
115
|
-
background: theme('colors.ink.50');
|
|
116
|
-
}
|
|
117
|
-
:global(.dark) .series-header {
|
|
118
|
-
color: theme('colors.ink.200');
|
|
119
|
-
}
|
|
120
|
-
:global(.dark) .series-header:hover {
|
|
121
|
-
background: theme('colors.ink.700');
|
|
122
|
-
}
|
|
123
|
-
.series-chevron {
|
|
124
|
-
font-size: 10px;
|
|
125
|
-
width: 0.625rem;
|
|
126
|
-
color: theme('colors.ink.300');
|
|
127
|
-
}
|
|
128
|
-
.series-title {
|
|
129
|
-
flex: 1;
|
|
130
|
-
overflow: hidden;
|
|
131
|
-
text-overflow: ellipsis;
|
|
132
|
-
white-space: nowrap;
|
|
133
|
-
font-family: 'DM Serif Display', Georgia, serif;
|
|
134
|
-
font-size: 13px;
|
|
135
|
-
font-weight: 400;
|
|
136
|
-
letter-spacing: -0.005em;
|
|
137
|
-
}
|
|
138
|
-
.series-count {
|
|
139
|
-
font-family: 'JetBrains Mono', monospace;
|
|
140
|
-
font-size: 10px;
|
|
141
|
-
color: theme('colors.ink.300');
|
|
142
|
-
background: theme('colors.ink.50');
|
|
143
|
-
padding: 1px 6px;
|
|
144
|
-
border-radius: 8px;
|
|
145
|
-
font-weight: 600;
|
|
146
|
-
}
|
|
147
|
-
:global(.dark) .series-count {
|
|
148
|
-
background: theme('colors.ink.700');
|
|
149
|
-
color: theme('colors.ink.300');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.series-editions {
|
|
153
|
-
list-style: none;
|
|
154
|
-
padding: 0 0 0 0.625rem;
|
|
155
|
-
margin: 0;
|
|
156
|
-
display: flex;
|
|
157
|
-
flex-direction: column;
|
|
158
|
-
gap: 0;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.edition-row {
|
|
162
|
-
position: relative;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.edition-button {
|
|
166
|
-
display: flex;
|
|
167
|
-
align-items: center;
|
|
168
|
-
gap: 0.375rem;
|
|
169
|
-
width: 100%;
|
|
170
|
-
padding: 0.25rem 0.5rem 0.25rem 0.875rem;
|
|
171
|
-
background: transparent;
|
|
172
|
-
border: none;
|
|
173
|
-
border-radius: 4px;
|
|
174
|
-
cursor: pointer;
|
|
175
|
-
font-family: inherit;
|
|
176
|
-
text-align: left;
|
|
177
|
-
color: theme('colors.ink.500');
|
|
178
|
-
font-size: 11px;
|
|
179
|
-
transition: all 0.15s;
|
|
180
|
-
position: relative;
|
|
181
|
-
}
|
|
182
|
-
.edition-button::before {
|
|
183
|
-
content: '';
|
|
184
|
-
position: absolute;
|
|
185
|
-
left: 0;
|
|
186
|
-
top: 50%;
|
|
187
|
-
transform: translateY(-50%);
|
|
188
|
-
width: 4px;
|
|
189
|
-
height: 4px;
|
|
190
|
-
border-radius: 50%;
|
|
191
|
-
background: theme('colors.ink.200');
|
|
192
|
-
}
|
|
193
|
-
.edition-button:hover {
|
|
194
|
-
background: theme('colors.ink.50');
|
|
195
|
-
color: theme('colors.ink.800');
|
|
196
|
-
}
|
|
197
|
-
:global(.dark) .edition-button {
|
|
198
|
-
color: theme('colors.ink.400');
|
|
199
|
-
}
|
|
200
|
-
:global(.dark) .edition-button:hover {
|
|
201
|
-
background: theme('colors.ink.700');
|
|
202
|
-
color: theme('colors.ink.100');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.edition-row.active .edition-button {
|
|
206
|
-
color: #B8935A;
|
|
207
|
-
font-weight: 600;
|
|
208
|
-
background: rgba(184, 147, 90, 0.08);
|
|
209
|
-
}
|
|
210
|
-
.edition-row.active .edition-button::before {
|
|
211
|
-
background: #B8935A;
|
|
212
|
-
width: 5px;
|
|
213
|
-
height: 5px;
|
|
214
|
-
box-shadow: 0 0 0 2px rgba(184, 147, 90, 0.20);
|
|
215
|
-
}
|
|
216
|
-
.edition-row.current:not(.active) .edition-button::before {
|
|
217
|
-
background: #B8935A;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.edition-year {
|
|
221
|
-
font-family: 'JetBrains Mono', monospace;
|
|
222
|
-
font-size: 11px;
|
|
223
|
-
font-weight: 500;
|
|
224
|
-
letter-spacing: 0.02em;
|
|
225
|
-
}
|
|
226
|
-
.edition-meta {
|
|
227
|
-
margin-left: auto;
|
|
228
|
-
display: flex;
|
|
229
|
-
align-items: center;
|
|
230
|
-
}
|
|
231
|
-
.edition-mark {
|
|
232
|
-
font-size: 7px;
|
|
233
|
-
color: theme('colors.ink.300');
|
|
234
|
-
}
|
|
235
|
-
.edition-mark.current {
|
|
236
|
-
color: #B8935A;
|
|
237
|
-
font-size: 9px;
|
|
238
|
-
}
|
|
239
|
-
</style>
|