@nexxtmove/ui 1.46.0 → 1.48.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.
Files changed (29) hide show
  1. package/dist/components/editor/molecules/EditorShadow/EditorShadow.vue.d.ts +72 -0
  2. package/dist/components/organisms/IconPicker/IconPicker.vue.d.ts +142 -0
  3. package/dist/components/organisms/IconPicker/_IconPickerGrid.vue.d.ts +40 -0
  4. package/dist/components/organisms/IconPicker/_IconPickerRail.vue.d.ts +22 -0
  5. package/dist/components/organisms/IconPicker/iconPickerHelpers.d.ts +39 -0
  6. package/dist/components/organisms/IconPicker/useIconPickerCatalogue.d.ts +72 -0
  7. package/dist/components/organisms/IconPicker/useIconPickerNavigation.d.ts +60 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +2254 -1386
  10. package/dist/nuxt.js +2 -0
  11. package/package.json +3 -3
  12. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue +1 -1
  13. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue +1 -1
  14. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue +1 -1
  15. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue +1 -1
  16. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.vue +3 -3
  17. package/src/components/editor/molecules/EditorShadow/EditorShadow.vue +106 -0
  18. package/src/components/editor/molecules/EditorSize/EditorSize.vue +1 -1
  19. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue +1 -1
  20. package/src/components/molecules/ToggleGroup/ToggleGroup.vue +75 -3
  21. package/src/components/organisms/IconPicker/IconPicker.vue +621 -0
  22. package/src/components/organisms/IconPicker/_IconPickerGrid.vue +282 -0
  23. package/src/components/organisms/IconPicker/_IconPickerRail.vue +112 -0
  24. package/src/components/organisms/IconPicker/iconPickerHelpers.ts +111 -0
  25. package/src/components/organisms/IconPicker/useIconPickerCatalogue.ts +238 -0
  26. package/src/components/organisms/IconPicker/useIconPickerNavigation.ts +206 -0
  27. package/src/components.json +2 -0
  28. package/src/index.ts +25 -0
  29. package/tailwind/nexxtmove.tailwind.custom.css +3 -4
