@glossarist/concept-browser 0.7.100 → 0.7.102

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 (58) hide show
  1. package/cli/index.mjs +14 -251
  2. package/cli/index.ts +289 -0
  3. package/package.json +3 -2
  4. package/src/__tests__/__fixtures__/concepts.ts +3 -3
  5. package/src/__tests__/components/learn/learn-card-dimension-card.test.ts +112 -0
  6. package/src/__tests__/composables/use-concept-edges-v2.test.ts +1 -0
  7. package/src/__tests__/designation-relationship.test.ts +2 -2
  8. package/src/__tests__/perf/serialization-perf.test.ts +1 -0
  9. package/src/__tests__/use-rdf-document.test.ts +1 -0
  10. package/src/__tests__/views/learn/learn-designations-view.test.ts +84 -0
  11. package/src/__tests__/views/learn/learn-index-view.test.ts +45 -0
  12. package/src/__tests__/views/learn/learn-relationships-view.test.ts +83 -0
  13. package/src/__tests__/views/learn/learn-statuses-view.test.ts +60 -0
  14. package/src/adapters/bibliography-adapter.ts +1 -1
  15. package/src/adapters/model-bridge.ts +15 -15
  16. package/src/adapters/non-verbal/figure-bridge.ts +2 -2
  17. package/src/adapters/non-verbal/formula-bridge.ts +2 -2
  18. package/src/adapters/non-verbal/table-bridge.ts +2 -2
  19. package/src/components/CitationDisplay.vue +1 -1
  20. package/src/components/ConceptDetail.vue +2 -2
  21. package/src/components/ConceptDiffModal.vue +2 -2
  22. package/src/components/ConceptDiffView.vue +1 -1
  23. package/src/components/DesignationList.vue +4 -4
  24. package/src/components/NavIcon.vue +1 -0
  25. package/src/components/RelationSphere.vue +1 -1
  26. package/src/components/figure/FigureDisplay.vue +5 -5
  27. package/src/components/formula/FormulaDisplay.vue +4 -4
  28. package/src/components/learn/DimensionCard.vue +71 -0
  29. package/src/components/learn/LearnCard.vue +59 -0
  30. package/src/components/table/TableDisplay.vue +3 -3
  31. package/src/components/use-rdf-document.ts +16 -7
  32. package/src/composables/use-concept-content.ts +3 -3
  33. package/src/composables/use-concept-diff.ts +2 -2
  34. package/src/composables/use-concept-edges.ts +3 -3
  35. package/src/config/page-types.ts +1 -1
  36. package/src/i18n/locales/eng.yml +114 -9
  37. package/src/i18n/locales/fra.yml +114 -9
  38. package/src/layouts/Default.astro +32 -2
  39. package/src/router/index.ts +21 -2
  40. package/src/utils/content-renderer.ts +1 -1
  41. package/src/utils/partitive-multiplicity.ts +3 -2
  42. package/src/views/ConceptView.vue +3 -3
  43. package/src/views/learn/LearnDesignationsView.vue +190 -0
  44. package/src/views/learn/LearnIndexView.vue +75 -0
  45. package/src/views/learn/LearnRelationshipsView.vue +317 -0
  46. package/src/views/learn/LearnStatusesView.vue +114 -0
  47. package/tailwind.config.js +3 -3
  48. package/tsconfig.json +1 -1
  49. package/src/__tests__/views/relation-types-view.test.ts +0 -85
  50. package/src/adapters/model-bridge/bridges.ts +0 -159
  51. package/src/adapters/model-bridge/concept.ts +0 -97
  52. package/src/adapters/model-bridge/entity-refs.ts +0 -49
  53. package/src/adapters/model-bridge/hyperedge.ts +0 -103
  54. package/src/adapters/model-bridge/index.ts +0 -46
  55. package/src/adapters/model-bridge/mappers.ts +0 -277
  56. package/src/adapters/model-bridge/partitive.ts +0 -79
  57. package/src/adapters/non-verbal/glossarist-augment.d.ts +0 -339
  58. /package/src/adapters/{model-bridge/jsonld-types.ts → jsonld-types.ts} +0 -0
