@nexxtmove/ui 1.33.0 → 1.35.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.
@@ -14,10 +14,11 @@ export type EditorColorProperty = 'bg' | 'text'
14
14
  */
15
15
  export type EditorColorValue = `${EditorColorProperty}-[#${string}]`
16
16
 
17
- /** Overridable copy of the picker. Every key is optional. */
18
17
  export interface EditorColorMessages {
19
18
  /** Text shown while no colour is set. Defaults to `'Kies kleur...'`. */
20
19
  placeholder?: string
20
+ /** Label shown above reusable colours. */
21
+ suggestionsLabel?: string
21
22
  }
22
23
 
23
24
  export interface NexxtEditorColorProps {
@@ -44,10 +45,13 @@ export interface NexxtEditorColorProps {
44
45
  * defaults of the others. Copy is never translated inside the component.
45
46
  */
46
47
  messages?: EditorColorMessages
48
+ /** Hex colours available inside the picker panel as one-click suggestions. */
49
+ suggestions?: string[]
47
50
  }
48
51
 
49
52
  const DEFAULT_MESSAGES = {
50
53
  placeholder: 'Kies kleur...',
54
+ suggestionsLabel: 'Gebruikte kleuren',
51
55
  } satisfies Required<EditorColorMessages>
52
56
 
53
57
  /**
@@ -72,6 +76,7 @@ const {
72
76
  switchable = false,
73
77
  property = 'bg',
74
78
  messages,
79
+ suggestions = [],
75
80
  } = defineProps<NexxtEditorColorProps>()
76
81
 
77
82
  /**
@@ -150,6 +155,8 @@ watch(
150
155
  :formats="['hex']"
151
156
  :show-alpha="false"
152
157
  :placeholder="copy.placeholder"
158
+ :suggestions="{ hex: suggestions }"
159
+ :suggestions-label="copy.suggestionsLabel"
153
160
  />
154
161
  </NexxtEditorSettingsBlock>
155
162
  </template>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import { computed, useId } from 'vue'
3
+ import { usePreferredReducedMotion } from '@vueuse/core'
4
+ import NexxtIcon from '../../atoms/Icon/Icon.vue'
5
+
6
+ interface NexxtPickerGroupProps {
7
+ /** Heading of the group, and the accessible name of its `role="group"`. */
8
+ label: string
9
+ /**
10
+ * Whether the heading folds the group. `false` turns the heading into a plain label: no
11
+ * chevron, no click handler, no `aria-expanded`.
12
+ */
13
+ collapsible?: boolean
14
+ }
15
+
16
+ defineOptions({ name: 'NexxtPickerGroup' })
17
+
18
+ const { label, collapsible = true } = defineProps<NexxtPickerGroupProps>()
19
+
20
+ /** Whether the options are shown. A closed group has its options out of the DOM. */
21
+ const open = defineModel<boolean>('open', { default: true })
22
+
23
+ defineSlots<{
24
+ /** The options: `NexxtPickerItem`s, or anything else with `role="option"`. */
25
+ default(): unknown
26
+ }>()
27
+
28
+ const headingId = useId()
29
+
30
+ const preferredMotion = usePreferredReducedMotion()
31
+ // `motion-reduce:transition-none` alone leaves Vue reading the 120ms off the leaving wrapper, so
32
+ // the options of a "closed" group would linger; without CSS mode they go in the same patch
33
+ const reduced = computed(() => preferredMotion.value === 'reduce')
34
+
35
+ const HEADING_CLASSES = {
36
+ base: 'flex h-7 items-center gap-1.5 rounded-lg px-2 extra-small-semibold tracking-wide text-gray-700 uppercase select-none',
37
+ collapsible: 'cursor-pointer hover:bg-gray-50',
38
+ static: 'cursor-default',
39
+ }
40
+
41
+ const CHEVRON_CLASSES = {
42
+ base: 'extra-small-normal transition-transform motion-reduce:transition-none',
43
+ open: 'duration-160 ease-out',
44
+ closed: '-rotate-90 duration-120 ease-in',
45
+ }
46
+
47
+ const headingClass = computed(() => [
48
+ HEADING_CLASSES.base,
49
+ collapsible ? HEADING_CLASSES.collapsible : HEADING_CLASSES.static,
50
+ ])
51
+
52
+ const chevronClass = computed(() => [
53
+ CHEVRON_CLASSES.base,
54
+ open.value ? CHEVRON_CLASSES.open : CHEVRON_CLASSES.closed,
55
+ ])
56
+
57
+ // Bound through `v-on`, so a static heading has no click handler at all
58
+ const headingListeners = computed(() =>
59
+ collapsible ? { click: () => (open.value = !open.value) } : {},
60
+ )
61
+
62
+ // Options on their way out can no longer be clicked, and assistive tech skips them
63
+ const onBeforeLeave = (el: Element) => el.setAttribute('inert', '')
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ role="group"
69
+ :aria-labelledby="headingId"
70
+ :aria-expanded="collapsible ? open : undefined"
71
+ class="flex flex-col not-first:mt-2"
72
+ >
73
+ <!-- Not a button and not a tab stop: a listbox may only hold options and groups. The keyboard
74
+ folds a group with ←/→ from the listbox. `mousedown.prevent` keeps focus where it was. -->
75
+ <div role="presentation" :class="headingClass" v-on="headingListeners" @mousedown.prevent>
76
+ <span
77
+ v-if="collapsible"
78
+ class="flex size-3 shrink-0 items-center justify-center text-gray-700"
79
+ >
80
+ <NexxtIcon name="chevron-down" aria-hidden="true" :class="chevronClass" />
81
+ </span>
82
+ <span :id="headingId">{{ label }}</span>
83
+ </div>
84
+
85
+ <!-- No row class on the wrapper itself: after entering it is plain `auto`, so a base class
86
+ never fights the from/to classes -->
87
+ <Transition
88
+ :css="!reduced"
89
+ enter-active-class="transition-[grid-template-rows,opacity] duration-160 ease-out motion-reduce:transition-none"
90
+ enter-from-class="grid-rows-[0fr] opacity-0"
91
+ enter-to-class="grid-rows-[1fr] opacity-100"
92
+ leave-active-class="transition-[grid-template-rows,opacity] duration-120 ease-in motion-reduce:transition-none"
93
+ leave-from-class="grid-rows-[1fr] opacity-100"
94
+ leave-to-class="grid-rows-[0fr] opacity-0"
95
+ @before-leave="onBeforeLeave"
96
+ >
97
+ <div v-if="open" class="grid">
98
+ <div class="min-h-0 overflow-hidden">
99
+ <!-- Padding here rather than on the clipping layer, so nothing is left at 0fr -->
100
+ <div class="flex flex-col gap-0.5 pt-0.5">
101
+ <slot />
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </Transition>
106
+ </div>
107
+ </template>