package/dist/nuxt.js CHANGED
@@ -64,6 +64,7 @@ var s = {
64
64
  NexxtDropdownButton: "components/organisms/DropdownButton/DropdownButton.vue",
65
65
  NexxtFontPicker: "components/organisms/FontPicker/FontPicker.vue",
66
66
  NexxtHeader: "components/organisms/Header/Header.vue",
67
+ NexxtIconPicker: "components/organisms/IconPicker/IconPicker.vue",
67
68
  NexxtModal: "components/organisms/Modal/Modal.vue",
68
69
  NexxtPickerPanel: "components/organisms/PickerPanel/PickerPanel.vue",
69
70
  NexxtProgressBar: "components/organisms/ProgressBar/ProgressBar.vue",
@@ -94,6 +95,7 @@ var s = {
94
95
  NexxtEditorMaxWidth: "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
95
96
  NexxtEditorPadding: "components/editor/molecules/EditorPadding/EditorPadding.vue",
96
97
  NexxtEditorRadius: "components/editor/molecules/EditorRadius/EditorRadius.vue",
98
+ NexxtEditorShadow: "components/editor/molecules/EditorShadow/EditorShadow.vue",
97
99
  NexxtEditorSize: "components/editor/molecules/EditorSize/EditorSize.vue",
98
100
  NexxtEditorSizeLimit: "components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue",
99
101
  NexxtEditorTextAlign: "components/editor/molecules/EditorTextAlign/EditorTextAlign.vue"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.46.0",
4
+ "version": "1.48.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -50,7 +50,7 @@
50
50
  "@vitejs/plugin-vue": "^6.0.8",
51
51
  "@vitest/browser": "^4.1.11",
52
52
  "@vitest/browser-playwright": "^4.1.11",
53
- "@vitest/coverage-v8": "^4.1.10",
53
+ "@vitest/coverage-v8": "^4.1.11",
54
54
  "@vue/eslint-config-typescript": "^14.9.0",
55
55
  "@vue/test-utils": "^2.4.11",
56
56
  "@vue/tsconfig": "^0.9.1",
@@ -74,7 +74,7 @@
74
74
  "typescript-eslint": "^8.67.0",
75
75
  "vite": "^8.2.1",
76
76
  "vite-plugin-dts": "^5.0.3",
77
- "vitest": "^4.1.10"
77
+ "vitest": "^4.1.11"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=22.18.0"
@@ -127,6 +127,6 @@ const selected = computed({
127
127
 
128
128
  <template>
129
129
  <NexxtEditorSettingsBlock :label="label">
130
- <NexxtToggleGroup v-model="selected" :options="options" />
130
+ <NexxtToggleGroup v-model="selected" :options="options" :aria-label="label" />
131
131
  </NexxtEditorSettingsBlock>
132
132
  </template>
@@ -105,6 +105,6 @@ const selected = computed({
105
105
 
106
106
  <template>
107
107
  <NexxtEditorSettingsBlock :label="label">
108
- <NexxtToggleGroup v-model="selected" :options="options" />
108
+ <NexxtToggleGroup v-model="selected" :options="options" :aria-label="label" />
109
109
  </NexxtEditorSettingsBlock>
110
110
  </template>
@@ -103,6 +103,6 @@ const selected = computed({
103
103
 
104
104
  <template>
105
105
  <NexxtEditorSettingsBlock :label="label">
106
- <NexxtToggleGroup v-model="selected" :options="options" />
106
+ <NexxtToggleGroup v-model="selected" :options="options" :aria-label="label" />
107
107
  </NexxtEditorSettingsBlock>
108
108
  </template>
@@ -124,6 +124,6 @@ const selected = computed({
124
124
 
125
125
  <template>
126
126
  <NexxtEditorSettingsBlock :label="label">
127
- <NexxtToggleGroup v-model="selected" :options="options" />
127
+ <NexxtToggleGroup v-model="selected" :options="options" :aria-label="label" />
128
128
  </NexxtEditorSettingsBlock>
129
129
  </template>
@@ -156,10 +156,10 @@ const inert = computed(() => (open.value ? undefined : true))
156
156
  <NexxtEditorSettingsBlock :label="label">
157
157
  <!--
158
158
  The block label is not a <label> (a <p>, or a heading when promoted), so
159
- the group is named explicitly. Both attributes fall through to
160
- ToggleGroup's root.
159
+ the toolbar is named explicitly; `aria-label` falls through to
160
+ ToggleGroup's root, which is the toolbar.
161
161
  -->
162
- <NexxtToggleGroup v-model="selected" :options="options" role="group" :aria-label="label">
162
+ <NexxtToggleGroup v-model="selected" :options="options" :aria-label="label">
163
163
  <!--
164
164
  Rendered on the button and again in the overlay that shows the active
165
165
  colour, so it draws in currentColor and takes ToggleGroup's colours:
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The strengths the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `shadow-` prefix, so the class is `shadow-` plus the
5
+ * token. They are three steps of the theme's `--shadow-*` scale -- the ones the
6
+ * sidebar has room for as labelled options. "No shadow" is not a strength: it is
7
+ * the switch being off. The labels are not in here; they arrive through
8
+ * `messages`, keyed by token.
9
+ */
10
+ export const EDITOR_SHADOW_OPTIONS = [{ token: 'sm' }, { token: 'md' }, { token: 'lg' }] as const
11
+
12
+ /**
13
+ * The shadow strength as the editor needs it: the Tailwind class itself, so the
14
+ * consumer can put the model value straight on the element it styles.
15
+ */
16
+ export type EditorShadowValue = `shadow-${(typeof EDITOR_SHADOW_OPTIONS)[number]['token']}`
17
+
18
+ /** Overridable copy of the strength control. Every key is optional. */
19
+ export interface EditorShadowMessages {
20
+ /** Label above the three strengths. Defaults to the Dutch `'Sterkte'`. */
21
+ strength?: string
22
+ /** Name of the `shadow-sm` option. Defaults to the Dutch `'Licht'`. */
23
+ sm?: string
24
+ /** Name of the `shadow-md` option. Defaults to the Dutch `'Gemiddeld'`. */
25
+ md?: string
26
+ /** Name of the `shadow-lg` option. Defaults to the Dutch `'Sterk'`. */
27
+ lg?: string
28
+ }
29
+
30
+ export interface NexxtEditorShadowProps {
31
+ /**
32
+ * Header text of the settings block. Defaults to the Dutch `'Schaduw'`; the
33
+ * library has no i18n layer, so a consumer in another language passes its own
34
+ * copy in.
35
+ */
36
+ label?: string
37
+ /**
38
+ * Render a switch next to the label that collapses and expands the strengths.
39
+ * Defaults to `true`: a shadow is the kind of setting that is off as often as
40
+ * it is on, so turning it off is part of the control rather than an extra.
41
+ * The switch state is the `enabled` model; the component only reports it and
42
+ * leaves the strength alone, so how a consumer stores an element without a
43
+ * shadow is up to the consumer.
44
+ */
45
+ switchable?: boolean
46
+ /**
47
+ * Copy of the strength control. Merged per key, so overriding one key keeps
48
+ * the defaults of the others. Copy is never translated inside the component.
49
+ */
50
+ messages?: EditorShadowMessages
51
+ }
52
+
53
+ const DEFAULT_MESSAGES = {
54
+ strength: 'Sterkte',
55
+ sm: 'Licht',
56
+ md: 'Gemiddeld',
57
+ lg: 'Sterk',
58
+ } satisfies Required<EditorShadowMessages>
59
+ </script>
60
+
61
+ <script lang="ts" setup>
62
+ import { computed } from 'vue'
63
+ import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
64
+ import NexxtToggleGroup from '../../../molecules/ToggleGroup/ToggleGroup.vue'
65
+ import type { NexxtToggleGroupOption } from '../../../molecules/ToggleGroup/ToggleGroup.vue'
66
+
67
+ defineOptions({ name: 'NexxtEditorShadow' })
68
+
69
+ const { label = 'Schaduw', switchable = true, messages } = defineProps<NexxtEditorShadowProps>()
70
+
71
+ /** The selected strength, as the Tailwind class to apply. */
72
+ const model = defineModel<EditorShadowValue>({ required: true })
73
+
74
+ /** Open state of the block. Only has an effect when `switchable` is `true`. */
75
+ const enabled = defineModel<boolean>('enabled', { default: true })
76
+
77
+ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
78
+
79
+ const options = computed<NexxtToggleGroupOption[]>(() =>
80
+ EDITOR_SHADOW_OPTIONS.map(({ token }) => ({
81
+ value: `shadow-${token}`,
82
+ label: copy.value[token],
83
+ })),
84
+ )
85
+
86
+ // ToggleGroup types its model as the open `string` of its options, so writing
87
+ // straight back into the narrower model is a type error. The setter is the one
88
+ // place that narrowing happens, and it is safe: the only values ToggleGroup can
89
+ // emit are the option values built above. A value that is not on the list -- a
90
+ // stored `shadow-xl` from before this component, say -- shows the first option
91
+ // instead of leaving every one of them unselected; the first change writes a
92
+ // valid class back.
93
+ const selected = computed({
94
+ get: () =>
95
+ options.value.some(({ value }) => value === model.value) ? model.value : options.value[0].value,
96
+ set: (value: string) => {
97
+ model.value = value as EditorShadowValue
98
+ },
99
+ })
100
+ </script>
101
+
102
+ <template>
103
+ <NexxtEditorSettingsBlock v-model="enabled" :label="label" :switchable="switchable">
104
+ <NexxtToggleGroup v-model="selected" :label="copy.strength" :options="options" />
105
+ </NexxtEditorSettingsBlock>
106
+ </template>
@@ -218,7 +218,7 @@ watch(
218
218
 
219
219
  <template>
220
220
  <NexxtEditorSettingsBlock :label="label">
221
- <NexxtToggleGroup v-model="mode" :options="options" />
221
+ <NexxtToggleGroup v-model="mode" :options="options" :aria-label="label" />
222
222
 
223
223
  <!--
224
224
  The field slides open under the choice it belongs to, the same 0fr/1fr
@@ -125,6 +125,6 @@ const selected = computed({
125
125
 
126
126
  <template>
127
127
  <NexxtEditorSettingsBlock :label="label">
128
- <NexxtToggleGroup v-model="selected" :options="options" />
128
+ <NexxtToggleGroup v-model="selected" :options="options" :aria-label="label" />
129
129
  </NexxtEditorSettingsBlock>
130
130
  </template>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, useTemplateRef, watch, onMounted, nextTick } from 'vue'
2
+ import { computed, ref, useId, useTemplateRef, watch, onMounted, nextTick } from 'vue'
3
3
  import { useResizeObserver } from '@vueuse/core'
4
4
  import type { IconName } from '@awesome.me/kit-37b149f3ef/icons'
5
5
  import NexxtIcon from '../../atoms/Icon/Icon.vue'
@@ -81,6 +81,71 @@ const updateSlider = async () => {
81
81
 
82
82
  watch(model, async () => await updateSlider())
83
83
 
84
+ /**
85
+ * Keyboard: the toolbar pattern (WAI-ARIA APG) with a roving tabindex. The
86
+ * group is one tab stop -- the selected option, or the first when none is --
87
+ * and the arrow keys move focus between the options, wrapping at the ends;
88
+ * Home and End jump to the first and last. Moving focus selects nothing:
89
+ * Enter and Space do, through the button's own click. There are no disabled
90
+ * options to skip, since an option cannot be disabled.
91
+ *
92
+ * While focus moves inside the group the tab stop moves with it, so Shift+Tab
93
+ * from a later option leaves the group instead of landing on the selected one.
94
+ * Once focus leaves, the tab stop goes back to the selected option.
95
+ */
96
+ /**
97
+ * The root is a `toolbar` (WAI-ARIA APG): a set of buttons with one tab stop
98
+ * and arrow-key navigation. Not a `radiogroup`, because there the arrows select
99
+ * and here they only move focus. Each option reports whether it is the chosen
100
+ * one with `aria-pressed`, so a screen reader hears "Week, toggle button,
101
+ * pressed". The toolbar is named by the visible `label` when there is one;
102
+ * otherwise a consumer passes `aria-label`, which falls through to the root.
103
+ */
104
+ const labelId = useId()
105
+
106
+ const selectedIndex = computed(() =>
107
+ Math.max(
108
+ 0,
109
+ props.options.findIndex((o) => o.value === model.value),
110
+ ),
111
+ )
112
+
113
+ /** The option that is focused inside the group; `null` while focus is outside. */
114
+ const focusedIndex = ref<number | null>(null)
115
+
116
+ const tabStop = computed(() => focusedIndex.value ?? selectedIndex.value)
117
+
118
+ const NEXT_KEYS: Record<string, (index: number, count: number) => number> = {
119
+ ArrowRight: (index, count) => (index + 1) % count,
120
+ ArrowDown: (index, count) => (index + 1) % count,
121
+ ArrowLeft: (index, count) => (index - 1 + count) % count,
122
+ ArrowUp: (index, count) => (index - 1 + count) % count,
123
+ Home: () => 0,
124
+ End: (_, count) => count - 1,
125
+ }
126
+
127
+ const onKeydown = (event: KeyboardEvent) => {
128
+ const move = NEXT_KEYS[event.key]
129
+ if (!move) return
130
+
131
+ const index = buttonRefs.value.indexOf(event.target as HTMLButtonElement)
132
+ if (index === -1) return
133
+
134
+ // Or the arrows would scroll the panel the group sits in.
135
+ event.preventDefault()
136
+ buttonRefs.value[move(index, props.options.length)]?.focus()
137
+ }
138
+
139
+ const onFocusin = (event: FocusEvent) => {
140
+ const index = buttonRefs.value.indexOf(event.target as HTMLButtonElement)
141
+ if (index !== -1) focusedIndex.value = index
142
+ }
143
+
144
+ const onFocusout = (event: FocusEvent) => {
145
+ const next = event.relatedTarget as Node | null
146
+ if (!next || !containerRef.value?.contains(next)) focusedIndex.value = null
147
+ }
148
+
84
149
  useResizeObserver(containerRef, () => updateSlider())
85
150
 
86
151
  onMounted(async () => {
@@ -92,14 +157,17 @@ onMounted(async () => {
92
157
  </script>
93
158
 
94
159
  <template>
95
- <div class="flex flex-col gap-1">
96
- <label v-if="label" class="justify-start small-normal text-gray-700">
160
+ <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">
97
162
  {{ label }}
98
163
  </label>
99
164
  <div
100
165
  ref="containerRef"
101
166
  class="relative isolate flex h-10 gap-1 rounded-xl bg-gray-100 p-1"
102
167
  :class="{ 'flex-wrap': !equalWidth }"
168
+ @keydown="onKeydown"
169
+ @focusin="onFocusin"
170
+ @focusout="onFocusout"
103
171
  >
104
172
  <!-- slider pill -->
105
173
  <div
@@ -127,6 +195,8 @@ onMounted(async () => {
127
195
  <button
128
196
  :ref="(el) => (buttonRefs[index] = el as HTMLButtonElement)"
129
197
  type="button"
198
+ :tabindex="index === tabStop ? 0 : -1"
199
+ :aria-pressed="option.value === model"
130
200
  :aria-label="option.ariaLabel ?? (option.label ? undefined : option.tooltip)"
131
201
  class="relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap text-gray-700 hover:text-gray-900"
132
202
  :class="{ 'flex-1': equalWidth }"
@@ -143,6 +213,8 @@ onMounted(async () => {
143
213
  v-else
144
214
  :ref="(el) => (buttonRefs[index] = el as HTMLButtonElement)"
145
215
  type="button"
216
+ :tabindex="index === tabStop ? 0 : -1"
217
+ :aria-pressed="option.value === model"
146
218
  :aria-label="option.ariaLabel ?? (option.label ? undefined : option.tooltip)"
147
219
  class="relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap text-gray-700 hover:text-gray-900"
148
220
  :class="{ 'flex-1': equalWidth }"