@@ -0,0 +1,317 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LearnRelationshipsView — expanded from the former /relation-types.
4
+ *
5
+ * Three sections:
6
+ * 1. Bilateral relations (one-to-one) — grouped by category.
7
+ * 2. Partitive hyperedge — card + per-dimension breakdown (presence,
8
+ * count, delimiting) anchored in ISO 704:2022 §5.5.4.3.1.
9
+ * 3. General hyperedge — card + criterion + thick/thin hierarchy
10
+ * anchored in ISO 704:2022 §5.5.4.2 + §5.6.3.
11
+ *
12
+ * Sphere legend labels deep-link here via `#<type>` anchors
13
+ * (e.g. `#__generic__`, `#__partitive__`, `#broader_generic`).
14
+ */
15
+ import { computed } from 'vue';
16
+ import { useI18n } from '../../i18n';
17
+ import {
18
+ RELATIONSHIP_CATEGORIES,
19
+ relationshipLabel,
20
+ relationshipDefinition,
21
+ } from '../../utils/relationship-categories';
22
+ import LearnCard from '../../components/learn/LearnCard.vue';
23
+ import DimensionCard, { type DimensionValue } from '../../components/learn/DimensionCard.vue';
24
+ import HyperedgeDiagram from '../../components/HyperedgeDiagram.vue';
25
+ import {
26
+ NORMAL_STROKE_WIDTH,
27
+ DELIMITING_STROKE_WIDTH,
28
+ } from '../../utils/partitive-multiplicity';
29
+
30
+ const { t } = useI18n();
31
+
32
+ const bilateralCategories = computed(() =>
33
+ RELATIONSHIP_CATEGORIES.map(cat => ({
34
+ id: cat.id,
35
+ label: cat.label,
36
+ color: cat.color,
37
+ types: cat.types.map(type => ({
38
+ type,
39
+ label: relationshipLabel(type),
40
+ definition: relationshipDefinition(type),
41
+ })),
42
+ })),
43
+ );
44
+
45
+ /* Per-dimension values for the partitive hyperedge (ISO 704:2022 §5.5.4.3.1). */
46
+ const presenceValues: readonly DimensionValue[] = [
47
+ {
48
+ value: 'required',
49
+ label: t('learn.dimensions.presence.required.label'),
50
+ meaning: t('learn.dimensions.presence.required.meaning'),
51
+ },
52
+ {
53
+ value: 'optional',
54
+ label: t('learn.dimensions.presence.optional.label'),
55
+ meaning: t('learn.dimensions.presence.optional.meaning'),
56
+ },
57
+ ];
58
+
59
+ const countValues: readonly DimensionValue[] = [
60
+ {
61
+ value: 'exactly_one',
62
+ label: t('learn.dimensions.count.exactly_one.label'),
63
+ meaning: t('learn.dimensions.count.exactly_one.meaning'),
64
+ },
65
+ {
66
+ value: 'at_least_one',
67
+ label: t('learn.dimensions.count.at_least_one.label'),
68
+ meaning: t('learn.dimensions.count.at_least_one.meaning'),
69
+ },
70
+ {
71
+ value: 'multiple',
72
+ label: t('learn.dimensions.count.multiple.label'),
73
+ meaning: t('learn.dimensions.count.multiple.meaning'),
74
+ },
75
+ ];
76
+
77
+ const delimitingValues: readonly DimensionValue[] = [
78
+ {
79
+ value: 'false',
80
+ label: t('learn.dimensions.delimiting.false.label'),
81
+ meaning: t('learn.dimensions.delimiting.false.meaning'),
82
+ },
83
+ {
84
+ value: 'true',
85
+ label: t('learn.dimensions.delimiting.true.label'),
86
+ meaning: t('learn.dimensions.delimiting.true.meaning'),
87
+ },
88
+ ];
89
+
90
+ const validCombinations = [
91
+ {
92
+ description: t('learn.dimensions.combo.compulsory'),
93
+ uml: '1..1',
94
+ model: 'required + exactly_one',
95
+ svg: '<svg width="44" height="14"><line x1="2" y1="7" x2="42" y2="7" stroke="currentColor" stroke-width="1.5"/></svg>',
96
+ },
97
+ {
98
+ description: t('learn.dimensions.combo.atLeastOne'),
99
+ uml: '1..*',
100
+ model: 'required + at_least_one',
101
+ svg: '<svg width="44" height="14"><line x1="2" y1="5" x2="42" y2="5" stroke="currentColor" stroke-width="1.5"/><line x1="2" y1="9" x2="42" y2="9" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/></svg>',
102
+ },
103
+ {
104
+ description: t('learn.dimensions.combo.optional'),
105
+ uml: '0..1',
106
+ model: 'optional + exactly_one',
107
+ svg: '<svg width="44" height="14"><line x1="2" y1="7" x2="42" y2="7" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/></svg>',
108
+ },
109
+ {
110
+ description: t('learn.dimensions.combo.optionalMultiple'),
111
+ uml: '0..*',
112
+ model: 'optional + multiple',
113
+ svg: '<svg width="44" height="14"><line x1="2" y1="5" x2="42" y2="5" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/><line x1="2" y1="9" x2="42" y2="9" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/></svg>',
114
+ },
115
+ {
116
+ description: t('learn.dimensions.combo.compulsoryMultiple'),
117
+ uml: '2..*',
118
+ model: 'required + multiple',
119
+ svg: '<svg width="44" height="14"><line x1="2" y1="5" x2="42" y2="5" stroke="currentColor" stroke-width="1.5"/><line x1="2" y1="9" x2="42" y2="9" stroke="currentColor" stroke-width="1.5"/></svg>',
120
+ },
121
+ ];
122
+ </script>
123
+
124
+ <template>
125
+ <div class="learn-relationships max-w-5xl mx-auto px-4 py-8">
126
+ <header class="mb-8">
127
+ <h1 class="text-3xl font-serif text-ink-800 dark:text-ink-50 mb-2">
128
+ {{ t('learn.relationships.title') }}
129
+ </h1>
130
+ <p class="text-ink-500 dark:text-ink-300 max-w-3xl">{{ t('learn.relationships.intro') }}</p>
131
+ </header>
132
+
133
+ <!-- ── Bilateral relations ────────────────────────────────────── -->
134
+ <section class="mb-12" :aria-label="t('learn.relationships.bilateralSection')">
135
+ <h2 class="text-xl font-semibold text-ink-700 dark:text-ink-100 mb-4 border-b border-ink-100 dark:border-ink-700 pb-2">
136
+ {{ t('learn.relationships.bilateralSection') }}
137
+ </h2>
138
+ <p class="text-sm text-ink-500 dark:text-ink-400 mb-4">
139
+ {{ t('learn.relationships.bilateralIntro') }}
140
+ </p>
141
+ <div class="space-y-4">
142
+ <article v-for="cat in bilateralCategories" :key="cat.id" class="card p-4">
143
+ <header class="flex items-center gap-2 mb-3">
144
+ <span :class="['text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded', cat.color]">
145
+ {{ cat.label }}
146
+ </span>
147
+ <span class="text-xs text-ink-400">{{ cat.types.length }}</span>
148
+ </header>
149
+ <ul class="space-y-2">
150
+ <li v-for="item in cat.types" :key="item.type" :id="item.type" class="bilateral-row scroll-mt-20">
151
+ <div class="flex items-baseline gap-2 flex-wrap">
152
+ <span class="font-medium text-ink-800 dark:text-ink-100">{{ item.label }}</span>
153
+ <code class="text-xs text-ink-400 dark:text-ink-500 font-mono">{{ item.type }}</code>
154
+ </div>
155
+ <p v-if="item.definition" class="text-sm text-ink-600 dark:text-ink-300 mt-0.5">
156
+ {{ item.definition }}
157
+ </p>
158
+ </li>
159
+ </ul>
160
+ </article>
161
+ </div>
162
+ </section>
163
+
164
+ <!-- ── Partitive hyperedge ────────────────────────────────────── -->
165
+ <section class="mb-12" :aria-label="t('learn.relationships.partitiveSection')">
166
+ <h2 class="text-xl font-semibold text-ink-700 dark:text-ink-100 mb-4 border-b border-ink-100 dark:border-ink-700 pb-2">
167
+ {{ t('learn.relationships.partitiveSection') }}
168
+ </h2>
169
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
170
+ <LearnCard
171
+ id="__partitive__"
172
+ :title="t('learn.relationships.partitiveTitle')"
173
+ iso-reference="ISO 704:2022 §5.5.4.3"
174
+ learn-more-url="https://www.glossarist.org/model/partitive-relations"
175
+ >
176
+ <template #diagram>
177
+ <HyperedgeDiagram kind="partitive" />
178
+ </template>
179
+ {{ t('learn.relationships.partitiveBody') }}
180
+ <template #example>
181
+ {{ t('learn.example') }}: VIM 1.3 system of quantities
182
+ — quantity equation (required, at_least_one), base quantity (required, multiple),
183
+ derived quantity (required, at_least_one).
184
+ </template>
185
+ </LearnCard>
186
+ </div>
187
+
188
+ <h3 class="text-base font-semibold text-ink-700 dark:text-ink-100 mb-3">
189
+ {{ t('learn.dimensions.sectionTitle') }}
190
+ </h3>
191
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
192
+ <DimensionCard
193
+ id="partitive-presence"
194
+ :name="t('learn.dimensions.presence.title')"
195
+ iso-reference="ISO 704:2022 §5.5.4.3.1"
196
+ :summary="t('learn.dimensions.presence.summary')"
197
+ :values="presenceValues"
198
+ >
199
+ <template #visual="{ value }">
200
+ <svg width="44" height="14" aria-hidden="true">
201
+ <line x1="2" y1="7" x2="42" y2="7" stroke="currentColor" stroke-width="1.5"
202
+ :stroke-dasharray="value === 'optional' ? '4 3' : undefined" />
203
+ </svg>
204
+ </template>
205
+ </DimensionCard>
206
+
207
+ <DimensionCard
208
+ id="partitive-count"
209
+ :name="t('learn.dimensions.count.title')"
210
+ iso-reference="ISO 704:2022 §5.5.4.3.1"
211
+ :summary="t('learn.dimensions.count.summary')"
212
+ :values="countValues"
213
+ >
214
+ <template #visual="{ value }">
215
+ <svg width="44" height="14" aria-hidden="true">
216
+ <line x1="2" y1="5" x2="42" y2="5" stroke="currentColor" stroke-width="1.5" />
217
+ <line v-if="value !== 'exactly_one'" x1="2" y1="9" x2="42" y2="9" stroke="currentColor"
218
+ stroke-width="1.5" :stroke-dasharray="value === 'at_least_one' ? '4 3' : undefined" />
219
+ </svg>
220
+ </template>
221
+ </DimensionCard>
222
+
223
+ <DimensionCard
224
+ id="partitive-delimiting"
225
+ :name="t('learn.dimensions.delimiting.title')"
226
+ iso-reference="ISO 704:2022 §5.5.4.3.1"
227
+ :summary="t('learn.dimensions.delimiting.summary')"
228
+ :values="delimitingValues"
229
+ >
230
+ <template #visual="{ value }">
231
+ <svg width="44" height="14" aria-hidden="true">
232
+ <line x1="2" y1="7" x2="42" y2="7" stroke="currentColor"
233
+ :stroke-width="value === 'true' ? DELIMITING_STROKE_WIDTH : NORMAL_STROKE_WIDTH" />
234
+ </svg>
235
+ </template>
236
+ </DimensionCard>
237
+ </div>
238
+
239
+ <h3 class="text-base font-semibold text-ink-700 dark:text-ink-100 mt-6 mb-3">
240
+ {{ t('learn.dimensions.combinationsTitle') }}
241
+ </h3>
242
+ <div class="card p-4 overflow-x-auto">
243
+ <table class="w-full text-sm">
244
+ <thead>
245
+ <tr class="border-b border-ink-100 dark:border-ink-700 text-ink-500 dark:text-ink-400 text-xs uppercase tracking-wide">
246
+ <th class="text-left py-2 pr-4">{{ t('learn.dimensions.combinationsDescription') }}</th>
247
+ <th class="text-center py-2 px-2">UML</th>
248
+ <th class="text-left py-2 px-2">{{ t('learn.dimensions.combinationsModel') }}</th>
249
+ <th class="text-center py-2 pl-4">{{ t('learn.dimensions.combinationsVisual') }}</th>
250
+ </tr>
251
+ </thead>
252
+ <tbody class="divide-y divide-ink-100 dark:divide-ink-700">
253
+ <tr v-for="combo in validCombinations" :key="combo.model">
254
+ <td class="py-2 pr-4 text-ink-700 dark:text-ink-200">{{ combo.description }}</td>
255
+ <td class="py-2 px-2 text-center font-mono text-ink-500 dark:text-ink-400">{{ combo.uml }}</td>
256
+ <td class="py-2 px-2 font-mono text-xs text-ink-600 dark:text-ink-300">{{ combo.model }}</td>
257
+ <td class="py-2 pl-4 text-center" v-html="combo.svg"></td>
258
+ </tr>
259
+ <tr class="opacity-50">
260
+ <td class="py-2 pr-4 text-ink-500 dark:text-ink-400 italic">{{ t('learn.dimensions.invalidDescription') }}</td>
261
+ <td class="py-2 px-2 text-center font-mono text-ink-400">—</td>
262
+ <td class="py-2 px-2 font-mono text-xs text-ink-400">{{ t('learn.dimensions.invalidModel') }}</td>
263
+ <td class="py-2 pl-4 text-center text-xs text-ink-400 italic">{{ t('learn.dimensions.invalidReason') }}</td>
264
+ </tr>
265
+ </tbody>
266
+ </table>
267
+ </div>
268
+ </section>
269
+
270
+ <!-- ── General hyperedge ──────────────────────────────────────── -->
271
+ <section :aria-label="t('learn.relationships.generalSection')">
272
+ <h2 class="text-xl font-semibold text-ink-700 dark:text-ink-100 mb-4 border-b border-ink-100 dark:border-ink-700 pb-2">
273
+ {{ t('learn.relationships.generalSection') }}
274
+ </h2>
275
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
276
+ <LearnCard
277
+ id="__generic__"
278
+ :title="t('learn.relationships.generalTitle')"
279
+ iso-reference="ISO 704:2022 §5.5.4.2"
280
+ learn-more-url="https://www.glossarist.org/model/generic-relations"
281
+ >
282
+ <template #diagram>
283
+ <HyperedgeDiagram kind="generic" />
284
+ </template>
285
+ {{ t('learn.relationships.generalBody') }}
286
+ <template #example>
287
+ {{ t('learn.example') }}: VIM 1.9 measurement unit
288
+ — group criterion "by magnitude relationship" splits into
289
+ multiple (1.17), submultiple (1.18), coherent derived (1.12).
290
+ </template>
291
+ </LearnCard>
292
+
293
+ <div class="space-y-4">
294
+ <LearnCard
295
+ :title="t('learn.relationships.criterionTitle')"
296
+ iso-reference="ISO 704:2022 §5.5.4.2 + §5.6.3"
297
+ >
298
+ {{ t('learn.relationships.criterionBody') }}
299
+ </LearnCard>
300
+
301
+ <LearnCard
302
+ :title="t('learn.relationships.thickThinTitle')"
303
+ iso-reference="ISO 704:2022 §5.6.3"
304
+ >
305
+ {{ t('learn.relationships.thickThinBody') }}
306
+ </LearnCard>
307
+ </div>
308
+ </div>
309
+ </section>
310
+ </div>
311
+ </template>
312
+
313
+ <style scoped>
314
+ .bilateral-row {
315
+ scroll-margin-top: 5rem;
316
+ }
317
+ </style>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LearnStatusesView — quick reference for the status badges used
4
+ * across the site.
5
+ *
6
+ * Three categories:
7
+ * 1. Entry status (glossarist lifecycle: valid, withdrawn, draft)
8
+ * 2. Normative status (preferred/admitted/deprecated — cross-links
9
+ * /learn/designations, no duplication)
10
+ * 3. Source status (identical, modified, restyled — provenance)
11
+ */
12
+ import { useI18n } from '../../i18n';
13
+ import LearnCard from '../../components/learn/LearnCard.vue';
14
+
15
+ const { t } = useI18n();
16
+
17
+ interface StatusRow {
18
+ value: string;
19
+ label: string;
20
+ meaning: string;
21
+ }
22
+
23
+ const entryStatuses: readonly StatusRow[] = [
24
+ { value: 'valid', label: t('learn.statuses.entry.valid.label'), meaning: t('learn.statuses.entry.valid.meaning') },
25
+ { value: 'withdrawn', label: t('learn.statuses.entry.withdrawn.label'), meaning: t('learn.statuses.entry.withdrawn.meaning') },
26
+ { value: 'draft', label: t('learn.statuses.entry.draft.label'), meaning: t('learn.statuses.entry.draft.meaning') },
27
+ { value: 'superseded', label: t('learn.statuses.entry.superseded.label'), meaning: t('learn.statuses.entry.superseded.meaning') },
28
+ ];
29
+
30
+ const sourceStatuses: readonly StatusRow[] = [
31
+ { value: 'identical', label: t('learn.statuses.source.identical.label'), meaning: t('learn.statuses.source.identical.meaning') },
32
+ { value: 'modified', label: t('learn.statuses.source.modified.label'), meaning: t('learn.statuses.source.modified.meaning') },
33
+ { value: 'restyled', label: t('learn.statuses.source.restyled.label'), meaning: t('learn.statuses.source.restyled.meaning') },
34
+ { value: 'lineage', label: t('learn.statuses.source.lineage.label'), meaning: t('learn.statuses.source.lineage.meaning') },
35
+ ];
36
+ </script>
37
+
38
+ <template>
39
+ <div class="learn-statuses max-w-5xl mx-auto px-4 py-8">
40
+ <header class="mb-8">
41
+ <h1 class="text-3xl font-serif text-ink-800 dark:text-ink-50 mb-2">
42
+ {{ t('learn.statuses.title') }}
43
+ </h1>
44
+ <p class="text-ink-500 dark:text-ink-300 max-w-3xl">{{ t('learn.statuses.intro') }}</p>
45
+ </header>
46
+
47
+ <!-- ── Entry status ──────────────────────────────────────────── -->
48
+ <section class="mb-12" :aria-label="t('learn.statuses.entrySection')">
49
+ <h2 class="text-xl font-semibold text-ink-700 dark:text-ink-100 mb-4 border-b border-ink-100 dark:border-ink-700 pb-2">
50
+ {{ t('learn.statuses.entrySection') }}
51
+ </h2>
52
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
53
+ <LearnCard
54
+ v-for="s in entryStatuses"
55
+ :key="s.value"
56
+ :id="`entry-${s.value}`"
57
+ :title="s.label"
58
+ >
59
+ <p>{{ s.meaning }}</p>
60
+ <template #example>
61
+ <span class="inline-block text-xs font-semibold px-2 py-0.5 rounded uppercase tracking-wide"
62
+ :class="{
63
+ 'bg-green-50 text-green-700 dark:bg-green-900/40 dark:text-green-300': s.value === 'valid',
64
+ 'bg-red-50 text-red-700 dark:bg-red-900/40 dark:text-red-300': s.value === 'withdrawn',
65
+ 'bg-amber-50 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300': s.value === 'draft',
66
+ 'bg-gray-50 text-gray-700 dark:bg-gray-700/40 dark:text-gray-300': s.value === 'superseded',
67
+ }">
68
+ {{ s.label }}
69
+ </span>
70
+ </template>
71
+ </LearnCard>
72
+ </div>
73
+ </section>
74
+
75
+ <!-- ── Normative status (cross-link) ─────────────────────────── -->
76
+ <section class="mb-12" :aria-label="t('learn.statuses.normativeSection')">
77
+ <h2 class="text-xl font-semibold text-ink-700 dark:text-ink-100 mb-4 border-b border-ink-100 dark:border-ink-700 pb-2">
78
+ {{ t('learn.statuses.normativeSection') }}
79
+ </h2>
80
+ <LearnCard
81
+ :title="t('learn.statuses.normativeTitle')"
82
+ iso-reference="ISO 10241-1:2011 §3.4.1.3"
83
+ >
84
+ <p>{{ t('learn.statuses.normativeSummary') }}</p>
85
+ <p class="mt-3">
86
+ <RouterLink to="/learn/designations" class="text-ink-500 dark:text-ink-400 underline">
87
+ {{ t('learn.statuses.normativeGoDeeper') }} →
88
+ </RouterLink>
89
+ </p>
90
+ </LearnCard>
91
+ </section>
92
+
93
+ <!-- ── Source status ─────────────────────────────────────────── -->
94
+ <section :aria-label="t('learn.statuses.sourceSection')">
95
+ <h2 class="text-xl font-semibold text-ink-700 dark:text-ink-100 mb-4 border-b border-ink-100 dark:border-ink-700 pb-2">
96
+ {{ t('learn.statuses.sourceSection') }}
97
+ </h2>
98
+ <p class="text-sm text-ink-500 dark:text-ink-400 mb-4">
99
+ {{ t('learn.statuses.sourceIntro') }}
100
+ </p>
101
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
102
+ <LearnCard
103
+ v-for="s in sourceStatuses"
104
+ :key="s.value"
105
+ :id="`source-${s.value}`"
106
+ :title="s.label"
107
+ learn-more-url="https://www.glossarist.org/model/sources"
108
+ >
109
+ <p>{{ s.meaning }}</p>
110
+ </LearnCard>
111
+ </div>
112
+ </section>
113
+ </div>
114
+ </template>
@@ -13,8 +13,8 @@ export default {
13
13
  theme: {
14
14
  extend: {
15
15
  fontFamily: {
16
- serif: ['"DM Serif Display"', 'Georgia', 'serif'],
17
- sans: ['"DM Sans"', 'system-ui', 'sans-serif'],
16
+ serif: ['var(--font-header, "DM Serif Display")', 'Georgia', 'serif'],
17
+ sans: ['var(--font-body, "DM Sans")', 'system-ui', 'sans-serif'],
18
18
  mono: ['"JetBrains Mono"', 'monospace'],
19
19
  },
20
20
  colors: {
@@ -39,7 +39,7 @@ export default {
39
39
  },
40
40
  typography: {
41
41
  serif: {
42
- 'font-family': '"DM Serif Display", Georgia, serif',
42
+ 'font-family': 'var(--font-header, "DM Serif Display"), Georgia, serif',
43
43
  },
44
44
  },
45
45
  },
package/tsconfig.json CHANGED
@@ -20,6 +20,6 @@
20
20
  },
21
21
  "baseUrl": "."
22
22
  },
23
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts", ".astro/types.d.ts"],
23
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "cli/**/*.ts", "env.d.ts", ".astro/types.d.ts"],
24
24
  "exclude": ["src/content.config.ts", "src/pages/**", "src/layouts/**", "src/astro-islands/**", "src/lib/site-config.ts"]
