@conduction/nextcloud-vue 2.3.0 → 2.4.0
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 +18 -2
- package/scripts/check-public-safe.js +115 -0
- package/src/public/__tests__/publicBlocks.spec.js +307 -0
- package/src/public/components/CnSiteCard.vue +178 -0
- package/src/public/components/CnSiteCardGrid.vue +98 -0
- package/src/public/components/CnSiteEmptyState.vue +172 -0
- package/src/public/components/CnSiteGlossary.vue +240 -0
- package/src/public/components/CnSiteHero.vue +239 -0
- package/src/public/components/CnSiteIcon.vue +90 -0
- package/src/public/components/CnSiteSearch.vue +173 -0
- package/src/public/components/CnSiteSection.vue +120 -0
- package/src/public/index.js +133 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<CnSiteSection variant="hero" :background-image="backgroundImage">
|
|
8
|
+
<!--
|
|
9
|
+
THE HEADING IS REAL BUT NOT NECESSARILY PAINTED, and that is a
|
|
10
|
+
deliberate improvement on the reference rather than a copy of it.
|
|
11
|
+
|
|
12
|
+
Measured on the NL Design System reference home page: the hero
|
|
13
|
+
contains NO heading element at all (its prompt lives inside the
|
|
14
|
+
search form) and the page therefore has no `h1` anywhere. Copying
|
|
15
|
+
that would inherit an outline defect.
|
|
16
|
+
|
|
17
|
+
Painting a heading directly on the band is the other trap. The band
|
|
18
|
+
is dark — `rgb(0, 56, 101)` on the reference — while the hero's own
|
|
19
|
+
computed colour is black, because the design system never puts bare
|
|
20
|
+
text there and so has no rule for it. A heading dropped onto that
|
|
21
|
+
band renders dark-on-dark: present, selectable, and unreadable.
|
|
22
|
+
(Exactly the failure that made this portal's footer links
|
|
23
|
+
`rgb(0, 68, 136)` on `rgb(0, 69, 137)`.)
|
|
24
|
+
|
|
25
|
+
So when the search box is shown, the heading stays in the DOM for
|
|
26
|
+
the outline and the VISIBLE prompt is the search label, which the
|
|
27
|
+
design system does style for this band. A host that wants a painted
|
|
28
|
+
heading asks for it with `heading-visible`.
|
|
29
|
+
-->
|
|
30
|
+
<component
|
|
31
|
+
:is="headingTag"
|
|
32
|
+
v-if="title"
|
|
33
|
+
:class="headingClass">
|
|
34
|
+
{{ title }}
|
|
35
|
+
</component>
|
|
36
|
+
|
|
37
|
+
<p v-if="subtitle" :class="subtitleClass">
|
|
38
|
+
{{ subtitle }}
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<!--
|
|
42
|
+
THE CARD IS WHAT SIZES THE SEARCH BOX, and it is not decoration.
|
|
43
|
+
Captured from the reference:
|
|
44
|
+
|
|
45
|
+
.ac-hero > .container > .ac-card.ac-card--blue.ac-card--padding-lg
|
|
46
|
+
> .ac-card__content > form.ac-search-box
|
|
47
|
+
|
|
48
|
+
`nlds-app.css` says `.ac-hero .ac-card { inline-size: min(100%, 744px) }`,
|
|
49
|
+
and the card's padding takes the form from 744 down to 648. Rendered
|
|
50
|
+
without it the form simply filled the content column — measured 1168
|
|
51
|
+
against the design's 648 — because nothing else in the cascade
|
|
52
|
+
constrains it.
|
|
53
|
+
-->
|
|
54
|
+
<div v-if="search" class="ac-card ac-card--blue ac-card--padding-lg">
|
|
55
|
+
<div class="ac-card__content">
|
|
56
|
+
<CnSiteSearch
|
|
57
|
+
:label="effectiveSearchLabel"
|
|
58
|
+
:label-visible="true"
|
|
59
|
+
:placeholder="searchPlaceholder"
|
|
60
|
+
:submit-label="searchSubmitLabel"
|
|
61
|
+
:value="searchValue"
|
|
62
|
+
:input-id="searchInputId"
|
|
63
|
+
@search="$emit('search', $event)" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<slot />
|
|
68
|
+
</CnSiteSection>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import CnSiteSearch from './CnSiteSearch.vue'
|
|
73
|
+
import CnSiteSection from './CnSiteSection.vue'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The leading band of a site page.
|
|
77
|
+
*
|
|
78
|
+
* Composes the section band and, optionally, the search box — the shape the NL
|
|
79
|
+
* Design System reference uses on its home page, where the hero IS the search
|
|
80
|
+
* entry point rather than decoration above one.
|
|
81
|
+
*
|
|
82
|
+
* The search is opt-in (`search`), because a hero that always renders a search
|
|
83
|
+
* field would force every portal to either wire up a query back end or ship an
|
|
84
|
+
* inert control. An inert search box is worse than none: it invites the one
|
|
85
|
+
* interaction it cannot honour.
|
|
86
|
+
*
|
|
87
|
+
* PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import.
|
|
88
|
+
*/
|
|
89
|
+
export default {
|
|
90
|
+
name: 'CnSiteHero',
|
|
91
|
+
|
|
92
|
+
components: { CnSiteSearch, CnSiteSection },
|
|
93
|
+
|
|
94
|
+
props: {
|
|
95
|
+
/** The hero heading. */
|
|
96
|
+
title: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: '',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/** Supporting line under the heading. */
|
|
102
|
+
subtitle: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: '',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/** Heading level, so the page outline stays intact. */
|
|
108
|
+
headingLevel: {
|
|
109
|
+
type: Number,
|
|
110
|
+
default: 1,
|
|
111
|
+
validator: (v) => v >= 1 && v <= 6,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** Whether to render the search box. */
|
|
115
|
+
search: {
|
|
116
|
+
type: Boolean,
|
|
117
|
+
default: false,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Accessible name for the search landmark.
|
|
122
|
+
*
|
|
123
|
+
* EMPTY BY DEFAULT so `title` can serve as the prompt. This defaulted
|
|
124
|
+
* to 'Zoeken', which made `searchLabel || title` dead code — the
|
|
125
|
+
* fallback could never fire, and a host that set only `title` got the
|
|
126
|
+
* generic word instead of its own question. Caught by the test that
|
|
127
|
+
* asserts the visible label carries the hero's words.
|
|
128
|
+
*/
|
|
129
|
+
searchLabel: {
|
|
130
|
+
type: String,
|
|
131
|
+
default: '',
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** Placeholder inside the search field. */
|
|
135
|
+
searchPlaceholder: {
|
|
136
|
+
type: String,
|
|
137
|
+
default: '',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/** Visible text on the search button. */
|
|
141
|
+
searchSubmitLabel: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: 'Zoeken',
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/** Pre-filled search term. */
|
|
147
|
+
searchValue: {
|
|
148
|
+
type: String,
|
|
149
|
+
default: '',
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/** DOM id for the search input. */
|
|
153
|
+
searchInputId: {
|
|
154
|
+
type: String,
|
|
155
|
+
default: 'cn-site-search',
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Optional background image for the band.
|
|
160
|
+
*
|
|
161
|
+
* The reference's hero carries one (`cover`, centred) over its blue.
|
|
162
|
+
* A URL rather than a token because it is CONTENT — each portal's own
|
|
163
|
+
* photograph — not a design decision the token set can make.
|
|
164
|
+
*/
|
|
165
|
+
backgroundImage: {
|
|
166
|
+
type: String,
|
|
167
|
+
default: '',
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Paint the heading on the band.
|
|
172
|
+
*
|
|
173
|
+
* Defaults to the safe answer: visible only when there is no search box
|
|
174
|
+
* to carry the prompt. Forcing it on a dark band without a colour rule
|
|
175
|
+
* for it is how a heading becomes invisible.
|
|
176
|
+
*/
|
|
177
|
+
headingVisible: {
|
|
178
|
+
type: Boolean,
|
|
179
|
+
default: null,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
emits: ['search'],
|
|
184
|
+
|
|
185
|
+
computed: {
|
|
186
|
+
/**
|
|
187
|
+
* @return {string} The heading element to render.
|
|
188
|
+
*/
|
|
189
|
+
headingTag() {
|
|
190
|
+
return `h${this.headingLevel}`
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The search box's accessible name.
|
|
195
|
+
*
|
|
196
|
+
* Prefers an explicit `searchLabel`, then the hero's own `title` — the
|
|
197
|
+
* reference implementation's hero puts its question inside the form, so
|
|
198
|
+
* the title IS the prompt there. Falls back to a generic word only when
|
|
199
|
+
* a host supplies neither, because an unnamed search field is worse
|
|
200
|
+
* than a generically named one.
|
|
201
|
+
*
|
|
202
|
+
* @return {string} A non-empty label.
|
|
203
|
+
*/
|
|
204
|
+
effectiveSearchLabel() {
|
|
205
|
+
return this.searchLabel || this.title || 'Zoeken'
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Whether the heading is painted.
|
|
210
|
+
*
|
|
211
|
+
* @return {boolean} True when it should be visible.
|
|
212
|
+
*/
|
|
213
|
+
showHeading() {
|
|
214
|
+
if (this.headingVisible !== null) {
|
|
215
|
+
return this.headingVisible
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// With a search box the label carries the prompt, so a painted
|
|
219
|
+
// heading would duplicate it — and duplicate it in the one place
|
|
220
|
+
// the design system has no text colour for.
|
|
221
|
+
return this.search === false
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @return {Array} Classes for the heading.
|
|
226
|
+
*/
|
|
227
|
+
headingClass() {
|
|
228
|
+
return ['ac-hero__title', this.showHeading ? null : 'sr-only'].filter(Boolean)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @return {Array} Classes for the subtitle.
|
|
233
|
+
*/
|
|
234
|
+
subtitleClass() {
|
|
235
|
+
return ['ac-hero__subtitle', this.showHeading ? null : 'sr-only'].filter(Boolean)
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<svg
|
|
8
|
+
v-if="path"
|
|
9
|
+
class="ac-icon"
|
|
10
|
+
:class="`ac-icon--${name}`"
|
|
11
|
+
:width="size"
|
|
12
|
+
:height="size"
|
|
13
|
+
viewBox="0 0 24 24"
|
|
14
|
+
fill="none"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
focusable="false">
|
|
17
|
+
<path :d="path" fill="currentColor" />
|
|
18
|
+
</svg>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
/**
|
|
23
|
+
* A small inline icon from a CLOSED vocabulary.
|
|
24
|
+
*
|
|
25
|
+
* WHY INLINE SVG AND NOT AN ICON FONT
|
|
26
|
+
*
|
|
27
|
+
* An icon font is a webfont, and this codebase has already been bitten by
|
|
28
|
+
* exactly that: the reference application's stylesheets declare `ac-icons` with
|
|
29
|
+
* a root-relative url, which from a portal's own path resolves against the
|
|
30
|
+
* ORIGIN and 404s. A missing icon font does not fail loudly — it renders a
|
|
31
|
+
* blank box or a stray letter, and the page looks finished.
|
|
32
|
+
*
|
|
33
|
+
* WHY A CLOSED SET AND NOT AN ARBITRARY `path` PROP
|
|
34
|
+
*
|
|
35
|
+
* Page content is authored data. Letting an author supply raw SVG path data
|
|
36
|
+
* would put attacker-controlled markup inside an `<svg>` on a public government
|
|
37
|
+
* page. A name resolved against a fixed map cannot express anything the map
|
|
38
|
+
* does not already contain, and an unknown name renders NOTHING rather than
|
|
39
|
+
* guessing.
|
|
40
|
+
*
|
|
41
|
+
* ALWAYS DECORATIVE. Every icon here sits beside its own visible label, so it
|
|
42
|
+
* is `aria-hidden` with `focusable="false"`. An icon that repeats the adjacent
|
|
43
|
+
* text is one more thing announced for no gain, and `focusable="false"` keeps
|
|
44
|
+
* IE-era SVG out of the tab order.
|
|
45
|
+
*
|
|
46
|
+
* PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The icon vocabulary. Paths are 24x24, single-path, currentColor.
|
|
51
|
+
*
|
|
52
|
+
* @type {Record<string, string>}
|
|
53
|
+
*/
|
|
54
|
+
const ICON_PATHS = {
|
|
55
|
+
search: 'M10 4a6 6 0 1 0 3.7 10.72l4.29 4.29a1 1 0 0 0 1.42-1.42l-4.29-4.29A6 6 0 0 0 10 4Zm-4 6a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z',
|
|
56
|
+
document: 'M6 2h7l5 5v15H6V2Zm7 1.5V8h4.5L13 3.5ZM8 12h8v1.5H8V12Zm0 4h8v1.5H8V16Z',
|
|
57
|
+
globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm6.9 9h-3a15.6 15.6 0 0 0-1.2-5.4A8 8 0 0 1 18.9 11ZM12 4.2c.8 1.2 1.5 3.4 1.7 6.8h-3.4c.2-3.4.9-5.6 1.7-6.8ZM5.1 11a8 8 0 0 1 4.2-5.4A15.6 15.6 0 0 0 8.1 11h-3Zm0 2h3a15.6 15.6 0 0 0 1.2 5.4A8 8 0 0 1 5.1 13Zm6.9 6.8c-.8-1.2-1.5-3.4-1.7-6.8h3.4c-.2 3.4-.9 5.6-1.7 6.8Zm2.7-1.4a15.6 15.6 0 0 0 1.2-5.4h3a8 8 0 0 1-4.2 5.4Z',
|
|
58
|
+
'external-link': 'M14 3h7v7h-2V6.41l-9.29 9.3-1.42-1.42 9.3-9.29H14V3ZM5 5h5v2H7v10h10v-3h2v5H5V5Z',
|
|
59
|
+
'arrow-right': 'M13.17 5.17 12 6.34 16.66 11H4v2h12.66L12 17.66l1.17 1.17L20 12l-6.83-6.83Z',
|
|
60
|
+
information: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 15h-2v-6h2v6Zm0-8h-2V7h2v2Z',
|
|
61
|
+
email: 'M4 4h16v16H4V4Zm2 2v.5l6 4 6-4V6H6Zm12 3-6 4-6-4v9h12V9Z',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default {
|
|
65
|
+
name: 'CnSiteIcon',
|
|
66
|
+
|
|
67
|
+
props: {
|
|
68
|
+
/** Icon name from the closed vocabulary. Unknown names render nothing. */
|
|
69
|
+
name: {
|
|
70
|
+
type: String,
|
|
71
|
+
required: true,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/** Square size in pixels. */
|
|
75
|
+
size: {
|
|
76
|
+
type: [Number, String],
|
|
77
|
+
default: 20,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
computed: {
|
|
82
|
+
/**
|
|
83
|
+
* @return {string} The path data, or '' when the name is unknown.
|
|
84
|
+
*/
|
|
85
|
+
path() {
|
|
86
|
+
return ICON_PATHS[this.name] || ''
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<form
|
|
8
|
+
class="ac-search-box ac-search-box--home"
|
|
9
|
+
role="search"
|
|
10
|
+
:aria-label="label"
|
|
11
|
+
@submit.prevent="submit">
|
|
12
|
+
<!--
|
|
13
|
+
A LABEL, not just a placeholder. A placeholder disappears the moment
|
|
14
|
+
the field has content and is not reliably announced, so a
|
|
15
|
+
placeholder-only search box leaves a screen-reader user with an
|
|
16
|
+
unnamed text input.
|
|
17
|
+
|
|
18
|
+
VISIBLE BY DEFAULT when the host asks for it, because on the NL
|
|
19
|
+
Design System reference the prompt IS this label — measured on its
|
|
20
|
+
home page, the hero contains no heading element at all and the
|
|
21
|
+
question sits inside the form. Hiding it there would delete the only
|
|
22
|
+
visible prompt on the band.
|
|
23
|
+
-->
|
|
24
|
+
<!--
|
|
25
|
+
Class names captured from the running reference rather than guessed —
|
|
26
|
+
`utrecht-form-label`, `utrecht-textbox--html-input` and
|
|
27
|
+
`utrecht-button--primary-action` are what the design system's CSS
|
|
28
|
+
actually selects on. Without the primary-action modifier the submit
|
|
29
|
+
button renders as the SUBTLE variant: measured, transparent
|
|
30
|
+
background and rgb(10, 39, 80) text where the design wants a filled
|
|
31
|
+
rgb(0, 68, 136) button with white text.
|
|
32
|
+
-->
|
|
33
|
+
<div class="ac-flex ac-flex--column ac-flex--spacing-sm">
|
|
34
|
+
<label :class="labelClass" :for="inputId">
|
|
35
|
+
{{ label }}
|
|
36
|
+
</label>
|
|
37
|
+
|
|
38
|
+
<div class="ac-search-box__search">
|
|
39
|
+
<input
|
|
40
|
+
:id="inputId"
|
|
41
|
+
v-model="term"
|
|
42
|
+
type="text"
|
|
43
|
+
name="q"
|
|
44
|
+
class="ac-search-box__input utrecht-textbox utrecht-textbox--html-input"
|
|
45
|
+
:placeholder="placeholder"
|
|
46
|
+
autocomplete="off">
|
|
47
|
+
<button
|
|
48
|
+
type="submit"
|
|
49
|
+
class="ac-search-box__button utrecht-button utrecht-button--submit utrecht-button--primary-action">
|
|
50
|
+
<!--
|
|
51
|
+
DECORATIVE, so `aria-hidden` and no title: the button
|
|
52
|
+
already has a text label beside it, and an icon that
|
|
53
|
+
repeats it is one more thing for a screen reader to read
|
|
54
|
+
out for no gain.
|
|
55
|
+
-->
|
|
56
|
+
<svg
|
|
57
|
+
class="ac-search-box__search-icon"
|
|
58
|
+
width="20"
|
|
59
|
+
height="20"
|
|
60
|
+
viewBox="0 0 20 20"
|
|
61
|
+
fill="none"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
focusable="false">
|
|
64
|
+
<path
|
|
65
|
+
d="M8.5 3a5.5 5.5 0 1 0 3.383 9.83l3.643 3.644a.75.75 0 1 0 1.061-1.06l-3.644-3.644A5.5 5.5 0 0 0 8.5 3Zm-4 5.5a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z"
|
|
66
|
+
fill="currentColor" />
|
|
67
|
+
</svg>
|
|
68
|
+
{{ submitLabel }}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</form>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<script>
|
|
76
|
+
/**
|
|
77
|
+
* The site search box.
|
|
78
|
+
*
|
|
79
|
+
* Emits `search` with the term and does NOT fetch anything itself. A block
|
|
80
|
+
* that owned its own transport could only ever work against one back end,
|
|
81
|
+
* which is the opposite of what a shared library is for — the host decides
|
|
82
|
+
* where a query goes.
|
|
83
|
+
*
|
|
84
|
+
* It is a real `<form>` with a submit button so that Enter works and the
|
|
85
|
+
* control is reachable and operable by keyboard without any script.
|
|
86
|
+
*
|
|
87
|
+
* PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import. Labels arrive as
|
|
88
|
+
* props rather than through `t()`, because the translation layer is exactly
|
|
89
|
+
* the dependency that makes a component unusable outside Nextcloud.
|
|
90
|
+
*/
|
|
91
|
+
export default {
|
|
92
|
+
name: 'CnSiteSearch',
|
|
93
|
+
|
|
94
|
+
props: {
|
|
95
|
+
/** Accessible name for the search landmark and its input. */
|
|
96
|
+
label: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: 'Zoeken',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/** Placeholder shown inside the field. */
|
|
102
|
+
placeholder: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: '',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/** Visible text on the submit button. */
|
|
108
|
+
submitLabel: {
|
|
109
|
+
type: String,
|
|
110
|
+
default: 'Zoeken',
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/** Pre-filled term, so a results page can round-trip the query. */
|
|
114
|
+
value: {
|
|
115
|
+
type: String,
|
|
116
|
+
default: '',
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/** DOM id for the input, so the label can reference it. */
|
|
120
|
+
inputId: {
|
|
121
|
+
type: String,
|
|
122
|
+
default: 'cn-site-search',
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether the label is shown or only exposed to assistive tech.
|
|
127
|
+
*
|
|
128
|
+
* It is ALWAYS in the DOM either way — this only decides whether it is
|
|
129
|
+
* painted. A search box whose label is hidden with `display: none`
|
|
130
|
+
* would be hidden from screen readers too, which is why the hidden
|
|
131
|
+
* state is a clip-based `sr-only`, not a display toggle.
|
|
132
|
+
*/
|
|
133
|
+
labelVisible: {
|
|
134
|
+
type: Boolean,
|
|
135
|
+
default: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
emits: ['search'],
|
|
140
|
+
|
|
141
|
+
data() {
|
|
142
|
+
return {
|
|
143
|
+
term: this.value,
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
computed: {
|
|
148
|
+
/**
|
|
149
|
+
* @return {Array} Classes for the label.
|
|
150
|
+
*/
|
|
151
|
+
labelClass() {
|
|
152
|
+
return ['ac-search-box__label', 'utrecht-form-label', this.labelVisible ? null : 'sr-only'].filter(Boolean)
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
watch: {
|
|
157
|
+
value(next) {
|
|
158
|
+
this.term = next
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
methods: {
|
|
163
|
+
/**
|
|
164
|
+
* Hand the term to the host.
|
|
165
|
+
*
|
|
166
|
+
* @return {void}
|
|
167
|
+
*/
|
|
168
|
+
submit() {
|
|
169
|
+
this.$emit('search', this.term)
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
</script>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<section :class="sectionClass" :style="sectionStyle">
|
|
8
|
+
<div class="container">
|
|
9
|
+
<slot />
|
|
10
|
+
</div>
|
|
11
|
+
</section>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
/**
|
|
16
|
+
* A full-bleed band with a constrained reading column inside it.
|
|
17
|
+
*
|
|
18
|
+
* The band paints edge to edge and the `.container` holds the content at the
|
|
19
|
+
* design system's column width. That split is not cosmetic: every band in the
|
|
20
|
+
* NL Design System reference works this way, and a region that emits its
|
|
21
|
+
* content WITHOUT a container renders body copy hard against the viewport edge
|
|
22
|
+
* while the navigation above and the footer below start inset.
|
|
23
|
+
*
|
|
24
|
+
* PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import, directly or
|
|
25
|
+
* transitively. This component must render at a public origin where there is
|
|
26
|
+
* no Nextcloud, no `OC` global and no translation bundle.
|
|
27
|
+
*/
|
|
28
|
+
export default {
|
|
29
|
+
name: 'CnSiteSection',
|
|
30
|
+
|
|
31
|
+
props: {
|
|
32
|
+
/**
|
|
33
|
+
* Vertical rhythm. `spacing` adds the design system's block padding;
|
|
34
|
+
* `hero` is the taller leading band.
|
|
35
|
+
*/
|
|
36
|
+
variant: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: 'spacing',
|
|
39
|
+
validator: (v) => ['spacing', 'hero', 'flush'].includes(v),
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** Optional background image URL, layered over the band's colour. */
|
|
43
|
+
backgroundImage: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: '',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
computed: {
|
|
50
|
+
/**
|
|
51
|
+
* Inline background, when the host supplies an image.
|
|
52
|
+
*
|
|
53
|
+
* Inline rather than a class because the URL is per-portal CONTENT; a
|
|
54
|
+
* stylesheet cannot enumerate them. `background-image` is layered OVER
|
|
55
|
+
* the band's own colour rather than replacing it, so a slow or blocked
|
|
56
|
+
* image leaves the blue behind it and the text stays readable — an
|
|
57
|
+
* image that fails is not allowed to become white-on-white.
|
|
58
|
+
*
|
|
59
|
+
* @return {object|null} Style bindings, or null.
|
|
60
|
+
*/
|
|
61
|
+
sectionStyle() {
|
|
62
|
+
if (this.backgroundImage === '') {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
backgroundImage: `url(${JSON.stringify(this.backgroundImage)})`,
|
|
68
|
+
backgroundSize: 'cover',
|
|
69
|
+
backgroundPosition: '50% 50%',
|
|
70
|
+
backgroundRepeat: 'no-repeat',
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @return {Array} The band's classes.
|
|
76
|
+
*/
|
|
77
|
+
sectionClass() {
|
|
78
|
+
return [
|
|
79
|
+
'ac-section',
|
|
80
|
+
this.variant === 'hero' ? 'ac-hero' : null,
|
|
81
|
+
this.variant === 'spacing' ? 'ac-section--spacing' : null,
|
|
82
|
+
].filter(Boolean)
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<style scoped>
|
|
89
|
+
/*
|
|
90
|
+
* ONLY THE TEXT THAT ACTUALLY SITS ON THE BAND IS RECOLOURED.
|
|
91
|
+
*
|
|
92
|
+
* `.ac-hero` paints itself `--tilburg-color-blue-500` — dark — and defines no
|
|
93
|
+
* colour for text placed directly on it, because the reference implementation
|
|
94
|
+
* never puts any there: its hero holds a white input and a blue button, both
|
|
95
|
+
* of which bring their own surface. So a heading or paragraph dropped onto the
|
|
96
|
+
* band inherits the document's dark body colour and lands dark-on-dark.
|
|
97
|
+
*
|
|
98
|
+
* THIS RULE WAS FIRST WRITTEN ON `.ac-hero` ITSELF, AND THAT WAS WRONG — it is
|
|
99
|
+
* the very mistake the rest of this codebase keeps recording. Colour set on an
|
|
100
|
+
* ancestor cannot know that a descendant paints its own background:
|
|
101
|
+
* `.ac-search-box` has one, `rgb(230, 246, 255)`, and inheriting white into it
|
|
102
|
+
* put the label and button at contrast 1.11 and the input's text at 1.0.
|
|
103
|
+
*
|
|
104
|
+
* Worse, the "bug" that prompted it was a MEASUREMENT ERROR. The label
|
|
105
|
+
* computed `rgb(51, 51, 51)`, which was compared against the BAND and scored
|
|
106
|
+
* 1.06 — but the label does not sit on the band, it sits on the search box's
|
|
107
|
+
* own light surface, where that colour is entirely correct. The contrast probe
|
|
108
|
+
* has to walk up to the first ancestor that actually paints a background;
|
|
109
|
+
* against the nearest NAMED band it reports a failure that is not there, and
|
|
110
|
+
* then invites a fix that breaks something real.
|
|
111
|
+
*
|
|
112
|
+
* So: the hero's own title and subtitle, and nothing else. The system's
|
|
113
|
+
* inverse token rather than a literal, so a theme with a light hero corrects
|
|
114
|
+
* both together.
|
|
115
|
+
*/
|
|
116
|
+
.ac-hero__title,
|
|
117
|
+
.ac-hero__subtitle {
|
|
118
|
+
color: var(--utrecht-document-inverse-color, #ffffff);
|
|
119
|
+
}
|
|
120
|
+
</style>
|