@glossarist/concept-browser 0.7.100 → 0.7.101

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.
@@ -0,0 +1,254 @@
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
+ </script>
90
+
91
+ <template>
92
+ <div class="learn-relationships max-w-5xl mx-auto px-4 py-8">
93
+ <header class="mb-8">
94
+ <h1 class="text-3xl font-serif text-ink-800 dark:text-ink-50 mb-2">
95
+ {{ t('learn.relationships.title') }}
96
+ </h1>
97
+ <p class="text-ink-500 dark:text-ink-300 max-w-3xl">{{ t('learn.relationships.intro') }}</p>
98
+ </header>
99
+
100
+ <!-- ── Bilateral relations ────────────────────────────────────── -->
101
+ <section class="mb-12" :aria-label="t('learn.relationships.bilateralSection')">
102
+ <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">
103
+ {{ t('learn.relationships.bilateralSection') }}
104
+ </h2>
105
+ <p class="text-sm text-ink-500 dark:text-ink-400 mb-4">
106
+ {{ t('learn.relationships.bilateralIntro') }}
107
+ </p>
108
+ <div class="space-y-4">
109
+ <article v-for="cat in bilateralCategories" :key="cat.id" class="card p-4">
110
+ <header class="flex items-center gap-2 mb-3">
111
+ <span :class="['text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded', cat.color]">
112
+ {{ cat.label }}
113
+ </span>
114
+ <span class="text-xs text-ink-400">{{ cat.types.length }}</span>
115
+ </header>
116
+ <ul class="space-y-2">
117
+ <li v-for="item in cat.types" :key="item.type" :id="item.type" class="bilateral-row scroll-mt-20">
118
+ <div class="flex items-baseline gap-2 flex-wrap">
119
+ <span class="font-medium text-ink-800 dark:text-ink-100">{{ item.label }}</span>
120
+ <code class="text-xs text-ink-400 dark:text-ink-500 font-mono">{{ item.type }}</code>
121
+ </div>
122
+ <p v-if="item.definition" class="text-sm text-ink-600 dark:text-ink-300 mt-0.5">
123
+ {{ item.definition }}
124
+ </p>
125
+ </li>
126
+ </ul>
127
+ </article>
128
+ </div>
129
+ </section>
130
+
131
+ <!-- ── Partitive hyperedge ────────────────────────────────────── -->
132
+ <section class="mb-12" :aria-label="t('learn.relationships.partitiveSection')">
133
+ <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">
134
+ {{ t('learn.relationships.partitiveSection') }}
135
+ </h2>
136
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
137
+ <LearnCard
138
+ id="__partitive__"
139
+ :title="t('learn.relationships.partitiveTitle')"
140
+ iso-reference="ISO 704:2022 §5.5.4.3"
141
+ learn-more-url="https://www.glossarist.org/model/partitive-relations"
142
+ >
143
+ <template #diagram>
144
+ <HyperedgeDiagram kind="partitive" />
145
+ </template>
146
+ {{ t('learn.relationships.partitiveBody') }}
147
+ <template #example>
148
+ {{ t('learn.example') }}: VIM 1.3 system of quantities
149
+ — quantity equation (required, at_least_one), base quantity (required, multiple),
150
+ derived quantity (required, at_least_one).
151
+ </template>
152
+ </LearnCard>
153
+ </div>
154
+
155
+ <h3 class="text-base font-semibold text-ink-700 dark:text-ink-100 mb-3">
156
+ {{ t('learn.dimensions.sectionTitle') }}
157
+ </h3>
158
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
159
+ <DimensionCard
160
+ id="partitive-presence"
161
+ :name="t('learn.dimensions.presence.title')"
162
+ iso-reference="ISO 704:2022 §5.5.4.3.1"
163
+ :summary="t('learn.dimensions.presence.summary')"
164
+ :values="presenceValues"
165
+ >
166
+ <template #visual="{ value }">
167
+ <svg width="44" height="14" aria-hidden="true">
168
+ <line x1="2" y1="7" x2="42" y2="7" stroke="currentColor" stroke-width="1.5"
169
+ :stroke-dasharray="value === 'optional' ? '4 3' : undefined" />
170
+ </svg>
171
+ </template>
172
+ </DimensionCard>
173
+
174
+ <DimensionCard
175
+ id="partitive-count"
176
+ :name="t('learn.dimensions.count.title')"
177
+ iso-reference="ISO 704:2022 §5.5.4.3.1"
178
+ :summary="t('learn.dimensions.count.summary')"
179
+ :values="countValues"
180
+ >
181
+ <template #visual="{ value }">
182
+ <svg width="44" height="14" aria-hidden="true">
183
+ <line x1="2" y1="5" x2="42" y2="5" stroke="currentColor" stroke-width="1.5" />
184
+ <line v-if="value !== 'exactly_one'" x1="2" y1="9" x2="42" y2="9" stroke="currentColor"
185
+ stroke-width="1.5" :stroke-dasharray="value === 'at_least_one' ? '4 3' : undefined" />
186
+ </svg>
187
+ </template>
188
+ </DimensionCard>
189
+
190
+ <DimensionCard
191
+ id="partitive-delimiting"
192
+ :name="t('learn.dimensions.delimiting.title')"
193
+ iso-reference="ISO 704:2022 §5.5.4.3.1"
194
+ :summary="t('learn.dimensions.delimiting.summary')"
195
+ :values="delimitingValues"
196
+ >
197
+ <template #visual="{ value }">
198
+ <svg width="44" height="14" aria-hidden="true">
199
+ <line x1="2" y1="7" x2="42" y2="7" stroke="currentColor"
200
+ :stroke-width="value === 'true' ? DELIMITING_STROKE_WIDTH : NORMAL_STROKE_WIDTH" />
201
+ </svg>
202
+ </template>
203
+ </DimensionCard>
204
+ </div>
205
+ </section>
206
+
207
+ <!-- ── General hyperedge ──────────────────────────────────────── -->
208
+ <section :aria-label="t('learn.relationships.generalSection')">
209
+ <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">
210
+ {{ t('learn.relationships.generalSection') }}
211
+ </h2>
212
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
213
+ <LearnCard
214
+ id="__generic__"
215
+ :title="t('learn.relationships.generalTitle')"
216
+ iso-reference="ISO 704:2022 §5.5.4.2"
217
+ learn-more-url="https://www.glossarist.org/model/generic-relations"
218
+ >
219
+ <template #diagram>
220
+ <HyperedgeDiagram kind="generic" />
221
+ </template>
222
+ {{ t('learn.relationships.generalBody') }}
223
+ <template #example>
224
+ {{ t('learn.example') }}: VIM 1.9 measurement unit
225
+ — group criterion "by magnitude relationship" splits into
226
+ multiple (1.17), submultiple (1.18), coherent derived (1.12).
227
+ </template>
228
+ </LearnCard>
229
+
230
+ <div class="space-y-4">
231
+ <LearnCard
232
+ :title="t('learn.relationships.criterionTitle')"
233
+ iso-reference="ISO 704:2022 §5.5.4.2 + §5.6.3"
234
+ >
235
+ {{ t('learn.relationships.criterionBody') }}
236
+ </LearnCard>
237
+
238
+ <LearnCard
239
+ :title="t('learn.relationships.thickThinTitle')"
240
+ iso-reference="ISO 704:2022 §5.6.3"
241
+ >
242
+ {{ t('learn.relationships.thickThinBody') }}
243
+ </LearnCard>
244
+ </div>
245
+ </div>
246
+ </section>
247
+ </div>
248
+ </template>
249
+
250
+ <style scoped>
251
+ .bilateral-row {
252
+ scroll-margin-top: 5rem;
253
+ }
254
+ </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>
@@ -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
- });