@conduction/nextcloud-vue 2.3.0-beta.1 → 2.3.0-beta.2

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,172 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div
8
+ :class="['ac-empty-state', `ac-empty-state--${variant}`]"
9
+ :role="variant === 'error' ? 'alert' : undefined"
10
+ :aria-busy="variant === 'loading' ? 'true' : undefined"
11
+ :aria-live="variant === 'loading' ? 'polite' : undefined">
12
+ <CnSiteIcon v-if="icon" :name="icon" :size="32" />
13
+
14
+ <component :is="headingTag" v-if="title" :class="headingClass">
15
+ {{ title }}
16
+ </component>
17
+
18
+ <p v-if="description" class="utrecht-paragraph">
19
+ {{ description }}
20
+ </p>
21
+
22
+ <a v-if="link" class="utrecht-link utrecht-link--html-a" :href="link">
23
+ {{ linkLabel || link }}
24
+ </a>
25
+
26
+ <slot />
27
+ </div>
28
+ </template>
29
+
30
+ <script>
31
+ import CnSiteIcon from './CnSiteIcon.vue'
32
+
33
+ /**
34
+ * The state a page shows when there is nothing to show yet, nothing to show at
35
+ * all, or nothing showable because something failed.
36
+ *
37
+ * WHY THIS IS NOT `NcEmptyContent`
38
+ *
39
+ * Nextcloud's own empty-state component is the obvious reuse and it cannot be
40
+ * used here: it comes from `@nextcloud/vue`, which is exactly the runtime
41
+ * coupling this entry point exists to keep out. A portal served from a
42
+ * municipality's own domain has no `OC` global, no session and no translation
43
+ * bundle, and a component reaching for them fails in a browser rather than at
44
+ * build time. The transitive-import guard in this package's CI would reject the
45
+ * import outright.
46
+ *
47
+ * WHY THE THREE VARIANTS ARE ONE COMPONENT
48
+ *
49
+ * `loading`, `empty` and `error` differ in what they ANNOUNCE, not in what they
50
+ * look like, and getting the announcement right is the entire point of having a
51
+ * component rather than a paragraph:
52
+ *
53
+ * loading aria-busy + aria-live="polite" — a screen reader is told the
54
+ * region is working and will be told again when it settles, rather
55
+ * than being handed a silent blank area
56
+ * error role="alert" — announced immediately, because a visitor who
57
+ * cannot see the page must not sit waiting for content that will
58
+ * never arrive
59
+ * empty neither. "There is nothing here" is ordinary content, and
60
+ * announcing it as an alert cries wolf.
61
+ *
62
+ * Splitting them into three components would let a caller pick the wrong
63
+ * announcement for the right visual, which is precisely the mistake this
64
+ * prevents.
65
+ *
66
+ * IT REPLACES A BARE SENTENCE. The portal previously rendered `<p>Bezig met
67
+ * laden…</p>` — no landmark, no live region, no icon, unstyled, and identical
68
+ * in markup to an error. A sighted visitor saw a stray line under the header; a
69
+ * screen-reader user was told nothing at all.
70
+ *
71
+ * PUBLIC-SAFE (see ../index.js): no `@nextcloud/*` import. All copy arrives as
72
+ * props, because `t()` is the dependency that makes a component unusable
73
+ * outside Nextcloud.
74
+ */
75
+ export default {
76
+ name: 'CnSiteEmptyState',
77
+
78
+ components: { CnSiteIcon },
79
+
80
+ props: {
81
+ /**
82
+ * Which state this is. Decides the ARIA posture, not the styling.
83
+ */
84
+ variant: {
85
+ type: String,
86
+ default: 'empty',
87
+ validator: (v) => ['loading', 'empty', 'error'].includes(v),
88
+ },
89
+
90
+ /** Headline, e.g. "Bezig met laden…" or "Pagina niet gevonden". */
91
+ title: {
92
+ type: String,
93
+ default: '',
94
+ },
95
+
96
+ /** Supporting line under the headline. */
97
+ description: {
98
+ type: String,
99
+ default: '',
100
+ },
101
+
102
+ /** Icon name from the closed vocabulary; '' renders none. */
103
+ icon: {
104
+ type: String,
105
+ default: '',
106
+ },
107
+
108
+ /** Optional way out — a link back to somewhere that works. */
109
+ link: {
110
+ type: String,
111
+ default: '',
112
+ },
113
+
114
+ /** Visible text for that link. */
115
+ linkLabel: {
116
+ type: String,
117
+ default: '',
118
+ },
119
+
120
+ /**
121
+ * Heading level, so the page outline stays intact.
122
+ *
123
+ * An empty state usually replaces a page's main content, so it usually
124
+ * wants the level that content would have had — not a fixed one.
125
+ */
126
+ headingLevel: {
127
+ type: Number,
128
+ default: 2,
129
+ validator: (v) => v >= 1 && v <= 6,
130
+ },
131
+ },
132
+
133
+ computed: {
134
+ /**
135
+ * @return {string} The heading element to render.
136
+ */
137
+ headingTag() {
138
+ return `h${this.headingLevel}`
139
+ },
140
+
141
+ /**
142
+ * The heading's class, tracking its level — the design system styles
143
+ * the class, not the tag.
144
+ *
145
+ * @return {string} e.g. `utrecht-heading-2`.
146
+ */
147
+ headingClass() {
148
+ return `utrecht-heading-${this.headingLevel}`
149
+ },
150
+ },
151
+ }
152
+ </script>
153
+
154
+ <style scoped>
155
+ /*
156
+ * Layout only, and deliberately token-driven.
157
+ *
158
+ * No colours are set here: an empty state can land on any surface a host puts
159
+ * it on, and this codebase has three separate bugs on record from rules that
160
+ * coloured text without knowing its background. The design system colours the
161
+ * heading, paragraph and link classes already.
162
+ */
163
+ .ac-empty-state {
164
+ display: flex;
165
+ flex-direction: column;
166
+ align-items: center;
167
+ justify-content: center;
168
+ text-align: center;
169
+ gap: var(--utrecht-space-block-md, 0.75rem);
170
+ padding-block: var(--utrecht-space-block-2xl, 3rem);
171
+ }
172
+ </style>
@@ -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>