@glossarist/concept-browser 0.7.54 → 0.7.55

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.7.54",
3
+ "version": "0.7.55",
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();
@@ -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 as DatasetColorSpec | undefined;
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);
@@ -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;
@@ -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 as DatasetColorSpec | undefined;
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>