@nexxtmove/ui 2.1.0 → 2.2.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/dist/nuxt.js CHANGED
@@ -84,6 +84,7 @@ var s = {
84
84
  NexxtEditorAlignmentGrid: "components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue",
85
85
  NexxtEditorAnchor: "components/editor/molecules/EditorAnchor/EditorAnchor.vue",
86
86
  NexxtEditorBackgroundSize: "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
87
+ NexxtEditorBlur: "components/editor/molecules/EditorBlur/EditorBlur.vue",
87
88
  NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
88
89
  NexxtEditorColor: "components/editor/molecules/EditorColor/EditorColor.vue",
89
90
  NexxtEditorDashPattern: "components/editor/molecules/EditorDashPattern/EditorDashPattern.vue",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -11,6 +11,12 @@ interface NexxtEditorSettingsBlockProps {
11
11
  * When `false` the content is always visible and the model is ignored.
12
12
  */
13
13
  switchable?: boolean
14
+ /**
15
+ * Id(s) of an element that describes the setting, set as `aria-describedby`
16
+ * on the switch. Only has an effect when `switchable` is `true`; without it
17
+ * no attribute is rendered.
18
+ */
19
+ describedby?: string
14
20
  }
15
21
 
16
22
  /**
@@ -21,7 +27,7 @@ interface NexxtEditorSettingsBlockProps {
21
27
  */
22
28
  defineOptions({ name: 'NexxtEditorSettingsBlock' })
23
29
 
24
- const { label, switchable = false } = defineProps<NexxtEditorSettingsBlockProps>()
30
+ const { label, switchable = false, describedby } = defineProps<NexxtEditorSettingsBlockProps>()
25
31
 
26
32
  /**
27
33
  * Open state of the block, driven by the switch. Only has an effect when
@@ -80,7 +86,14 @@ const labelTag = computed(() => (promoted.value && group ? `h${group.value.headi
80
86
  back to the `gap-2` that Switch.vue sets for itself.
81
87
  -->
82
88
  <div class="flex items-center [&>label]:flex [&>label]:w-full [&>label]:gap-3">
83
- <Switch v-if="switchable" v-model="model" size="sm" color="blue" label-position="before">
89
+ <Switch
90
+ v-if="switchable"
91
+ v-model="model"
92
+ size="sm"
93
+ color="blue"
94
+ label-position="before"
95
+ :aria-describedby="describedby"
96
+ >
84
97
  <span :class="['grow text-left', labelTypography, labelColor]">{{ label }}</span>
85
98
  </Switch>
86
99
  <component :is="labelTag" v-else :class="[labelTypography, labelColor]">
@@ -0,0 +1,160 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The blur steps the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `backdrop-blur-` prefix, so the class is
5
+ * `backdrop-blur-` plus the token -- the same shape as `EDITOR_SHADOW_OPTIONS`.
6
+ * "No blur" is not a step: it is the switch being off. The labels are not in
7
+ * here; they arrive through `messages`, keyed by token.
8
+ */
9
+ export const EDITOR_BLUR_OPTIONS = [
10
+ { token: 'xs' },
11
+ { token: 'sm' },
12
+ { token: 'md' },
13
+ { token: 'lg' },
14
+ { token: 'xl' },
15
+ ] as const
16
+
17
+ /**
18
+ * The blur as the editor needs it: the Tailwind class itself, so the consumer
19
+ * can put the model value straight on the group it styles.
20
+ */
21
+ export type EditorBlurValue = `backdrop-blur-${(typeof EDITOR_BLUR_OPTIONS)[number]['token']}`
22
+
23
+ /** Overridable copy of the step control. Every key is optional. */
24
+ export interface EditorBlurMessages {
25
+ /**
26
+ * Accessible name of the group of steps. Visually hidden: the block label
27
+ * already says what the steps are. Defaults to the Dutch `'Sterkte'`.
28
+ */
29
+ strength?: string
30
+ /** Name of the `backdrop-blur-xs` option. Defaults to `'XS'`. */
31
+ xs?: string
32
+ /** Name of the `backdrop-blur-sm` option. Defaults to `'S'`. */
33
+ sm?: string
34
+ /** Name of the `backdrop-blur-md` option. Defaults to `'M'`. */
35
+ md?: string
36
+ /** Name of the `backdrop-blur-lg` option. Defaults to `'L'`. */
37
+ lg?: string
38
+ /** Name of the `backdrop-blur-xl` option. Defaults to `'XL'`. */
39
+ xl?: string
40
+ }
41
+
42
+ export interface NexxtEditorBlurProps {
43
+ /**
44
+ * Header text of the settings block. Defaults to the Dutch `'Vervagen'`; the
45
+ * library has no i18n layer, so a consumer in another language passes its own
46
+ * copy in.
47
+ */
48
+ label?: string
49
+ /**
50
+ * Render a switch next to the label that collapses and expands the steps.
51
+ * Defaults to `true`: there is no "none" step, so the switch is the only way
52
+ * to turn the blur off. The switch state is the `enabled` model; switching
53
+ * off leaves the step alone.
54
+ */
55
+ switchable?: boolean
56
+ /**
57
+ * Copy of the step control. Merged per key, so overriding one key keeps the
58
+ * defaults of the others. Copy is never translated inside the component.
59
+ */
60
+ messages?: EditorBlurMessages
61
+ /**
62
+ * Id(s) of an element that describes the setting, such as a hint the
63
+ * consumer renders itself. Set as `aria-describedby` on the switch (when
64
+ * `switchable`) and on the group of steps. Without it no attribute is set.
65
+ */
66
+ describedby?: string
67
+ }
68
+
69
+ const DEFAULT_MESSAGES = {
70
+ strength: 'Sterkte',
71
+ xs: 'XS',
72
+ sm: 'S',
73
+ md: 'M',
74
+ lg: 'L',
75
+ xl: 'XL',
76
+ } satisfies Required<EditorBlurMessages>
77
+
78
+ /** The class a control starts on when it is switched on without a valid one. */
79
+ const DEFAULT_VALUE: EditorBlurValue = 'backdrop-blur-md'
80
+
81
+ const toValue = (token: (typeof EDITOR_BLUR_OPTIONS)[number]['token']): EditorBlurValue =>
82
+ `backdrop-blur-${token}`
83
+
84
+ const isValue = (value: unknown): value is EditorBlurValue =>
85
+ EDITOR_BLUR_OPTIONS.some(({ token }) => toValue(token) === value)
86
+ </script>
87
+
88
+ <script lang="ts" setup>
89
+ import { computed, watch } from 'vue'
90
+ import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
91
+ import NexxtToggleGroup from '../../../molecules/ToggleGroup/ToggleGroup.vue'
92
+ import type { NexxtToggleGroupOption } from '../../../molecules/ToggleGroup/ToggleGroup.vue'
93
+
94
+ defineOptions({ name: 'NexxtEditorBlur' })
95
+
96
+ const {
97
+ label = 'Vervagen',
98
+ switchable = true,
99
+ messages,
100
+ describedby,
101
+ } = defineProps<NexxtEditorBlurProps>()
102
+
103
+ /**
104
+ * The selected blur, as the Tailwind class to apply (`backdrop-blur-md`).
105
+ * Optional: a consumer may have no class stored yet. Without a valid class the
106
+ * control shows `md` but writes nothing, so the consumer has to assume
107
+ * `backdrop-blur-md` itself. Only a switch from off to on (when `switchable`)
108
+ * writes `backdrop-blur-md` back; mounting never emits.
109
+ */
110
+ const model = defineModel<EditorBlurValue>()
111
+
112
+ /**
113
+ * Open state of the block. Only has an effect when `switchable` is `true`; then
114
+ * off is the way to express "no blur". With `switchable` set to `false` the
115
+ * steps are always active, whatever this model says.
116
+ */
117
+ const enabled = defineModel<boolean>('enabled', { default: true })
118
+
119
+ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
120
+
121
+ const options = computed<NexxtToggleGroupOption[]>(() =>
122
+ EDITOR_BLUR_OPTIONS.map(({ token }) => ({ value: toValue(token), label: copy.value[token] })),
123
+ )
124
+
125
+ // ToggleGroup types its model as an open `string`; the setter narrows it, which
126
+ // is safe because it can only emit the option values built above. A missing or
127
+ // unknown class shows `md` rather than leaving every option unselected.
128
+ const selected = computed({
129
+ get: () => (isValue(model.value) ? model.value : DEFAULT_VALUE),
130
+ set: (value: string) => {
131
+ model.value = value as EditorBlurValue
132
+ },
133
+ })
134
+
135
+ // Switching on without a valid class writes `backdrop-blur-md` back, so the
136
+ // consumer never ends up with an enabled blur and no class to apply. Without a
137
+ // switch there is no switching on, so `enabled` is ignored there like everywhere
138
+ // else. The current model is read here rather than captured, so a parent that
139
+ // sets both in one tick wins.
140
+ watch(enabled, (on, wasOn) => {
141
+ if (switchable && on && !wasOn && !isValue(model.value)) model.value = DEFAULT_VALUE
142
+ })
143
+ </script>
144
+
145
+ <template>
146
+ <NexxtEditorSettingsBlock
147
+ v-model="enabled"
148
+ :label="label"
149
+ :switchable="switchable"
150
+ :describedby="describedby"
151
+ >
152
+ <NexxtToggleGroup
153
+ v-model="selected"
154
+ :label="copy.strength"
155
+ label-hidden
156
+ :options="options"
157
+ :aria-describedby="describedby"
158
+ />
159
+ </NexxtEditorSettingsBlock>
160
+ </template>
@@ -48,6 +48,11 @@ export interface EditorColorMessages {
48
48
  suggestionsLabel?: string
49
49
  /** Label shown above the theme roles. Defaults to `'Huisstijl'`. */
50
50
  themeLabel?: string
51
+ /**
52
+ * Label above the opacity slider, and its accessible name. Defaults to
53
+ * `'Dekking'`. Only shown with `showAlpha`.
54
+ */
55
+ alphaLabel?: string
51
56
  }
52
57
 
53
58
  export interface NexxtEditorColorProps {
@@ -99,6 +104,7 @@ const DEFAULT_MESSAGES = {
99
104
  placeholder: 'Kies kleur...',
100
105
  suggestionsLabel: 'Gebruikte kleuren',
101
106
  themeLabel: 'Huisstijl',
107
+ alphaLabel: 'Dekking',
102
108
  } satisfies Required<EditorColorMessages>
103
109
 
104
110
  /**
@@ -267,6 +273,7 @@ watch(
267
273
  :placeholder="copy.placeholder"
268
274
  :suggestions="{ hex: suggestions }"
269
275
  :suggestions-label="copy.suggestionsLabel"
276
+ :alpha-label="copy.alphaLabel"
270
277
  :value-label="activeRole?.label"
271
278
  >
272
279
  <template v-if="offeredRoles.length" #presets>
@@ -24,6 +24,12 @@ interface NexxtToggleGroupProps {
24
24
  options: NexxtToggleGroupOption[]
25
25
  label?: string
26
26
  equalWidth?: boolean
27
+ /**
28
+ * Hide `label` visually while it stays the group's accessible name, for a
29
+ * group whose purpose is already clear from a heading around it. Defaults to
30
+ * `false`. Has no effect without a `label`.
31
+ */
32
+ labelHidden?: boolean
27
33
  }
28
34
 
29
35
  defineOptions({
@@ -32,6 +38,7 @@ defineOptions({
32
38
 
33
39
  const props = withDefaults(defineProps<NexxtToggleGroupProps>(), {
34
40
  equalWidth: true,
41
+ labelHidden: false,
35
42
  })
36
43
 
37
44
  const model = defineModel<string>({ required: true })
@@ -158,7 +165,11 @@ onMounted(async () => {
158
165
 
159
166
  <template>
160
167
  <div class="flex flex-col gap-1" role="toolbar" :aria-labelledby="label ? labelId : undefined">
161
- <label v-if="label" :id="labelId" class="justify-start small-normal text-gray-700">
168
+ <label
169
+ v-if="label"
170
+ :id="labelId"
171
+ :class="['justify-start small-normal text-gray-700', labelHidden && 'sr-only']"
172
+ >
162
173
  {{ label }}
163
174
  </label>
164
175
  <div
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, watch, computed } from 'vue'
2
+ import { ref, watch, computed, useId } from 'vue'
3
3
  import type { Color, ColorChannel } from 'reka-ui'
4
4
  import {
5
5
  ColorAreaArea,
@@ -40,6 +40,8 @@ export interface NexxtColorPickerProps {
40
40
  suggestions?: ColorSuggestions
41
41
  /** Accessible and visible label for the suggestions. */
42
42
  suggestionsLabel?: string
43
+ /** Visible label of the opacity slider, and the slider's accessible name. */
44
+ alphaLabel?: string
43
45
  /**
44
46
  * Text shown in the trigger in place of the formatted colour, for a value that has a name of
45
47
  * its own, such as a theme role. Only shown while a colour is set.
@@ -54,7 +56,16 @@ defineSlots<{
54
56
  presets?(): unknown
55
57
  }>()
56
58
 
57
- const PICKER_ID = 'color-picker-' + Math.random().toString(36).slice(2, 10)
59
+ // `useId` is the same on the server and in the browser; a random id would not hydrate.
60
+ const PICKER_ID = useId()
61
+ const ALPHA_LABEL_ID = useId()
62
+
63
+ /**
64
+ * reka reports the alpha channel as `alpha * 100`, which is a fraction for most
65
+ * stored colours (`#…80` is 50.196) and float noise for some steps (7.000…01).
66
+ * Only what the slider announces is rounded; the colour itself is untouched.
67
+ */
68
+ const roundedChannel = (value: number) => Math.round(value)
58
69
 
59
70
  const props = withDefaults(defineProps<NexxtColorPickerProps>(), {
60
71
  placeholder: 'Kies kleur...',
@@ -64,6 +75,7 @@ const props = withDefaults(defineProps<NexxtColorPickerProps>(), {
64
75
  matchWidth: false,
65
76
  suggestions: () => ({}),
66
77
  suggestionsLabel: 'Gebruikte kleuren',
78
+ alphaLabel: 'Dekking',
67
79
  })
68
80
 
69
81
  const model = defineModel<string>({ default: '' })
@@ -288,28 +300,43 @@ watch(
288
300
  </ColorSliderRoot>
289
301
 
290
302
  <!-- Alpha slider (opt-in via showAlpha prop) -->
291
- <!-- The checkerboard wrapper is behind the track; the gradient goes from
292
- rgba(…,0) → rgba(…,1) so its transparent left end reveals it. -->
293
- <div v-if="showAlpha" class="relative flex h-5 items-center">
294
- <div
295
- class="absolute inset-x-0 inset-y-1 rounded-full"
296
- style="
297
- background-image: repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%);
298
- background-size: 10px 10px;
299
- "
300
- />
301
- <ColorSliderRoot
302
- :model-value="colorObj"
303
- channel="alpha"
304
- color-space="hsl"
305
- class="relative z-10 flex size-full touch-none items-center select-none"
306
- @update:color="handleColorUpdate"
307
- >
308
- <ColorSliderTrack class="relative h-3 flex-1 overflow-hidden rounded-full" />
309
- <ColorSliderThumb
310
- class="block size-5 cursor-grab rounded-full border-2 border-white bg-white shadow-sm focus:ring-2 focus:ring-cornflower-blue-500 focus:outline-none active:cursor-grabbing"
303
+ <div v-if="showAlpha" class="flex flex-col gap-1">
304
+ <span :id="ALPHA_LABEL_ID" class="extra-small-normal text-gray-500">{{
305
+ alphaLabel
306
+ }}</span>
307
+ <!-- The checkerboard wrapper is behind the track; the gradient goes from
308
+ rgba(…,0) → rgba(…,1) so its transparent left end reveals it. -->
309
+ <div class="relative flex h-5 items-center">
310
+ <div
311
+ class="absolute inset-x-0 inset-y-1 rounded-full"
312
+ style="
313
+ background-image: repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%);
314
+ background-size: 10px 10px;
315
+ "
311
316
  />
312
- </ColorSliderRoot>
317
+ <ColorSliderRoot
318
+ :model-value="colorObj"
319
+ channel="alpha"
320
+ color-space="hsl"
321
+ class="relative z-10 flex size-full touch-none items-center select-none"
322
+ @update:color="handleColorUpdate"
323
+ >
324
+ <ColorSliderTrack class="relative h-3 flex-1 overflow-hidden rounded-full" />
325
+ <!--
326
+ With `as-child` reka merges its own attributes under the child's,
327
+ so the child's win: the name comes from the visible label instead
328
+ of reka's English "Alpha", and the value is announced whole.
329
+ -->
330
+ <ColorSliderThumb v-slot="{ channelValue }" as-child>
331
+ <span
332
+ :aria-label="undefined"
333
+ :aria-labelledby="ALPHA_LABEL_ID"
334
+ :aria-valuenow="roundedChannel(channelValue)"
335
+ class="block size-5 cursor-grab rounded-full border-2 border-white bg-white shadow-sm focus:ring-2 focus:ring-cornflower-blue-500 focus:outline-none active:cursor-grabbing"
336
+ />
337
+ </ColorSliderThumb>
338
+ </ColorSliderRoot>
339
+ </div>
313
340
  </div>
314
341
 
315
342
  <!-- Format input fields — only active format shown, cycle with chevrons -->
@@ -81,6 +81,7 @@
81
81
  "NexxtEditorAlignmentGrid": "components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue",
82
82
  "NexxtEditorAnchor": "components/editor/molecules/EditorAnchor/EditorAnchor.vue",
83
83
  "NexxtEditorBackgroundSize": "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
84
+ "NexxtEditorBlur": "components/editor/molecules/EditorBlur/EditorBlur.vue",
84
85
  "NexxtEditorBorder": "components/editor/molecules/EditorBorder/EditorBorder.vue",
85
86
  "NexxtEditorColor": "components/editor/molecules/EditorColor/EditorColor.vue",
86
87
  "NexxtEditorDashPattern": "components/editor/molecules/EditorDashPattern/EditorDashPattern.vue",
package/src/index.ts CHANGED
@@ -70,6 +70,12 @@ export type {
70
70
  EditorBackgroundSizeMessages,
71
71
  EditorBackgroundSizeValue,
72
72
  } from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
73
+ export { EDITOR_BLUR_OPTIONS } from './components/editor/molecules/EditorBlur/EditorBlur.vue'
74
+ export type {
75
+ EditorBlurMessages,
76
+ EditorBlurValue,
77
+ NexxtEditorBlurProps,
78
+ } from './components/editor/molecules/EditorBlur/EditorBlur.vue'
73
79
  export { EDITOR_FLEX_DIRECTION_OPTIONS } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
74
80
  export type {
75
81
  EditorFlexDirectionMessages,
@@ -233,6 +239,7 @@ export { default as NexxtEditorAlignItems } from './components/editor/molecules/
233
239
  export { default as NexxtEditorAlignmentGrid } from './components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue'
234
240
  export { default as NexxtEditorAnchor } from './components/editor/molecules/EditorAnchor/EditorAnchor.vue'
235
241
  export { default as NexxtEditorBackgroundSize } from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
242
+ export { default as NexxtEditorBlur } from './components/editor/molecules/EditorBlur/EditorBlur.vue'
236
243
  export { default as NexxtEditorBorder } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
237
244
  export { default as NexxtEditorColor } from './components/editor/molecules/EditorColor/EditorColor.vue'
238
245
  export { default as NexxtEditorDashPattern } from './components/editor/molecules/EditorDashPattern/EditorDashPattern.vue'