@conduction/nextcloud-vue 2.3.0-beta.2 → 2.3.0-beta.3
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.3.0-beta.
|
|
3
|
+
"version": "2.3.0-beta.3",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"license": "EUPL-1.2",
|
|
6
6
|
"author": "Conduction B.V. <info@conduction.nl>",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
CnSiteCard,
|
|
17
17
|
CnSiteCardGrid,
|
|
18
18
|
CnSiteEmptyState,
|
|
19
|
+
CnSiteGlossary,
|
|
19
20
|
CnSiteHero,
|
|
20
21
|
CnSiteSearch,
|
|
21
22
|
CnSiteSection,
|
|
@@ -56,6 +57,7 @@ describe('public site blocks — vocabulary', () => {
|
|
|
56
57
|
expect(siteBlockRegistry.cardGrid).toBe(CnSiteCardGrid)
|
|
57
58
|
expect(siteBlockRegistry.card).toBe(CnSiteCard)
|
|
58
59
|
expect(siteBlockRegistry.emptyState).toBe(CnSiteEmptyState)
|
|
60
|
+
expect(siteBlockRegistry.glossary).toBe(CnSiteGlossary)
|
|
59
61
|
})
|
|
60
62
|
})
|
|
61
63
|
|
|
@@ -215,6 +217,78 @@ describe('public site blocks — markup contract', () => {
|
|
|
215
217
|
expect(wrapper.find('h3.utrecht-heading-3').exists()).toBe(true)
|
|
216
218
|
})
|
|
217
219
|
|
|
220
|
+
it('the glossary is a description list, not a stack of divs', () => {
|
|
221
|
+
// `<dl>`/`<dt>`/`<dd>` is what makes a screen reader announce "term,
|
|
222
|
+
// definition" pairs instead of an undifferentiated run of text. The
|
|
223
|
+
// two render identically, which is why this is easy to get wrong and
|
|
224
|
+
// impossible to notice by looking.
|
|
225
|
+
const wrapper = mount(CnSiteGlossary, {
|
|
226
|
+
props: {
|
|
227
|
+
terms: [
|
|
228
|
+
{ term: 'Publicatie', definition: 'Een document dat de gemeente openbaar maakt.' },
|
|
229
|
+
{ term: 'Woo-verzoek', definition: 'Een verzoek om openbaarmaking.' },
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
expect(wrapper.find('dl').exists()).toBe(true)
|
|
235
|
+
expect(wrapper.findAll('dt')).toHaveLength(2)
|
|
236
|
+
expect(wrapper.findAll('dd')).toHaveLength(2)
|
|
237
|
+
expect(wrapper.text()).toContain('Publicatie')
|
|
238
|
+
expect(wrapper.text()).toContain('Een verzoek om openbaarmaking.')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('renders synonyms, because the old name is often the only one a visitor has', () => {
|
|
242
|
+
// Someone searching for "Wob-verzoek" finds nothing if only the current
|
|
243
|
+
// term is rendered, and concludes the concept is gone rather than
|
|
244
|
+
// renamed.
|
|
245
|
+
const wrapper = mount(CnSiteGlossary, {
|
|
246
|
+
props: {
|
|
247
|
+
synonymsLabel: 'Ook bekend als:',
|
|
248
|
+
terms: [
|
|
249
|
+
{
|
|
250
|
+
term: 'Woo-verzoek',
|
|
251
|
+
definition: 'Een verzoek om openbaarmaking.',
|
|
252
|
+
synonyms: ['Wob-verzoek'],
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expect(wrapper.text()).toContain('Wob-verzoek')
|
|
259
|
+
expect(wrapper.text()).toContain('Ook bekend als:')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('treats a bare string synonym as ONE synonym, not one per character', () => {
|
|
263
|
+
// `synonyms` arrives as a string or an array depending on the store
|
|
264
|
+
// that produced it. Spreading the string renders `W, o, b, …` — which
|
|
265
|
+
// is a real list, correctly styled, and complete nonsense.
|
|
266
|
+
const wrapper = mount(CnSiteGlossary, {
|
|
267
|
+
props: { terms: [{ term: 'Woo-verzoek', definition: 'x', synonyms: 'Wob-verzoek' }] },
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(wrapper.text()).toContain('Wob-verzoek')
|
|
271
|
+
expect(wrapper.text()).not.toContain('W, o, b')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('says something when there are no terms', () => {
|
|
275
|
+
// A bare heading over nothing reads as a page that failed to load.
|
|
276
|
+
const wrapper = mount(CnSiteGlossary, {
|
|
277
|
+
props: { title: 'Begrippenlijst', emptyLabel: 'Nog geen begrippen.' },
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
expect(wrapper.find('dl').exists()).toBe(false)
|
|
281
|
+
expect(wrapper.text()).toContain('Nog geen begrippen.')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('the glossary heading class tracks its level', () => {
|
|
285
|
+
const wrapper = mount(CnSiteGlossary, {
|
|
286
|
+
props: { title: 'Begrippenlijst', headingLevel: 3, terms: [] },
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
expect(wrapper.find('h3.utrecht-heading-3').exists()).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
|
|
218
292
|
it('the card grid renders one card per entry and reflows by width', () => {
|
|
219
293
|
const wrapper = mount(CnSiteCardGrid, {
|
|
220
294
|
props: {
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="ac-glossary">
|
|
8
|
+
<component :is="headingTag" v-if="title" :class="headingClass">
|
|
9
|
+
{{ title }}
|
|
10
|
+
</component>
|
|
11
|
+
|
|
12
|
+
<p v-if="description" class="utrecht-paragraph">
|
|
13
|
+
{{ description }}
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
A DESCRIPTION LIST, not a stack of divs.
|
|
18
|
+
|
|
19
|
+
`<dl>`/`<dt>`/`<dd>` is what a glossary IS, and it is the difference
|
|
20
|
+
between a screen reader announcing "term, definition" pairs and
|
|
21
|
+
announcing an undifferentiated run of text. The visual result is
|
|
22
|
+
identical either way, which is why this is easy to get wrong.
|
|
23
|
+
-->
|
|
24
|
+
<dl v-if="terms.length" class="ac-glossary__list">
|
|
25
|
+
<div
|
|
26
|
+
v-for="(entry, index) in terms"
|
|
27
|
+
:key="entry.term || index"
|
|
28
|
+
class="ac-glossary__entry">
|
|
29
|
+
<dt class="ac-glossary__term utrecht-data-list__item-key">
|
|
30
|
+
{{ entry.term }}
|
|
31
|
+
</dt>
|
|
32
|
+
<dd class="ac-glossary__definition">
|
|
33
|
+
<p class="utrecht-paragraph">
|
|
34
|
+
{{ entry.definition }}
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<!--
|
|
38
|
+
SYNONYMS ARE PART OF THE DEFINITION, not a footnote.
|
|
39
|
+
|
|
40
|
+
A visitor searching for the word they know ("Wob-verzoek")
|
|
41
|
+
finds nothing if only the current term is rendered, and
|
|
42
|
+
concludes the concept is absent rather than renamed. The
|
|
43
|
+
old name is often the only one they have.
|
|
44
|
+
-->
|
|
45
|
+
<p
|
|
46
|
+
v-if="synonymsOf(entry).length"
|
|
47
|
+
class="utrecht-paragraph ac-glossary__synonyms">
|
|
48
|
+
{{ synonymsLabel }} {{ synonymsOf(entry).join(', ') }}
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p v-if="entry.source" class="utrecht-paragraph ac-glossary__source">
|
|
52
|
+
{{ sourceLabel }} {{ entry.source }}
|
|
53
|
+
</p>
|
|
54
|
+
</dd>
|
|
55
|
+
</div>
|
|
56
|
+
</dl>
|
|
57
|
+
|
|
58
|
+
<p v-else class="utrecht-paragraph ac-glossary__empty">
|
|
59
|
+
{{ emptyLabel }}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
/**
|
|
66
|
+
* A glossary: the terms a portal uses, with what each one means.
|
|
67
|
+
*
|
|
68
|
+
* WHY THIS IS A BLOCK AND NOT A PAGE
|
|
69
|
+
*
|
|
70
|
+
* The consuming portal used to render its glossary from a hard-coded
|
|
71
|
+
* `<section>` carrying a literal `<h2>Begrippenlijst</h2>`, on every page that
|
|
72
|
+
* satisfied a route check. A municipality could not move it, rename it,
|
|
73
|
+
* translate it, reorder it or leave it out, and it could only ever live at one
|
|
74
|
+
* URL. As a block it is content an author places — which is what it always
|
|
75
|
+
* was.
|
|
76
|
+
*
|
|
77
|
+
* WHY THE TERMS ARE A PROP
|
|
78
|
+
*
|
|
79
|
+
* Every string and every row arrives from the host. This entry point exists for
|
|
80
|
+
* pages served from a municipality's own domain, where there is no `OC` global,
|
|
81
|
+
* no session and no translation bundle; a component that fetched its own data
|
|
82
|
+
* or called `t()` would fail in a browser, on a live page, rather than at build
|
|
83
|
+
* time. The host already holds these rows — it fetched them over its own public
|
|
84
|
+
* contract — so passing them down costs nothing and keeps this component
|
|
85
|
+
* portable.
|
|
86
|
+
*
|
|
87
|
+
* THE LABELS ARE PROPS FOR THE SAME REASON. "Synoniemen" is not a word this
|
|
88
|
+
* library is entitled to choose on behalf of a Dutch government portal, and
|
|
89
|
+
* hard-coding an English default would put the wrong language on the page for
|
|
90
|
+
* every consumer this entry point was built for.
|
|
91
|
+
*
|
|
92
|
+
* PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import.
|
|
93
|
+
*/
|
|
94
|
+
export default {
|
|
95
|
+
name: 'CnSiteGlossary',
|
|
96
|
+
|
|
97
|
+
props: {
|
|
98
|
+
/**
|
|
99
|
+
* The glossary rows.
|
|
100
|
+
*
|
|
101
|
+
* Each entry is `{ term, definition, synonyms?, source? }`. `synonyms`
|
|
102
|
+
* tolerates a string or an array, because a single synonym arrives as
|
|
103
|
+
* either depending on the store that produced it, and a component that
|
|
104
|
+
* rendered `W,o,b` one character per row would be technically correct.
|
|
105
|
+
*/
|
|
106
|
+
terms: {
|
|
107
|
+
type: Array,
|
|
108
|
+
default: () => [],
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/** Heading above the list; '' renders none. */
|
|
112
|
+
title: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: '',
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/** Supporting line under the heading. */
|
|
118
|
+
description: {
|
|
119
|
+
type: String,
|
|
120
|
+
default: '',
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Heading level, so the page outline stays intact.
|
|
125
|
+
*
|
|
126
|
+
* The design system styles `.utrecht-heading-2`, not `h2`, so the class
|
|
127
|
+
* tracks the level too — a host changing the level to keep an outline
|
|
128
|
+
* intact must not silently lose the styling with it.
|
|
129
|
+
*/
|
|
130
|
+
headingLevel: {
|
|
131
|
+
type: Number,
|
|
132
|
+
default: 2,
|
|
133
|
+
validator: (v) => v >= 1 && v <= 6,
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/** Prefix for the synonyms line, e.g. 'Ook bekend als:'. */
|
|
137
|
+
synonymsLabel: {
|
|
138
|
+
type: String,
|
|
139
|
+
default: '',
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/** Prefix for the source line, e.g. 'Bron:'. */
|
|
143
|
+
sourceLabel: {
|
|
144
|
+
type: String,
|
|
145
|
+
default: '',
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* What to say when there are no terms.
|
|
150
|
+
*
|
|
151
|
+
* An empty glossary renders this sentence rather than a bare heading
|
|
152
|
+
* over nothing, which reads as a page that failed to load.
|
|
153
|
+
*/
|
|
154
|
+
emptyLabel: {
|
|
155
|
+
type: String,
|
|
156
|
+
default: '',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
computed: {
|
|
161
|
+
/**
|
|
162
|
+
* @return {string} The heading element to render.
|
|
163
|
+
*/
|
|
164
|
+
headingTag() {
|
|
165
|
+
return `h${this.headingLevel}`
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @return {string} The heading's class, tracking its level.
|
|
170
|
+
*/
|
|
171
|
+
headingClass() {
|
|
172
|
+
return `utrecht-heading-${this.headingLevel}`
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
methods: {
|
|
177
|
+
/**
|
|
178
|
+
* A term's synonyms, as a list, whatever shape they arrived in.
|
|
179
|
+
*
|
|
180
|
+
* @param {object} entry One glossary row.
|
|
181
|
+
* @return {string[]} The synonyms, empty when there are none.
|
|
182
|
+
*/
|
|
183
|
+
synonymsOf(entry) {
|
|
184
|
+
const raw = entry && entry.synonyms
|
|
185
|
+
if (!raw) {
|
|
186
|
+
return []
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A bare string is ONE synonym. Spreading it would render each
|
|
190
|
+
// character as its own entry.
|
|
191
|
+
if (typeof raw === 'string') {
|
|
192
|
+
return raw.trim() ? [raw.trim()] : []
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Array.isArray(raw) ? raw.filter(Boolean) : []
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<style scoped>
|
|
202
|
+
/*
|
|
203
|
+
* Layout and rhythm only — no colours.
|
|
204
|
+
*
|
|
205
|
+
* A block lands on whatever surface a host puts it on, and this codebase has
|
|
206
|
+
* three separate defects on record from rules that coloured text without
|
|
207
|
+
* reference to its background. The design system colours
|
|
208
|
+
* `.utrecht-heading-*` and `.utrecht-paragraph` already.
|
|
209
|
+
*/
|
|
210
|
+
.ac-glossary__list {
|
|
211
|
+
margin-block-start: var(--utrecht-space-block-lg, 1.5rem);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.ac-glossary__entry + .ac-glossary__entry {
|
|
215
|
+
margin-block-start: var(--utrecht-space-block-lg, 1.5rem);
|
|
216
|
+
padding-block-start: var(--utrecht-space-block-lg, 1.5rem);
|
|
217
|
+
/*
|
|
218
|
+
* `currentColor` at low alpha rather than a colour token: the rule has to
|
|
219
|
+
* be visible on a light band and on a dark one, and it cannot know which
|
|
220
|
+
* it is on.
|
|
221
|
+
*/
|
|
222
|
+
border-block-start: 1px solid color-mix(in srgb, currentColor 15%, transparent);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.ac-glossary__term {
|
|
226
|
+
font-weight: 700;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.ac-glossary__definition {
|
|
230
|
+
/* `<dd>` carries a UA indent that misaligns the definition with its term. */
|
|
231
|
+
margin-inline-start: 0;
|
|
232
|
+
margin-block-start: var(--utrecht-space-block-sm, 0.5rem);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.ac-glossary__synonyms,
|
|
236
|
+
.ac-glossary__source {
|
|
237
|
+
margin-block-start: var(--utrecht-space-block-sm, 0.5rem);
|
|
238
|
+
font-size: 0.875em;
|
|
239
|
+
}
|
|
240
|
+
</style>
|
package/src/public/index.js
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
import CnSiteCard from './components/CnSiteCard.vue'
|
|
44
44
|
import CnSiteCardGrid from './components/CnSiteCardGrid.vue'
|
|
45
45
|
import CnSiteEmptyState from './components/CnSiteEmptyState.vue'
|
|
46
|
+
import CnSiteGlossary from './components/CnSiteGlossary.vue'
|
|
46
47
|
import CnSiteHero from './components/CnSiteHero.vue'
|
|
47
48
|
import CnSiteIcon from './components/CnSiteIcon.vue'
|
|
48
49
|
import CnSiteSearch from './components/CnSiteSearch.vue'
|
|
@@ -52,6 +53,7 @@ export {
|
|
|
52
53
|
CnSiteCard,
|
|
53
54
|
CnSiteCardGrid,
|
|
54
55
|
CnSiteEmptyState,
|
|
56
|
+
CnSiteGlossary,
|
|
55
57
|
CnSiteHero,
|
|
56
58
|
CnSiteIcon,
|
|
57
59
|
CnSiteSearch,
|
|
@@ -79,6 +81,7 @@ export const siteBlockRegistry = {
|
|
|
79
81
|
cardGrid: CnSiteCardGrid,
|
|
80
82
|
card: CnSiteCard,
|
|
81
83
|
emptyState: CnSiteEmptyState,
|
|
84
|
+
glossary: CnSiteGlossary,
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/**
|