@glossarist/concept-browser 0.7.50 → 0.7.52
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/cli/index.mjs +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/figure-bridge.ts +22 -23
- package/src/adapters/non-verbal/formula-bridge.ts +11 -9
- package/src/adapters/non-verbal/glossarist-augment.d.ts +133 -0
- package/src/adapters/non-verbal/index.ts +12 -9
- package/src/adapters/non-verbal/kind.ts +2 -1
- package/src/adapters/non-verbal/table-bridge.ts +12 -10
- package/src/adapters/non-verbal/types.ts +36 -54
- package/src/adapters/non-verbal-resolver.ts +6 -3
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/NonVerbalRepDisplay.vue +2 -2
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/components/figure/FigureDisplay.vue +16 -15
- package/src/components/figure/FigureImages.vue +38 -16
- package/src/components/figure/figure-image-pick.ts +1 -1
- package/src/components/figure/figure-layout.ts +1 -1
- package/src/components/formula/FormulaDisplay.vue +11 -9
- package/src/components/formula/FormulaExpression.vue +4 -4
- package/src/components/non-verbal/NonVerbalCaption.vue +5 -5
- package/src/components/non-verbal/NonVerbalSources.vue +3 -11
- package/src/components/table/TableDisplay.vue +6 -4
- package/src/components/table/TableMarkup.vue +1 -1
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/use-non-verbal-entity.ts +2 -1
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +183 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- 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 {
|
|
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
|
-
|
|
15
|
+
id: string;
|
|
16
16
|
label: string;
|
|
17
17
|
url: string;
|
|
18
|
+
download: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const links = computed<FormatLink[]>(() =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.map(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
41
|
+
:key="link.id"
|
|
38
42
|
:href="link.url"
|
|
39
|
-
:download="
|
|
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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue';
|
|
3
3
|
import type { NonVerbRep, Citation } from 'glossarist';
|
|
4
|
-
import type {
|
|
4
|
+
import type { LocalizedString, NonVerbRepV3, NonVerbRepImage, NonVerbalSource } from '../adapters/non-verbal/types';
|
|
5
5
|
import { resolveFallbackChain } from '../utils/locale';
|
|
6
6
|
import FigureImages from './figure/FigureImages.vue';
|
|
7
7
|
import NonVerbalCaption from './non-verbal/NonVerbalCaption.vue';
|
|
@@ -21,7 +21,7 @@ const fallbackChain = computed(() => resolveFallbackChain(props.datasetLocales))
|
|
|
21
21
|
// (images/alt/caption/description/sources). See TODO.figures/19.
|
|
22
22
|
const v3Reps = computed<NonVerbRepV3[]>(() => props.reps as unknown as NonVerbRepV3[]);
|
|
23
23
|
|
|
24
|
-
function imagesOf(rep: NonVerbRepV3):
|
|
24
|
+
function imagesOf(rep: NonVerbRepV3): NonVerbRepImage[] {
|
|
25
25
|
return rep.images ?? [];
|
|
26
26
|
}
|
|
27
27
|
|