@glossarist/concept-browser 0.7.51 → 0.7.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/cli/index.mjs +32 -0
  2. package/env.d.ts +15 -0
  3. package/package.json +12 -2
  4. package/scripts/__tests__/doctor.test.mjs +147 -0
  5. package/scripts/doctor.mjs +327 -0
  6. package/scripts/generate-data.mjs +136 -0
  7. package/scripts/generate-ontology-data.mjs +3 -3
  8. package/scripts/generate-ontology-schema.mjs +3 -3
  9. package/scripts/lib/agents-turtle.mjs +64 -0
  10. package/scripts/lib/bibliography-turtle.mjs +54 -0
  11. package/scripts/lib/build-activity-turtle.mjs +92 -0
  12. package/scripts/lib/build-cache.mjs +70 -0
  13. package/scripts/lib/dataset-turtle.mjs +79 -0
  14. package/scripts/lib/turtle-escape.mjs +0 -0
  15. package/scripts/lib/version-turtle.mjs +56 -0
  16. package/scripts/lib/vocab-turtle.mjs +64 -0
  17. package/scripts/normalize-yaml.mjs +99 -0
  18. package/scripts/sync-concept-model.mjs +86 -0
  19. package/scripts/validate-shacl.mjs +194 -0
  20. package/src/App.vue +2 -0
  21. package/src/__fixtures__/concept-shape.ttl +20 -0
  22. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  23. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  24. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  25. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  26. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  27. package/src/__tests__/components/error-boundary.test.ts +109 -0
  28. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  29. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  30. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  31. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  32. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  33. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  34. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  35. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  36. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  37. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  38. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  39. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  40. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  41. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  43. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  44. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  45. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  46. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  47. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  48. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  49. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  50. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  51. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  52. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  53. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  54. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  55. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  56. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  57. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  58. package/src/__tests__/config/group-renderers.test.ts +35 -0
  59. package/src/__tests__/config/group-types.test.ts +76 -0
  60. package/src/__tests__/dataset-style.test.ts +12 -7
  61. package/src/__tests__/errors/errors.test.ts +142 -0
  62. package/src/__tests__/format-downloads.test.ts +47 -65
  63. package/src/__tests__/markdown-lite.test.ts +19 -0
  64. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  65. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  66. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  67. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  68. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  69. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  70. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  71. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  72. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  73. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  74. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  75. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  76. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  77. package/src/__tests__/use-format-registry.test.ts +125 -0
  78. package/src/__tests__/utils/bcp47.test.ts +166 -0
  79. package/src/__tests__/utils/color-theme.test.ts +143 -0
  80. package/src/__tests__/utils/url-safety.test.ts +65 -0
  81. package/src/__tests__/validate-shacl.test.ts +100 -0
  82. package/src/adapters/DatasetAdapter.ts +11 -5
  83. package/src/adapters/GraphDataSource.ts +2 -1
  84. package/src/adapters/UriRouter.ts +2 -1
  85. package/src/adapters/concept-identity.ts +69 -0
  86. package/src/adapters/factory.ts +3 -2
  87. package/src/adapters/model-bridge.ts +2 -1
  88. package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
  89. package/src/adapters/non-verbal-resolver.ts +2 -1
  90. package/src/components/AppSidebar.vue +189 -93
  91. package/src/components/ConceptDetail.vue +8 -0
  92. package/src/components/ConceptEditionRail.vue +222 -0
  93. package/src/components/ConceptRdfView.vue +37 -377
  94. package/src/components/DatasetSeriesCard.vue +270 -0
  95. package/src/components/ErrorBoundary.vue +95 -0
  96. package/src/components/FormatDownloads.vue +17 -13
  97. package/src/components/HomeSeriesSection.vue +277 -0
  98. package/src/components/RelationSphere.vue +1672 -0
  99. package/src/components/SidebarSeriesSection.vue +239 -0
  100. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  101. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  102. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  103. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  104. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  105. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  106. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  107. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  108. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  109. package/src/components/concept-rdf/group-emitter.ts +69 -0
  110. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  111. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  112. package/src/components/concept-rdf/predicates.ts +261 -0
  113. package/src/components/concept-rdf/provenance.ts +80 -0
  114. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  115. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  116. package/src/components/concept-rdf/sections-builder.ts +62 -0
  117. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  118. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  119. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  120. package/src/components/concept-rdf/version-emitter.ts +65 -0
  121. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  122. package/src/components/groups/DatasetGroupRenderer.vue +32 -0
  123. package/src/components/groups/DefaultGroupSidebar.vue +50 -0
  124. package/src/components/groups/LineageGroupSidebar.vue +75 -0
  125. package/src/composables/use-color-theme.ts +82 -0
  126. package/src/composables/use-format-registry.ts +42 -0
  127. package/src/composables/useDatasetSeries.ts +258 -0
  128. package/src/composables/useSphereProjection.ts +125 -0
  129. package/src/config/group-renderers.ts +27 -0
  130. package/src/config/group-types.ts +92 -0
  131. package/src/config/types.ts +81 -2
  132. package/src/config/use-site-config.ts +2 -1
  133. package/src/errors.ts +136 -0
  134. package/src/i18n/locales/eng.yml +24 -0
  135. package/src/i18n/locales/fra.yml +24 -0
  136. package/src/stores/vocabulary.ts +3 -1
  137. package/src/style.css +17 -2
  138. package/src/types/agents-version-turtle.d.ts +27 -0
  139. package/src/types/bibliography-turtle.d.ts +12 -0
  140. package/src/types/build-activity-turtle.d.ts +16 -0
  141. package/src/types/build-cache.d.ts +20 -0
  142. package/src/types/dataset-turtle.d.ts +32 -0
  143. package/src/types/normalize-yaml.d.ts +16 -0
  144. package/src/types/turtle-escape.d.ts +6 -0
  145. package/src/types/vocab-turtle.d.ts +13 -0
  146. package/src/utils/asciidoc-lite.ts +11 -6
  147. package/src/utils/bcp47.ts +141 -0
  148. package/src/utils/color-theme-integration.ts +11 -0
  149. package/src/utils/color-theme.ts +129 -0
  150. package/src/utils/dataset-style.ts +31 -6
  151. package/src/utils/locale.ts +6 -14
  152. package/src/utils/markdown-lite.ts +6 -1
  153. package/src/utils/relation-sphere-styling.ts +63 -0
  154. package/src/utils/relationship-categories.ts +30 -0
  155. package/src/utils/url-safety.ts +30 -0
  156. package/src/views/ConceptView.vue +187 -9
  157. package/src/views/DatasetView.vue +6 -0
  158. package/src/views/HomeView.vue +5 -0
  159. package/vite.config.ts +7 -0
