@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.
- package/package.json +1 -1
- package/src/__tests__/components/learn/learn-card-dimension-card.test.ts +112 -0
- package/src/__tests__/views/learn/learn-designations-view.test.ts +84 -0
- package/src/__tests__/views/learn/learn-index-view.test.ts +45 -0
- package/src/__tests__/views/learn/learn-relationships-view.test.ts +83 -0
- package/src/__tests__/views/learn/learn-statuses-view.test.ts +60 -0
- package/src/components/RelationSphere.vue +1 -1
- package/src/components/learn/DimensionCard.vue +71 -0
- package/src/components/learn/LearnCard.vue +59 -0
- package/src/config/page-types.ts +1 -1
- package/src/i18n/locales/eng.yml +102 -9
- package/src/i18n/locales/fra.yml +102 -9
- package/src/router/index.ts +21 -2
- package/src/views/learn/LearnDesignationsView.vue +190 -0
- package/src/views/learn/LearnIndexView.vue +75 -0
- package/src/views/learn/LearnRelationshipsView.vue +254 -0
- package/src/views/learn/LearnStatusesView.vue +114 -0
- package/src/__tests__/views/relation-types-view.test.ts +0 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.101",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import LearnCard from '../../../components/learn/LearnCard.vue';
|
|
5
|
+
import DimensionCard from '../../../components/learn/DimensionCard.vue';
|
|
6
|
+
|
|
7
|
+
describe('LearnCard', () => {
|
|
8
|
+
beforeEach(() => setActivePinia(createPinia()));
|
|
9
|
+
|
|
10
|
+
it('renders the title and ISO reference', () => {
|
|
11
|
+
const w = mount(LearnCard, {
|
|
12
|
+
props: { title: 'partitive relation', isoReference: 'ISO 704:2022 §5.5.4.3' },
|
|
13
|
+
});
|
|
14
|
+
expect(w.find('h3').text()).toBe('partitive relation');
|
|
15
|
+
expect(w.find('.font-mono').text()).toBe('ISO 704:2022 §5.5.4.3');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders the body slot', () => {
|
|
19
|
+
const w = mount(LearnCard, {
|
|
20
|
+
props: { title: 'x' },
|
|
21
|
+
slots: { default: '<p>Body content</p>' },
|
|
22
|
+
});
|
|
23
|
+
expect(w.find('.learn-card-body').html()).toContain('Body content');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders the example slot in its own section', () => {
|
|
27
|
+
const w = mount(LearnCard, {
|
|
28
|
+
props: { title: 'x' },
|
|
29
|
+
slots: { example: 'VIM 1.9 measurement unit' },
|
|
30
|
+
});
|
|
31
|
+
expect(w.find('.learn-card-example').text()).toContain('VIM 1.9');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('omits the "learn more" footer when learnMoreUrl is absent', () => {
|
|
35
|
+
const w = mount(LearnCard, { props: { title: 'x' } });
|
|
36
|
+
expect(w.find('.learn-card-footer').exists()).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders the learn-more footer when learnMoreUrl is present', () => {
|
|
40
|
+
const w = mount(LearnCard, {
|
|
41
|
+
props: { title: 'x', learnMoreUrl: 'https://www.glossarist.org/model/x' },
|
|
42
|
+
});
|
|
43
|
+
const link = w.find('.learn-card-footer a');
|
|
44
|
+
expect(link.attributes('href')).toBe('https://www.glossarist.org/model/x');
|
|
45
|
+
expect(link.attributes('target')).toBe('_blank');
|
|
46
|
+
expect(link.attributes('rel')).toBe('noopener');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('binds the id prop for deep-link anchors', () => {
|
|
50
|
+
const w = mount(LearnCard, {
|
|
51
|
+
props: { id: '__partitive__', title: 'x' },
|
|
52
|
+
});
|
|
53
|
+
expect(w.find('article').attributes('id')).toBe('__partitive__');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('DimensionCard', () => {
|
|
58
|
+
beforeEach(() => setActivePinia(createPinia()));
|
|
59
|
+
|
|
60
|
+
it('renders one row per value', () => {
|
|
61
|
+
const w = mount(DimensionCard, {
|
|
62
|
+
props: {
|
|
63
|
+
name: 'Presence',
|
|
64
|
+
summary: 'Compulsory or optional',
|
|
65
|
+
values: [
|
|
66
|
+
{ value: 'required', label: 'required', meaning: 'must be present' },
|
|
67
|
+
{ value: 'optional', label: 'optional', meaning: 'may be absent' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
expect(w.findAll('.dimension-value-row').length).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders value labels and meanings', () => {
|
|
75
|
+
const w = mount(DimensionCard, {
|
|
76
|
+
props: {
|
|
77
|
+
name: 'x',
|
|
78
|
+
summary: 's',
|
|
79
|
+
values: [{ value: 'required', label: 'required', meaning: 'must be present' }],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const row = w.find('.dimension-value-row');
|
|
83
|
+
expect(row.text()).toContain('required');
|
|
84
|
+
expect(row.text()).toContain('must be present');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('exposes a visual scoped slot per value', () => {
|
|
88
|
+
const w = mount(DimensionCard, {
|
|
89
|
+
props: {
|
|
90
|
+
name: 'x',
|
|
91
|
+
summary: 's',
|
|
92
|
+
values: [{ value: 'required', label: 'required', meaning: 'm' }],
|
|
93
|
+
},
|
|
94
|
+
slots: {
|
|
95
|
+
visual: '<svg data-test="custom-visual" />',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
expect(w.find('svg[data-test="custom-visual"]').exists()).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('binds the id prop for deep-link anchors', () => {
|
|
102
|
+
const w = mount(DimensionCard, {
|
|
103
|
+
props: {
|
|
104
|
+
id: 'partitive-presence',
|
|
105
|
+
name: 'x',
|
|
106
|
+
summary: 's',
|
|
107
|
+
values: [{ value: 'required', label: 'required', meaning: 'm' }],
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
expect(w.find('article').attributes('id')).toBe('partitive-presence');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import LearnDesignationsView from '../../../views/learn/LearnDesignationsView.vue';
|
|
6
|
+
|
|
7
|
+
async function makeRouter() {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
12
|
+
{ path: '/learn/designations', name: 'learn-designations', component: LearnDesignationsView },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
await router.push('/learn/designations');
|
|
16
|
+
await router.isReady();
|
|
17
|
+
return router;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('LearnDesignationsView', () => {
|
|
21
|
+
beforeEach(() => setActivePinia(createPinia()));
|
|
22
|
+
|
|
23
|
+
it('renders the page header', async () => {
|
|
24
|
+
const router = await makeRouter();
|
|
25
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
26
|
+
expect(w.find('h1').text()).toContain('Designations');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders three top-level sections (normative, kinds, types)', async () => {
|
|
30
|
+
const router = await makeRouter();
|
|
31
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
32
|
+
const sections = w.findAll('section[aria-label]');
|
|
33
|
+
expect(sections.length).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('normative section has all three ratings (preferred/admitted/deprecated)', async () => {
|
|
37
|
+
const router = await makeRouter();
|
|
38
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
39
|
+
const normative = w.find('section[aria-label="Normative status"]');
|
|
40
|
+
const cardTitles = normative.findAll('h3').map(h => h.text());
|
|
41
|
+
expect(cardTitles).toEqual(expect.arrayContaining([
|
|
42
|
+
'preferred term', 'admitted term', 'deprecated term',
|
|
43
|
+
]));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('each normative card notes "there can be more than one"', async () => {
|
|
47
|
+
/* The user's specific concern: explain that there can be multiple. */
|
|
48
|
+
const router = await makeRouter();
|
|
49
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
50
|
+
const normative = w.find('section[aria-label="Normative status"]');
|
|
51
|
+
const notes = normative.findAll('.italic').map(i => i.text().toLowerCase());
|
|
52
|
+
expect(notes.filter(t => t.includes('more than one')).length).toBe(3);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('kinds section renders all four kinds (full form, abbreviated, borrowed, variant)', async () => {
|
|
56
|
+
const router = await makeRouter();
|
|
57
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
58
|
+
const kinds = w.find('section[aria-label="Kinds and forms of term"]');
|
|
59
|
+
const titles = kinds.findAll('h3').map(h => h.text());
|
|
60
|
+
expect(titles).toEqual(expect.arrayContaining([
|
|
61
|
+
'full form', 'abbreviated form', 'borrowed term', 'variant',
|
|
62
|
+
]));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('types section renders term, acronym, initialism, symbol', async () => {
|
|
66
|
+
const router = await makeRouter();
|
|
67
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
68
|
+
const types = w.find('section[aria-label="Designation types"]');
|
|
69
|
+
const titles = types.findAll('h3').map(h => h.text());
|
|
70
|
+
expect(titles).toEqual(expect.arrayContaining([
|
|
71
|
+
'term (expression)', 'acronym', 'initialism', 'symbol',
|
|
72
|
+
]));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('every example slot mentions a concrete ISO example', async () => {
|
|
76
|
+
const router = await makeRouter();
|
|
77
|
+
const w = mount(LearnDesignationsView, { global: { plugins: [router] } });
|
|
78
|
+
const examples = w.findAll('.learn-card-example').map(e => e.text());
|
|
79
|
+
expect(examples.some(t => t.includes('compact disc'))).toBe(true);
|
|
80
|
+
expect(examples.some(t => t.includes('WWW'))).toBe(true);
|
|
81
|
+
expect(examples.some(t => t.includes('laser'))).toBe(true);
|
|
82
|
+
expect(examples.some(t => t.includes('OIML'))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import LearnIndexView from '../../../views/learn/LearnIndexView.vue';
|
|
6
|
+
|
|
7
|
+
async function makeRouter() {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
12
|
+
{ path: '/learn', name: 'learn', component: LearnIndexView },
|
|
13
|
+
{ path: '/learn/relationships', name: 'learn-relationships', component: { template: '<div/>' } },
|
|
14
|
+
{ path: '/learn/designations', name: 'learn-designations', component: { template: '<div/>' } },
|
|
15
|
+
{ path: '/learn/statuses', name: 'learn-statuses', component: { template: '<div/>' } },
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
await router.push('/learn');
|
|
19
|
+
await router.isReady();
|
|
20
|
+
return router;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('LearnIndexView', () => {
|
|
24
|
+
beforeEach(() => setActivePinia(createPinia()));
|
|
25
|
+
|
|
26
|
+
it('renders the page header with translated title', async () => {
|
|
27
|
+
const router = await makeRouter();
|
|
28
|
+
const w = mount(LearnIndexView, { global: { plugins: [router] } });
|
|
29
|
+
expect(w.find('h1').text()).toContain('Learn');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders exactly 3 topic cards for Phase 1', async () => {
|
|
33
|
+
const router = await makeRouter();
|
|
34
|
+
const w = mount(LearnIndexView, { global: { plugins: [router] } });
|
|
35
|
+
/* Each RouterLink wraps a LearnCard. Phase 1 = 3 topics. */
|
|
36
|
+
expect(w.findAllComponents({ name: 'LearnCard' }).length).toBe(3);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('each topic card links to one of the Phase 1 routes', async () => {
|
|
40
|
+
const router = await makeRouter();
|
|
41
|
+
const w = mount(LearnIndexView, { global: { plugins: [router] } });
|
|
42
|
+
const links = w.findAll('a').map(a => a.attributes('href'));
|
|
43
|
+
expect(links).toEqual(expect.arrayContaining(['/learn/relationships', '/learn/designations', '/learn/statuses']));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import LearnRelationshipsView from '../../../views/learn/LearnRelationshipsView.vue';
|
|
6
|
+
|
|
7
|
+
async function makeRouter() {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
12
|
+
{ path: '/learn/relationships', name: 'learn-relationships', component: LearnRelationshipsView },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
await router.push('/learn/relationships');
|
|
16
|
+
await router.isReady();
|
|
17
|
+
return router;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('LearnRelationshipsView', () => {
|
|
21
|
+
beforeEach(() => setActivePinia(createPinia()));
|
|
22
|
+
|
|
23
|
+
it('renders the page header with translated title', async () => {
|
|
24
|
+
const router = await makeRouter();
|
|
25
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
26
|
+
expect(w.find('h1').text()).toContain('Relationships');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders three top-level sections (bilateral, partitive, general)', async () => {
|
|
30
|
+
const router = await makeRouter();
|
|
31
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
32
|
+
const sections = w.findAll('section[aria-label]');
|
|
33
|
+
expect(sections.length).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders the hyperedge cards with the expected anchors', async () => {
|
|
37
|
+
const router = await makeRouter();
|
|
38
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
39
|
+
const ids = w.findAll('.learn-card[id]').map(c => c.attributes('id'));
|
|
40
|
+
expect(ids).toContain('__partitive__');
|
|
41
|
+
expect(ids).toContain('__generic__');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders three DimensionCards for the partitive dimensions', async () => {
|
|
45
|
+
const router = await makeRouter();
|
|
46
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
47
|
+
const dimIds = w.findAll('.dimension-card[id]').map(c => c.attributes('id'));
|
|
48
|
+
expect(dimIds).toEqual(expect.arrayContaining(['partitive-presence', 'partitive-count', 'partitive-delimiting']));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders the bilateral section grouped by category', async () => {
|
|
52
|
+
const router = await makeRouter();
|
|
53
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
54
|
+
const bilateralArticles = w.find('section[aria-label="Bilateral relations (one-to-one)"]').findAll('article');
|
|
55
|
+
expect(bilateralArticles.length).toBeGreaterThanOrEqual(5);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('every bilateral type row has an id anchor matching the type key', async () => {
|
|
59
|
+
const router = await makeRouter();
|
|
60
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
61
|
+
const rows = w.findAll('.bilateral-row');
|
|
62
|
+
expect(rows.length).toBeGreaterThan(10);
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const id = row.attributes('id');
|
|
65
|
+
expect(id).toBeTruthy();
|
|
66
|
+
expect(id).toMatch(/^[a-z_]+$/);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders a live diagram for each hyperedge card', async () => {
|
|
71
|
+
const router = await makeRouter();
|
|
72
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
73
|
+
expect(w.findAll('svg.hyperedge-diagram').length).toBe(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders ISO references on each hyperedge card', async () => {
|
|
77
|
+
const router = await makeRouter();
|
|
78
|
+
const w = mount(LearnRelationshipsView, { global: { plugins: [router] } });
|
|
79
|
+
const isoRefs = w.findAll('.learn-card span.font-mono').map(s => s.text());
|
|
80
|
+
expect(isoRefs.some(t => t.includes('ISO 704:2022 §5.5.4.3'))).toBe(true);
|
|
81
|
+
expect(isoRefs.some(t => t.includes('ISO 704:2022 §5.5.4.2'))).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import LearnStatusesView from '../../../views/learn/LearnStatusesView.vue';
|
|
6
|
+
|
|
7
|
+
async function makeRouter() {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
12
|
+
{ path: '/learn/statuses', name: 'learn-statuses', component: LearnStatusesView },
|
|
13
|
+
{ path: '/learn/designations', name: 'learn-designations', component: { template: '<div/>' } },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
await router.push('/learn/statuses');
|
|
17
|
+
await router.isReady();
|
|
18
|
+
return router;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('LearnStatusesView', () => {
|
|
22
|
+
beforeEach(() => setActivePinia(createPinia()));
|
|
23
|
+
|
|
24
|
+
it('renders the page header', async () => {
|
|
25
|
+
const router = await makeRouter();
|
|
26
|
+
const w = mount(LearnStatusesView, { global: { plugins: [router] } });
|
|
27
|
+
expect(w.find('h1').text()).toContain('Statuses');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders three top-level sections (entry, normative, source)', async () => {
|
|
31
|
+
const router = await makeRouter();
|
|
32
|
+
const w = mount(LearnStatusesView, { global: { plugins: [router] } });
|
|
33
|
+
const sections = w.findAll('section[aria-label]');
|
|
34
|
+
expect(sections.length).toBe(3);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('entry section renders all four entry statuses with anchors', async () => {
|
|
38
|
+
const router = await makeRouter();
|
|
39
|
+
const w = mount(LearnStatusesView, { global: { plugins: [router] } });
|
|
40
|
+
const entrySection = w.find('section[aria-label="Entry status"]');
|
|
41
|
+
const ids = entrySection.findAll('.learn-card[id]').map(c => c.attributes('id'));
|
|
42
|
+
expect(ids).toEqual(expect.arrayContaining(['entry-valid', 'entry-withdrawn', 'entry-draft', 'entry-superseded']));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('normative section cross-links to /learn/designations', async () => {
|
|
46
|
+
const router = await makeRouter();
|
|
47
|
+
const w = mount(LearnStatusesView, { global: { plugins: [router] } });
|
|
48
|
+
const normative = w.find('section[aria-label="Normative status"]');
|
|
49
|
+
const link = normative.find('a');
|
|
50
|
+
expect(link.attributes('href')).toBe('/learn/designations');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('source section renders the four source statuses with anchors', async () => {
|
|
54
|
+
const router = await makeRouter();
|
|
55
|
+
const w = mount(LearnStatusesView, { global: { plugins: [router] } });
|
|
56
|
+
const sourceSection = w.find('section[aria-label="Source status"]');
|
|
57
|
+
const ids = sourceSection.findAll('.learn-card[id]').map(c => c.attributes('id'));
|
|
58
|
+
expect(ids).toEqual(expect.arrayContaining(['source-identical', 'source-modified', 'source-restyled', 'source-lineage']));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1419,7 +1419,7 @@ defineExpose({ navigate });
|
|
|
1419
1419
|
<span class="sp-count">{{ item.count }}</span>
|
|
1420
1420
|
</button>
|
|
1421
1421
|
<router-link class="sp-legend-label"
|
|
1422
|
-
:to="{ name: '
|
|
1422
|
+
:to="{ name: 'learn-relationships', hash: '#' + item.type }"
|
|
1423
1423
|
:title="t('sphere.learnAbout') + ': ' + item.label">{{ item.label }}</router-link>
|
|
1424
1424
|
</div>
|
|
1425
1425
|
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DimensionCard — structured breakdown of a single dimension along
|
|
4
|
+
* which hyperedge members vary. Used on /learn/relationships for the
|
|
5
|
+
* partitive dimensions (presence, count, delimiting).
|
|
6
|
+
*
|
|
7
|
+
* Renders one row per value showing:
|
|
8
|
+
* - the raw value (machine key)
|
|
9
|
+
* - the human label
|
|
10
|
+
* - a short meaning (what it encodes)
|
|
11
|
+
* - a small visual representation (slot — typically a mini SVG)
|
|
12
|
+
*
|
|
13
|
+
* The user's specific concern: "individually show what dimensions
|
|
14
|
+
* there are and how lines look like". This component is the answer.
|
|
15
|
+
*/
|
|
16
|
+
export interface DimensionValue {
|
|
17
|
+
value: string;
|
|
18
|
+
label: string;
|
|
19
|
+
meaning: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
defineProps<{
|
|
23
|
+
id?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
isoReference?: string;
|
|
26
|
+
/** Plain-language summary of what this dimension captures. */
|
|
27
|
+
summary: string;
|
|
28
|
+
values: readonly DimensionValue[];
|
|
29
|
+
}>();
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<article :id="id" class="dimension-card card p-5 scroll-mt-20">
|
|
34
|
+
<header class="flex items-baseline justify-between gap-3 mb-2 flex-wrap">
|
|
35
|
+
<h4 class="text-base font-semibold text-ink-800 dark:text-ink-50">{{ name }}</h4>
|
|
36
|
+
<span v-if="isoReference" class="text-xs font-mono text-ink-400 dark:text-ink-500">
|
|
37
|
+
{{ isoReference }}
|
|
38
|
+
</span>
|
|
39
|
+
</header>
|
|
40
|
+
<p class="text-sm text-ink-600 dark:text-ink-200 mb-4">{{ summary }}</p>
|
|
41
|
+
<ul class="dimension-values space-y-3">
|
|
42
|
+
<li v-for="v in values" :key="v.value" class="dimension-value-row">
|
|
43
|
+
<div class="flex items-center gap-3">
|
|
44
|
+
<div class="dimension-visual">
|
|
45
|
+
<slot name="visual" :value="v.value" />
|
|
46
|
+
</div>
|
|
47
|
+
<div class="flex-1 min-w-0">
|
|
48
|
+
<div class="flex items-baseline gap-2 flex-wrap">
|
|
49
|
+
<span class="font-medium text-ink-800 dark:text-ink-100">{{ v.label }}</span>
|
|
50
|
+
<code class="text-xs text-ink-400 dark:text-ink-500 font-mono">{{ v.value }}</code>
|
|
51
|
+
</div>
|
|
52
|
+
<p class="text-sm text-ink-600 dark:text-ink-300 mt-0.5">{{ v.meaning }}</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</article>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<style scoped>
|
|
61
|
+
.dimension-card {
|
|
62
|
+
scroll-margin-top: 5rem;
|
|
63
|
+
}
|
|
64
|
+
.dimension-visual {
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
width: 48px;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* LearnCard — the standard topic container across all /learn pages.
|
|
4
|
+
*
|
|
5
|
+
* Layout: title + ISO reference on top, then a flexible body. Optional
|
|
6
|
+
* named slots:
|
|
7
|
+
* - `diagram` — a small visual (live SVG, badge samples, etc.)
|
|
8
|
+
* - `example` — a concrete real-world example
|
|
9
|
+
* Optional `learnMoreUrl` renders a "Learn more on glossarist.org"
|
|
10
|
+
* link at the bottom — keeps concept-browser reader-focused while
|
|
11
|
+
* bridging to the canonical author docs on glossarist.org.
|
|
12
|
+
*
|
|
13
|
+
* Anchored at the `id` prop so URLs like `/learn/relationships#partitive`
|
|
14
|
+
* deep-link to the right card.
|
|
15
|
+
*/
|
|
16
|
+
defineProps<{
|
|
17
|
+
id?: string;
|
|
18
|
+
title: string;
|
|
19
|
+
isoReference?: string;
|
|
20
|
+
learnMoreUrl?: string;
|
|
21
|
+
learnMoreLabel?: string;
|
|
22
|
+
}>();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<article :id="id" class="learn-card card p-5 scroll-mt-20">
|
|
27
|
+
<header class="flex items-baseline justify-between gap-3 mb-3 flex-wrap">
|
|
28
|
+
<h3 class="text-lg font-semibold text-ink-800 dark:text-ink-50">{{ title }}</h3>
|
|
29
|
+
<span v-if="isoReference" class="text-xs font-mono text-ink-400 dark:text-ink-500">
|
|
30
|
+
{{ isoReference }}
|
|
31
|
+
</span>
|
|
32
|
+
</header>
|
|
33
|
+
<div v-if="$slots.diagram" class="learn-card-diagram mb-3">
|
|
34
|
+
<slot name="diagram" />
|
|
35
|
+
</div>
|
|
36
|
+
<div class="learn-card-body text-sm text-ink-600 dark:text-ink-200">
|
|
37
|
+
<slot />
|
|
38
|
+
</div>
|
|
39
|
+
<div v-if="$slots.example" class="learn-card-example mt-3 pt-3 border-t border-ink-100 dark:border-ink-700 text-xs text-ink-500 dark:text-ink-400">
|
|
40
|
+
<slot name="example" />
|
|
41
|
+
</div>
|
|
42
|
+
<footer v-if="learnMoreUrl" class="learn-card-footer mt-3 text-xs">
|
|
43
|
+
<a :href="learnMoreUrl" target="_blank" rel="noopener"
|
|
44
|
+
class="text-ink-500 dark:text-ink-400 hover:text-ink-800 dark:hover:text-ink-100 underline">
|
|
45
|
+
{{ learnMoreLabel ?? 'Learn more on glossarist.org' }} ↗
|
|
46
|
+
</a>
|
|
47
|
+
</footer>
|
|
48
|
+
</article>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.learn-card {
|
|
53
|
+
scroll-margin-top: 5rem;
|
|
54
|
+
}
|
|
55
|
+
.learn-card-diagram {
|
|
56
|
+
display: flex;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
package/src/config/page-types.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const PAGE_TYPES: readonly PageTypeDefinition[] = [
|
|
|
35
35
|
{ type: 'stats', route: 'stats', title: 'Statistics', icon: 'chart', scope: 'dataset', featureFlag: '!stats', autoSynthesize: true },
|
|
36
36
|
{ type: 'sources', route: 'sources', title: 'Sources', icon: 'database', scope: 'dataset', autoSynthesize: true },
|
|
37
37
|
{ type: 'about', route: 'about', title: 'About', icon: 'info', scope: 'dataset', featureFlag: '!about', autoSynthesize: true },
|
|
38
|
-
{ type: '
|
|
38
|
+
{ type: 'learn', route: 'learn', title: 'Learn', icon: 'book-open', scope: 'global', autoSynthesize: true },
|
|
39
39
|
] as const;
|
|
40
40
|
|
|
41
41
|
export type BuiltinPageType = typeof PAGE_TYPES[number]['type'];
|