25
25
  }
@@ -1,85 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { mount } from '@vue/test-utils';
3
- import { createPinia, setActivePinia } from 'pinia';
4
- import { createRouter, createMemoryHistory } from 'vue-router';
5
- import RelationTypesView from '../../views/RelationTypesView.vue';
6
-
7
- /**
8
- * Smoke + structure tests for the Relation Types teaching page.
9
- *
10
- * Hyperedge section: 2 cards (partitive + general) with anchors.
11
- * Bilateral section: at least one category with type rows.
12
- */
13
- async function makeRouter() {
14
- const router = createRouter({
15
- history: createMemoryHistory(),
16
- routes: [
17
- { path: '/', name: 'home', component: { template: '<div/>' } },
18
- { path: '/relation-types', name: 'relation-types', component: RelationTypesView },
19
- ],
20
- });
21
- await router.push('/relation-types');
22
- await router.isReady();
23
- return router;
24
- }
25
-
26
- describe('RelationTypesView', () => {
27
- beforeEach(() => {
28
- setActivePinia(createPinia());
29
- });
30
-
31
- it('renders the page header with translated title', async () => {
32
- const router = await makeRouter();
33
- const w = mount(RelationTypesView, { global: { plugins: [router] } });
34
- expect(w.find('h1').text()).toContain('Relation Types');
35
- });
36
-
37
- it('renders both hyperedge cards with the expected anchors', async () => {
38
- const router = await makeRouter();
39
- const w = mount(RelationTypesView, { global: { plugins: [router] } });
40
- const cards = w.findAll('.hyperedge-card');
41
- expect(cards.length).toBe(2);
42
- const ids = cards.map(c => c.attributes('id'));
43
- expect(ids).toContain('__partitive__');
44
- expect(ids).toContain('__generic__');
45
- });
46
-
47
- it('renders a HyperedgeDiagram child for each card', async () => {
48
- const router = await makeRouter();
49
- const w = mount(RelationTypesView, { global: { plugins: [router] } });
50
- /* The HyperedgeDiagram component renders an <svg class="hyperedge-diagram"> */
51
- expect(w.findAll('svg.hyperedge-diagram').length).toBe(2);
52
- });
53
-
54
- it('renders the bilateral section grouped by category', async () => {
55
- const router = await makeRouter();
56
- const w = mount(RelationTypesView, { global: { plugins: [router] } });
57
- const bilateralArticles = w.findAll('section[aria-label="Bilateral relations (one-to-one)"] article');
58
- /* The taxonomy has ≥5 categories (hierarchical, associative, lifecycle, …). */
59
- expect(bilateralArticles.length).toBeGreaterThanOrEqual(5);
60
- });
61
-
62
- it('every bilateral type row has an id anchor matching the type key', async () => {
63
- const router = await makeRouter();
64
- const w = mount(RelationTypesView, { global: { plugins: [router] } });
65
- const rows = w.findAll('.bilateral-row');
66
- expect(rows.length).toBeGreaterThan(10); /* taxonomy has ~40 types */
67
- for (const row of rows) {
68
- const id = row.attributes('id');
69
- expect(id).toBeTruthy();
70
- expect(id).toMatch(/^[a-z_]+$/);
71
- }
72
- });
73
-
74
- it('renders label + ISO standard + definition for each hyperedge card', async () => {
75
- const router = await makeRouter();
76
- const w = mount(RelationTypesView, { global: { plugins: [router] } });
77
- const cards = w.findAll('.hyperedge-card');
78
- for (const card of cards) {
79
- /* Label is the h3 heading; ISO reference is a mono span; definition is a paragraph. */
80
- expect(card.find('h3').text()).toBeTruthy();
81
- expect(card.find('span.font-mono').text()).toMatch(/ISO 704/);
82
- expect(card.find('p').text().length).toBeGreaterThan(20);
83
- }
84
- });
85
- });