@@ -0,0 +1,270 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * DatasetSeriesCard — sidebar widget showing all editions of the same
4
+ * vocabulary series as the current dataset. Click any edition to navigate.
5
+ *
6
+ * Plugged into the DatasetView main column so users browsing viml-2022 see the
7
+ * 1968 / 2000 / 2013 / 2022 family at a glance. Dark-mode aware via the
8
+ * `theme-dark` class (driven by uiStore.isDark).
9
+ */
10
+ import { computed } from 'vue';
11
+ import { useRouter } from 'vue-router';
12
+ import { useDatasetSeries } from '../composables/useDatasetSeries';
13
+ import { useUiStore } from '../stores/ui';
14
+
15
+ const props = defineProps<{
16
+ registerId: string;
17
+ }>();
18
+
19
+ const router = useRouter();
20
+ const uiStore = useUiStore();
21
+
22
+ const { seriesForActive } = useDatasetSeries(() => props.registerId);
23
+ const series = computed(() => seriesForActive.value);
24
+
25
+ function navigate(registerId: string) {
26
+ if (registerId === props.registerId) return;
27
+ router.push({ name: 'dataset', params: { registerId } });
28
+ }
29
+
30
+ function memberBadge(member: { isCurrent: boolean; isActive: boolean; status: string }): string | null {
31
+ if (member.isActive) return 'viewing';
32
+ if (member.isCurrent) return 'current';
33
+ if (member.status === 'withdrawn' || member.status === 'superseded') return 'archived';
34
+ return null;
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <section
40
+ v-if="series && series.members.length > 1"
41
+ :class="['series-card', { 'theme-dark': uiStore.isDark }]"
42
+ :aria-label="`Series ${series.title}`"
43
+ >
44
+ <header class="series-head">
45
+ <span class="series-label">Edition series</span>
46
+ <span class="series-key">{{ series.key }}</span>
47
+ </header>
48
+
49
+ <h3 class="series-title">{{ series.title }}</h3>
50
+
51
+ <div class="series-meta">
52
+ <span class="series-count">{{ series.members.length }} editions</span>
53
+ <span class="series-sep">·</span>
54
+ <span class="series-total">{{ series.totalConcepts.toLocaleString() }} concepts</span>
55
+ </div>
56
+
57
+ <ol class="series-list">
58
+ <li
59
+ v-for="member in [...series.members].reverse()"
60
+ :key="member.id"
61
+ :class="['series-item', { active: member.isActive }]"
62
+ >
63
+ <button
64
+ class="series-button"
65
+ :disabled="member.isActive"
66
+ @click="navigate(member.id)"
67
+ >
68
+ <span class="series-year">{{ member.year ?? '—' }}</span>
69
+ <span class="series-detail">
70
+ <span class="series-ref">{{ member.ref }}</span>
71
+ <span v-if="member.conceptCount" class="series-concepts">
72
+ {{ member.conceptCount.toLocaleString() }} concepts
73
+ </span>
74
+ </span>
75
+ <span
76
+ v-if="memberBadge(member)"
77
+ class="series-badge"
78
+ :class="{
79
+ 'badge-current': memberBadge(member) === 'current',
80
+ 'badge-viewing': memberBadge(member) === 'viewing',
81
+ 'badge-archived': memberBadge(member) === 'archived',
82
+ }"
83
+ >
84
+ {{ memberBadge(member) }}
85
+ </span>
86
+ </button>
87
+ </li>
88
+ </ol>
89
+ </section>
90
+ </template>
91
+
92
+ <style scoped>
93
+ /* Light theme tokens */
94
+ .series-card {
95
+ --sc-bg: linear-gradient(180deg, rgba(255, 252, 240, 0.96) 0%, rgba(248, 240, 220, 0.92) 100%);
96
+ --sc-bg-solid: #FFFCF2;
97
+ --sc-ink: #0F1A30;
98
+ --sc-ink-soft: #2D3A52;
99
+ --sc-ink-mute: #5A6577;
100
+ --sc-rule: rgba(184, 147, 90, 0.30);
101
+ --sc-gold: #B8935A;
102
+ --sc-gold-deep: #8C6A3A;
103
+ --sc-badge-text: #FFFCF2;
104
+
105
+ background: var(--sc-bg);
106
+ border: 1px solid var(--sc-rule);
107
+ border-radius: 8px;
108
+ padding: 14px 16px 10px;
109
+ box-shadow:
110
+ 0 1px 2px rgba(0, 0, 0, 0.05),
111
+ 0 4px 14px rgba(0, 0, 0, 0.06),
112
+ inset 0 1px 0 rgba(255, 255, 255, 0.5);
113
+ color: var(--sc-ink);
114
+ }
115
+
116
+ /* Dark theme */
117
+ .series-card.theme-dark,
118
+ :global(.dark) .series-card {
119
+ --sc-bg: linear-gradient(180deg, rgba(36, 38, 60, 0.96) 0%, rgba(28, 30, 50, 0.92) 100%);
120
+ --sc-bg-solid: #1c1e32;
121
+ --sc-ink: #f0f0f4;
122
+ --sc-ink-soft: #dddde6;
123
+ --sc-ink-mute: #8d8faa;
124
+ --sc-rule: rgba(212, 175, 110, 0.40);
125
+ --sc-gold: #D4AF6E;
126
+ --sc-gold-deep: #B8935A;
127
+ --sc-badge-text: #1c1e32;
128
+ }
129
+
130
+ .series-head {
131
+ display: flex;
132
+ align-items: baseline;
133
+ justify-content: space-between;
134
+ margin-bottom: 6px;
135
+ }
136
+ .series-label {
137
+ font-size: 9.5px;
138
+ font-weight: 700;
139
+ text-transform: uppercase;
140
+ letter-spacing: 0.18em;
141
+ color: var(--sc-gold-deep);
142
+ }
143
+ .series-key {
144
+ font-family: 'JetBrains Mono', monospace;
145
+ font-size: 11px;
146
+ color: var(--sc-ink-mute);
147
+ font-weight: 600;
148
+ }
149
+
150
+ .series-title {
151
+ font-family: 'DM Serif Display', Georgia, serif;
152
+ font-size: 15px;
153
+ color: var(--sc-ink);
154
+ margin: 0 0 4px;
155
+ line-height: 1.2;
156
+ letter-spacing: -0.005em;
157
+ }
158
+
159
+ .series-meta {
160
+ font-family: 'DM Sans', system-ui, sans-serif;
161
+ font-size: 10.5px;
162
+ color: var(--sc-ink-mute);
163
+ margin-bottom: 10px;
164
+ letter-spacing: 0.02em;
165
+ }
166
+ .series-sep { margin: 0 6px; opacity: 0.6; }
167
+
168
+ .series-list { list-style: none; padding: 0; margin: 0; }
169
+
170
+ .series-item {
171
+ position: relative;
172
+ margin-bottom: 1px;
173
+ }
174
+ .series-item.active::before {
175
+ content: '';
176
+ position: absolute;
177
+ left: -1px;
178
+ top: 6px;
179
+ bottom: 6px;
180
+ width: 2px;
181
+ background: var(--sc-gold);
182
+ border-radius: 1px;
183
+ }
184
+
185
+ .series-button {
186
+ display: flex;
187
+ align-items: baseline;
188
+ gap: 10px;
189
+ width: 100%;
190
+ background: transparent;
191
+ border: none;
192
+ padding: 7px 6px;
193
+ text-align: left;
194
+ cursor: pointer;
195
+ font-family: inherit;
196
+ border-radius: 4px;
197
+ transition: background 0.15s;
198
+ color: inherit;
199
+ }
200
+ .series-button:hover:not(:disabled) {
201
+ background: rgba(184, 147, 90, 0.10);
202
+ }
203
+ .series-button:disabled { cursor: default; }
204
+
205
+ .series-year {
206
+ font-family: 'JetBrains Mono', monospace;
207
+ font-size: 12.5px;
208
+ font-weight: 600;
209
+ color: var(--sc-ink);
210
+ letter-spacing: 0.02em;
211
+ min-width: 38px;
212
+ flex-shrink: 0;
213
+ }
214
+ .series-item.active .series-year { color: var(--sc-gold-deep); }
215
+
216
+ .series-detail {
217
+ display: flex;
218
+ flex-direction: column;
219
+ flex: 1;
220
+ min-width: 0;
221
+ }
222
+ .series-ref {
223
+ font-family: 'JetBrains Mono', monospace;
224
+ font-size: 10.5px;
225
+ color: var(--sc-ink-soft);
226
+ white-space: nowrap;
227
+ overflow: hidden;
228
+ text-overflow: ellipsis;
229
+ }
230
+ .series-concepts {
231
+ font-family: 'DM Sans', system-ui, sans-serif;
232
+ font-size: 9px;
233
+ color: var(--sc-ink-mute);
234
+ font-style: italic;
235
+ text-transform: lowercase;
236
+ margin-top: 1px;
237
+ }
238
+
239
+ .series-badge {
240
+ font-family: 'DM Sans', system-ui, sans-serif;
241
+ font-size: 8.5px;
242
+ font-weight: 700;
243
+ text-transform: uppercase;
244
+ letter-spacing: 0.12em;
245
+ padding: 2px 6px;
246
+ border-radius: 2px;
247
+ flex-shrink: 0;
248
+ }
249
+ .badge-current {
250
+ /* Outlined style — gold text on the card's surface color, with a thin
251
+ gold border. Contrast: ~12:1 in light mode, ~10:1 in dark mode.
252
+ The filled variant had white-on-gold which capped at 2.7:1 (light) /
253
+ 2.0:1 (dark) — well below WCAG AA for small text. */
254
+ background: rgba(184, 147, 90, 0.10);
255
+ color: var(--sc-gold-deep);
256
+ border: 1px solid var(--sc-gold);
257
+ }
258
+ :global(.dark) .badge-current {
259
+ background: rgba(212, 175, 110, 0.12);
260
+ color: var(--sc-gold);
261
+ }
262
+ .badge-viewing {
263
+ background: var(--sc-gold-deep);
264
+ color: var(--sc-badge-text);
265
+ }
266
+ .badge-archived {
267
+ background: rgba(168, 168, 155, 0.18);
268
+ color: var(--sc-ink-mute);
269
+ }
270
+ </style>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { onErrorCaptured, ref } from 'vue';
3
+ import { GlossaristError, formatError, isGlossaristError } from '../errors';
4
+
5
+ defineOptions({ name: 'ErrorBoundary' });
6
+
7
+ const props = defineProps<{
8
+ title?: string;
9
+ retryKey?: string | number;
10
+ }>();
11
+
12
+ const emit = defineEmits<{ (e: 'error', err: unknown): void }>();
13
+
14
+ const error = ref<GlossaristError | Error | null>(null);
15
+
16
+ onErrorCaptured((err: unknown) => {
17
+ error.value = err instanceof Error ? err : new Error(String(err));
18
+ emit('error', err);
19
+ return false;
20
+ });
21
+
22
+ function dismiss() {
23
+ error.value = null;
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <slot v-if="!error" />
29
+ <div
30
+ v-else
31
+ class="error-boundary"
32
+ :data-retry-key="props.retryKey"
33
+ role="alert"
34
+ aria-live="assertive"
35
+ >
36
+ <div class="error-boundary__header">
37
+ <h3>{{ props.title ?? 'Something went wrong' }}</h3>
38
+ <button type="button" class="error-boundary__retry" @click="dismiss">
39
+ Retry
40
+ </button>
41
+ </div>
42
+ <p class="error-boundary__message">{{ error.message }}</p>
43
+ <details v-if="isGlossaristError(error)" class="error-boundary__details">
44
+ <summary>Details</summary>
45
+ <pre dir="auto">{{ formatError(error) }}</pre>
46
+ </details>
47
+ </div>
48
+ </template>
49
+
50
+ <style scoped>
51
+ .error-boundary {
52
+ border: 1px solid #fca5a5;
53
+ background: #fef2f2;
54
+ color: #7f1d1d;
55
+ border-radius: 0.5rem;
56
+ padding: 1rem 1.25rem;
57
+ }
58
+ .error-boundary__header {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ gap: 0.75rem;
63
+ }
64
+ .error-boundary__header h3 {
65
+ margin: 0;
66
+ font-size: 1rem;
67
+ font-weight: 600;
68
+ }
69
+ .error-boundary__retry {
70
+ background: transparent;
71
+ border: 1px solid currentColor;
72
+ color: inherit;
73
+ border-radius: 0.25rem;
74
+ padding: 0.25rem 0.75rem;
75
+ font-size: 0.875rem;
76
+ cursor: pointer;
77
+ }
78
+ .error-boundary__message {
79
+ margin: 0.5rem 0 0;
80
+ font-size: 0.95rem;
81
+ }
82
+ .error-boundary__details {
83
+ margin-top: 0.5rem;
84
+ }
85
+ .error-boundary__details summary {
86
+ cursor: pointer;
87
+ font-size: 0.85rem;
88
+ }
89
+ .error-boundary__details pre {
90
+ margin: 0.5rem 0 0;
91
+ white-space: pre-wrap;
92
+ font-size: 0.8rem;
93
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
94
+ }
95
+ </style>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue';
3
- import { FORMAT_REGISTRY } from '../utils/concept-formats';
3
+ import { getFormat } from '../composables/use-format-registry';
4
4
  import { useI18n } from '../i18n';
5
5
 
6
6
  const { t } = useI18n();
@@ -12,20 +12,24 @@ const props = defineProps<{
12
12
  }>();
13
13
 
14
14
  interface FormatLink {
15
- key: string;
15
+ id: string;
16
16
  label: string;
17
17
  url: string;
18
+ download: string;
18
19
  }
19
20
 
20
- const links = computed<FormatLink[]>(() =>
21
- props.formats
22
- .filter(f => FORMAT_REGISTRY[f])
23
- .map(f => ({
24
- key: f,
25
- label: FORMAT_REGISTRY[f].label,
26
- url: `/data/${props.registerId}/concepts/${props.conceptId}.${FORMAT_REGISTRY[f].extension}`,
27
- })),
28
- );
21
+ const links = computed<FormatLink[]>(() => {
22
+ const base = import.meta.env.BASE_URL.replace(/\/+$/, '');
23
+ return props.formats
24
+ .map(id => ({ id, desc: getFormat(id) }))
25
+ .filter(({ desc }) => desc && (desc.available === 'per-concept' || desc.available === 'both'))
26
+ .map(({ id, desc }) => ({
27
+ id,
28
+ label: desc!.label,
29
+ url: `${base}/data/${props.registerId}/concepts/${props.conceptId}.${desc!.extension}`,
30
+ download: `${props.conceptId}.${desc!.extension}`,
31
+ }));
32
+ });
29
33
  </script>
30
34
 
31
35
  <template>
@@ -34,9 +38,9 @@ const links = computed<FormatLink[]>(() =>
34
38
  <div class="flex flex-wrap gap-2">
35
39
  <a
36
40
  v-for="link in links"
37
- :key="link.key"
41
+ :key="link.id"
38
42
  :href="link.url"
39
- :download="`${conceptId}.${FORMAT_REGISTRY[link.key].extension}`"
43
+ :download="link.download"
40
44
  class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-ink-50 text-ink-600 hover:bg-ink-100 hover:text-ink-800 transition-colors border border-ink-100"
41
45
  >
42
46
  <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -0,0 +1,277 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * HomeSeriesSection — vocabulary series overview for the home page.
4
+ *
5
+ * Groups all loaded datasets into series (e.g., all `viml-*` editions
6
+ * together) and renders each as a horizontal timeline of edition pills.
7
+ * The newest valid edition is highlighted; older editions are subtler.
8
+ *
9
+ * Only renders if at least one series has 2+ members — single-edition
10
+ * vocabs don't benefit from this view. Theme-aware (light/dark).
11
+ */
12
+ import { computed } from 'vue';
13
+ import { useRouter } from 'vue-router';
14
+ import { useUiStore } from '../stores/ui';
15
+ import { useDatasetSeries } from '../composables/useDatasetSeries';
16
+
17
+ const router = useRouter();
18
+ const uiStore = useUiStore();
19
+ const { series } = useDatasetSeries();
20
+
21
+ const multiEditionSeries = computed(() =>
22
+ series.value.filter(s => s.members.length > 1)
23
+ );
24
+
25
+ function openDataset(registerId: string) {
26
+ router.push({ name: 'dataset', params: { registerId } });
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <section
32
+ v-if="multiEditionSeries.length"
33
+ :class="['series-section', { 'theme-dark': uiStore.isDark }]"
34
+ aria-label="Vocabulary edition series"
35
+ >
36
+ <header class="series-section-head">
37
+ <span class="series-section-ornament">✦</span>
38
+ <div>
39
+ <h2 class="series-section-title">Vocabulary Series</h2>
40
+ <p class="series-section-sub">Multi-edition terminology archives · click any edition</p>
41
+ </div>
42
+ </header>
43
+
44
+ <div class="series-grid">
45
+ <article
46
+ v-for="s in multiEditionSeries"
47
+ :key="s.key"
48
+ class="series-article"
49
+ >
50
+ <header class="series-article-head">
51
+ <h3 class="series-article-title">{{ s.title }}</h3>
52
+ <span class="series-article-meta">
53
+ {{ s.members.length }} editions · {{ s.totalConcepts.toLocaleString() }} concepts
54
+ </span>
55
+ </header>
56
+
57
+ <ol class="series-timeline">
58
+ <li
59
+ v-for="member in s.members"
60
+ :key="member.id"
61
+ :class="['timeline-item', { current: member.isCurrent }]"
62
+ >
63
+ <button
64
+ class="timeline-button"
65
+ :class="{ current: member.isCurrent }"
66
+ @click="openDataset(member.id)"
67
+ >
68
+ <span class="timeline-year">{{ member.year ?? '—' }}</span>
69
+ <span class="timeline-status">{{ member.status }}</span>
70
+ <span v-if="member.isCurrent" class="timeline-mark">◆</span>
71
+ </button>
72
+ </li>
73
+ </ol>
74
+ </article>
75
+ </div>
76
+ </section>
77
+ </template>
78
+
79
+ <style scoped>
80
+ /* Light theme (default) — warm parchment */
81
+ .series-section {
82
+ --hs-bg: transparent;
83
+ --hs-ink: #0F1A30;
84
+ --hs-ink-soft: #2D3A52;
85
+ --hs-ink-mute: #5A6577;
86
+ --hs-rule: rgba(184, 147, 90, 0.25);
87
+ --hs-card-bg: linear-gradient(180deg, rgba(255, 252, 240, 0.96) 0%, rgba(248, 240, 220, 0.92) 100%);
88
+ --hs-timeline-bg: rgba(255, 255, 255, 0.6);
89
+ --hs-timeline-border: rgba(184, 147, 90, 0.20);
90
+ --hs-timeline-hover: rgba(255, 252, 240, 1);
91
+ --hs-gold: #B8935A;
92
+ --hs-gold-deep: #8C6A3A;
93
+ --hs-gold-tint: rgba(184, 147, 90, 0.12);
94
+
95
+ max-width: 80rem;
96
+ margin: 3rem auto 4rem;
97
+ padding: 0 1rem;
98
+ color: var(--hs-ink);
99
+ }
100
+
101
+ /* Dark theme */
102
+ .series-section.theme-dark,
103
+ :global(.dark) .series-section {
104
+ --hs-bg: transparent;
105
+ --hs-ink: #f0f0f4;
106
+ --hs-ink-soft: #dddde6;
107
+ --hs-ink-mute: #8d8faa;
108
+ --hs-rule: rgba(184, 147, 90, 0.30);
109
+ --hs-card-bg: linear-gradient(180deg, rgba(36, 38, 60, 0.96) 0%, rgba(28, 30, 50, 0.92) 100%);
110
+ --hs-timeline-bg: rgba(28, 30, 50, 0.6);
111
+ --hs-timeline-border: rgba(184, 147, 90, 0.25);
112
+ --hs-timeline-hover: rgba(36, 38, 60, 1);
113
+ --hs-gold: #D4AF6E;
114
+ --hs-gold-deep: #B8935A;
115
+ --hs-gold-tint: rgba(212, 175, 110, 0.15);
116
+ }
117
+
118
+ .series-section-head {
119
+ display: flex;
120
+ align-items: baseline;
121
+ gap: 14px;
122
+ margin-bottom: 1.5rem;
123
+ padding-bottom: 1rem;
124
+ border-bottom: 1px solid var(--hs-rule);
125
+ }
126
+ .series-section-ornament {
127
+ color: var(--hs-gold);
128
+ font-size: 22px;
129
+ transform: rotate(45deg);
130
+ display: inline-block;
131
+ }
132
+ .series-section-title {
133
+ font-family: 'DM Serif Display', Georgia, serif;
134
+ font-size: 28px;
135
+ color: var(--hs-ink);
136
+ letter-spacing: -0.015em;
137
+ line-height: 1;
138
+ margin: 0;
139
+ }
140
+ .series-section-sub {
141
+ font-family: 'DM Sans', system-ui, sans-serif;
142
+ font-size: 12px;
143
+ color: var(--hs-ink-mute);
144
+ margin-top: 4px;
145
+ font-style: italic;
146
+ letter-spacing: 0.02em;
147
+ }
148
+
149
+ .series-grid {
150
+ display: grid;
151
+ grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
152
+ gap: 1.25rem;
153
+ }
154
+
155
+ .series-article {
156
+ background: var(--hs-card-bg);
157
+ border: 1px solid var(--hs-rule);
158
+ border-radius: 10px;
159
+ padding: 18px 22px 16px;
160
+ box-shadow:
161
+ 0 1px 2px rgba(0, 0, 0, 0.08),
162
+ 0 6px 20px rgba(0, 0, 0, 0.10);
163
+ }
164
+
165
+ .series-article-head {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 4px;
169
+ margin-bottom: 14px;
170
+ }
171
+ .series-article-title {
172
+ font-family: 'DM Serif Display', Georgia, serif;
173
+ font-size: 17px;
174
+ color: var(--hs-ink);
175
+ line-height: 1.15;
176
+ letter-spacing: -0.005em;
177
+ margin: 0;
178
+ }
179
+ .series-article-meta {
180
+ font-family: 'DM Sans', system-ui, sans-serif;
181
+ font-size: 10.5px;
182
+ color: var(--hs-ink-mute);
183
+ letter-spacing: 0.04em;
184
+ text-transform: lowercase;
185
+ }
186
+
187
+ .series-timeline {
188
+ list-style: none;
189
+ padding: 0;
190
+ margin: 0;
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ gap: 6px;
194
+ align-items: stretch;
195
+ }
196
+
197
+ .timeline-item {
198
+ position: relative;
199
+ }
200
+ .timeline-item:not(:last-child)::after {
201
+ content: '→';
202
+ position: absolute;
203
+ right: -5px;
204
+ top: 50%;
205
+ transform: translateY(-50%);
206
+ font-size: 11px;
207
+ color: var(--hs-gold);
208
+ opacity: 0.5;
209
+ pointer-events: none;
210
+ }
211
+
212
+ .timeline-button {
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: center;
216
+ gap: 2px;
217
+ padding: 8px 12px 6px;
218
+ background: var(--hs-timeline-bg);
219
+ border: 1px solid var(--hs-timeline-border);
220
+ border-radius: 6px;
221
+ cursor: pointer;
222
+ transition: all 0.2s;
223
+ font-family: inherit;
224
+ min-width: 64px;
225
+ position: relative;
226
+ color: var(--hs-ink);
227
+ }
228
+ .timeline-button:hover {
229
+ background: var(--hs-timeline-hover);
230
+ border-color: var(--hs-gold);
231
+ transform: translateY(-1px);
232
+ box-shadow: 0 2px 8px var(--hs-gold-tint);
233
+ }
234
+ .timeline-button.current {
235
+ background: var(--hs-timeline-hover);
236
+ border-color: var(--hs-gold);
237
+ box-shadow:
238
+ 0 0 0 3px var(--hs-gold-tint),
239
+ 0 2px 8px var(--hs-gold-tint);
240
+ }
241
+
242
+ .timeline-year {
243
+ font-family: 'JetBrains Mono', monospace;
244
+ font-size: 14px;
245
+ font-weight: 600;
246
+ color: var(--hs-ink);
247
+ letter-spacing: 0.02em;
248
+ }
249
+ .timeline-button.current .timeline-year {
250
+ color: var(--hs-gold-deep);
251
+ }
252
+
253
+ .timeline-status {
254
+ font-family: 'DM Sans', system-ui, sans-serif;
255
+ font-size: 8.5px;
256
+ color: var(--hs-ink-mute);
257
+ text-transform: uppercase;
258
+ letter-spacing: 0.08em;
259
+ font-weight: 500;
260
+ }
261
+
262
+ .timeline-mark {
263
+ position: absolute;
264
+ top: -6px;
265
+ right: -6px;
266
+ width: 14px;
267
+ height: 14px;
268
+ background: var(--hs-gold);
269
+ color: white;
270
+ border-radius: 50%;
271
+ font-size: 7px;
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
276
+ }
277
+ </style>