@nexxtmove/ui 0.1.53 → 0.1.54

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
@@ -1,41 +1,42 @@
1
1
  import { defineNuxtModule as c, addComponent as s, addTemplate as a } from "@nuxt/kit";
2
- const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar/Avatar.vue", u = "components/Button/Button.vue", m = "components/Calendar/Calendar.vue", r = "components/Card/Card.vue", p = "components/Checkbox/Checkbox.vue", d = "components/Chip/Chip.vue", l = "components/Combobox/Combobox.vue", v = "components/ContactInfoCard/ContactInfoCard.vue", C = "components/CustomerJourneyTile/CustomerJourneyTile.vue", N = "components/DatePicker/DatePicker.vue", T = "components/DoughnutChart/DoughnutChart.vue", b = "components/DropdownButton/DropdownButton.vue", P = "components/Header/Header.vue", S = "components/Icon/Icon.vue", h = "components/InfoBlock/InfoBlock.vue", I = "components/InputField/InputField.vue", f = "components/Modal/Modal.vue", g = "components/Pagination/Pagination.vue", y = "components/PresenceIndicator/PresenceIndicator.vue", M = "components/ProgressBar/ProgressBar.vue", k = "components/PropertyCard/PropertyCard.vue", B = "components/PropertyListing/PropertyListing.vue", D = "components/SocialIcons/SocialIcons.vue", A = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", H = "components/SocialMediaTemplate/SocialMediaTemplate.vue", w = "components/SocialMediaType/SocialMediaType.vue", E = "components/Source/Source.vue", F = "components/StepperHeader/StepperHeader.vue", J = "components/TabbedContent/TabbedContent.vue", L = "components/Table/Table.vue", j = "components/Tabs/Tabs.vue", $ = "components/TimelineEvent/TimelineEvent.vue", K = "components/TimelinePhaseblock/TimelinePhaseblock.vue", O = "components/Tooltip/Tooltip.vue", U = {
2
+ const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar/Avatar.vue", m = "components/Button/Button.vue", u = "components/Calendar/Calendar.vue", r = "components/Card/Card.vue", p = "components/Checkbox/Checkbox.vue", d = "components/Chip/Chip.vue", l = "components/Combobox/Combobox.vue", v = "components/ComboboxPanel/ComboboxPanel.vue", C = "components/ContactInfoCard/ContactInfoCard.vue", N = "components/CustomerJourneyTile/CustomerJourneyTile.vue", b = "components/DatePicker/DatePicker.vue", T = "components/DoughnutChart/DoughnutChart.vue", P = "components/DropdownButton/DropdownButton.vue", S = "components/Header/Header.vue", h = "components/Icon/Icon.vue", I = "components/InfoBlock/InfoBlock.vue", f = "components/InputField/InputField.vue", g = "components/Modal/Modal.vue", y = "components/Pagination/Pagination.vue", M = "components/PresenceIndicator/PresenceIndicator.vue", k = "components/ProgressBar/ProgressBar.vue", B = "components/PropertyCard/PropertyCard.vue", D = "components/PropertyListing/PropertyListing.vue", A = "components/SocialIcons/SocialIcons.vue", H = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", w = "components/SocialMediaTemplate/SocialMediaTemplate.vue", E = "components/SocialMediaType/SocialMediaType.vue", F = "components/Source/Source.vue", J = "components/StepperHeader/StepperHeader.vue", L = "components/TabbedContent/TabbedContent.vue", j = "components/Table/Table.vue", $ = "components/Tabs/Tabs.vue", K = "components/TimelineEvent/TimelineEvent.vue", O = "components/TimelinePhaseblock/TimelinePhaseblock.vue", U = "components/Tooltip/Tooltip.vue", _ = {
3
3
  NexxtAnimatedNumber: x,
4
4
  NexxtAvatar: i,
5
- NexxtButton: u,
6
- NexxtCalendar: m,
5
+ NexxtButton: m,
6
+ NexxtCalendar: u,
7
7
  NexxtCard: r,
8
8
  NexxtCheckbox: p,
9
9
  NexxtChip: d,
10
10
  NexxtCombobox: l,
11
- NexxtContactInfoCard: v,
12
- NexxtCustomerJourneyTile: C,
13
- NexxtDatePicker: N,
11
+ NexxtComboboxPanel: v,
12
+ NexxtContactInfoCard: C,
13
+ NexxtCustomerJourneyTile: N,
14
+ NexxtDatePicker: b,
14
15
  NexxtDoughnutChart: T,
15
- NexxtDropdownButton: b,
16
- NexxtHeader: P,
17
- NexxtIcon: S,
18
- NexxtInfoBlock: h,
19
- NexxtInputField: I,
20
- NexxtModal: f,
21
- NexxtPagination: g,
22
- NexxtPresenceIndicator: y,
23
- NexxtProgressBar: M,
24
- NexxtPropertyCard: k,
25
- NexxtPropertyListing: B,
26
- NexxtSocialIcons: D,
27
- NexxtSocialMediaCustomTemplate: A,
28
- NexxtSocialMediaTemplate: H,
29
- NexxtSocialMediaType: w,
30
- NexxtSource: E,
31
- NexxtStepperHeader: F,
32
- NexxtTabbedContent: J,
33
- NexxtTable: L,
34
- NexxtTabs: j,
35
- NexxtTimelineEvent: $,
36
- NexxtTimelinePhaseblock: K,
37
- NexxtTooltip: O
38
- }, q = c({
16
+ NexxtDropdownButton: P,
17
+ NexxtHeader: S,
18
+ NexxtIcon: h,
19
+ NexxtInfoBlock: I,
20
+ NexxtInputField: f,
21
+ NexxtModal: g,
22
+ NexxtPagination: y,
23
+ NexxtPresenceIndicator: M,
24
+ NexxtProgressBar: k,
25
+ NexxtPropertyCard: B,
26
+ NexxtPropertyListing: D,
27
+ NexxtSocialIcons: A,
28
+ NexxtSocialMediaCustomTemplate: H,
29
+ NexxtSocialMediaTemplate: w,
30
+ NexxtSocialMediaType: E,
31
+ NexxtSource: F,
32
+ NexxtStepperHeader: J,
33
+ NexxtTabbedContent: L,
34
+ NexxtTable: j,
35
+ NexxtTabs: $,
36
+ NexxtTimelineEvent: K,
37
+ NexxtTimelinePhaseblock: O,
38
+ NexxtTooltip: U
39
+ }, z = c({
39
40
  meta: {
40
41
  name: "@nexxtmove/ui",
41
42
  configKey: "nexxtmoveUi",
@@ -47,7 +48,7 @@ const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar
47
48
  addCSS: !0
48
49
  },
49
50
  setup(e, o) {
50
- for (const [t, n] of Object.entries(U))
51
+ for (const [t, n] of Object.entries(_))
51
52
  s({
52
53
  name: t,
53
54
  export: "default",
@@ -66,5 +67,5 @@ const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar
66
67
  }
67
68
  });
68
69
  export {
69
- q as default
70
+ z as default
70
71
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "0.1.53",
4
+ "version": "0.1.54",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -2,41 +2,54 @@
2
2
  import { computed } from 'vue'
3
3
 
4
4
  interface NexxtChipProps {
5
- variant?: 'default' | 'warning' | 'success' | 'danger' | 'ghost'
5
+ variant?: 'default' | 'warning' | 'success' | 'danger'
6
+ ghost?: boolean
6
7
  }
7
8
 
8
9
  defineOptions({
9
10
  name: 'NexxtChip',
10
11
  })
11
12
 
12
- const { variant = 'default' } = defineProps<NexxtChipProps>()
13
+ const { variant = 'default', ghost = false } = defineProps<NexxtChipProps>()
13
14
 
14
- const backgroundColor = computed(() => {
15
+ const styles = computed(() => {
16
+ if (ghost) {
17
+ switch (variant) {
18
+ case 'warning':
19
+ return { bg: 'bg-orange-50', border: 'border border-orange-500', text: 'text-orange-500' }
20
+ case 'success':
21
+ return { bg: 'bg-green-50', border: 'border border-green-500', text: 'text-green-500' }
22
+ case 'danger':
23
+ return { bg: 'bg-brick-100/20', border: 'border border-brick-500', text: 'text-brick-500' }
24
+ default:
25
+ return {
26
+ bg: 'bg-cornflower-blue-50',
27
+ border: 'border border-cornflower-blue-500',
28
+ text: 'text-cornflower-blue-500',
29
+ }
30
+ }
31
+ }
15
32
  switch (variant) {
16
33
  case 'warning':
17
- return 'bg-orange-500'
34
+ return { bg: 'bg-orange-500', border: '', text: 'text-white' }
18
35
  case 'success':
19
- return 'bg-green-500'
36
+ return { bg: 'bg-green-500', border: '', text: 'text-white' }
20
37
  case 'danger':
21
- return 'bg-brick-500'
22
- case 'ghost':
23
- return 'bg-cornflower-blue-50'
38
+ return { bg: 'bg-brick-500', border: '', text: 'text-white' }
24
39
  default:
25
- return 'bg-cornflower-blue-600'
40
+ return { bg: 'bg-cornflower-blue-600', border: '', text: 'text-white' }
26
41
  }
27
42
  })
28
43
  </script>
29
44
 
30
45
  <template>
31
46
  <span
32
- class="inline-flex h-5 flex-col items-center justify-center gap-2.5 rounded-[200px] px-4"
33
- :class="[backgroundColor, variant === 'ghost' ? 'border border-cornflower-blue-500' : '']"
47
+ class="inline-flex h-5 flex-col items-center justify-center gap-2.5 rounded-[200px]"
48
+ :class="[styles.bg, styles.border, $slots.close ? 'pr-2 pl-4' : 'px-4']"
34
49
  >
35
- <span
36
- class="justify-start text-center extra-small-semibold"
37
- :class="variant === 'ghost' ? 'text-cornflower-blue-500' : 'text-white'"
38
- >
50
+ <span class="justify-start text-center extra-small-semibold" :class="styles.text">
39
51
  <slot>Chip</slot>
52
+ <slot v-if="$slots.close" name="close"></slot>
40
53
  </span>
41
54
  </span>
42
55
  </template>
@@ -1,17 +1,16 @@
1
1
  <script lang="ts" setup>
2
2
  import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
3
3
  import NexxtButton from '../Button/Button.vue'
4
- import NexxtChip from '../Chip/Chip.vue'
5
4
  import NexxtIcon from '../Icon/Icon.vue'
6
- import NexxtInputField from '../InputField/InputField.vue'
5
+ import ComboboxPanel from '../ComboboxPanel/ComboboxPanel.vue'
6
+ import type { ComboboxOptions } from '../ComboboxPanel/combobox-types'
7
7
 
8
- interface NexxtComboboxProps {
8
+ type NexxtComboboxProps = {
9
9
  variant?: InstanceType<typeof NexxtButton>['variant']
10
10
  icon?: InstanceType<typeof NexxtIcon>['name']
11
11
  placeholder?: string
12
12
  placement?: 'left' | 'right'
13
- options: (string | { label: string; value: string | number })[]
14
- }
13
+ } & ComboboxOptions
15
14
 
16
15
  const props = defineProps<NexxtComboboxProps>()
17
16
 
@@ -25,43 +24,13 @@ const {
25
24
  const selected = defineModel<(string | number)[]>({ default: [] })
26
25
 
27
26
  const isOpen = ref(false)
28
- const inputFieldRef = ref<InstanceType<typeof NexxtInputField> | null>(null)
27
+ const panelRef = ref<InstanceType<typeof ComboboxPanel> | null>(null)
29
28
  const wrapperRef = ref<HTMLElement | null>(null)
30
- const search = ref('')
31
-
32
- const getLabel = (option: string | { label: string; value: string | number }) =>
33
- typeof option === 'string' ? option : option.label
34
-
35
- const getValue = (option: string | { label: string; value: string | number }) =>
36
- typeof option === 'string' ? option : option.value
37
-
38
- const selectedLabels = computed(() =>
39
- selected.value.map((val) => {
40
- const match = props.options.find((o) => getValue(o) === val)
41
- return { value: val, label: match ? getLabel(match) : String(val) }
42
- }),
43
- )
44
-
45
- const filteredOptions = computed(() => {
46
- const q = search.value.toLowerCase()
47
- return props.options
48
- .filter((o) => !selected.value.includes(getValue(o)))
49
- .filter((o) => getLabel(o).toLowerCase().includes(q))
50
- })
51
-
52
- const selectOption = (option: string | { label: string; value: string | number }) => {
53
- selected.value = [...selected.value, getValue(option)]
54
- search.value = ''
55
- }
56
-
57
- const removeSelected = (val: string | number) => {
58
- selected.value = selected.value.filter((v) => v !== val)
59
- }
60
29
 
61
30
  watch(isOpen, async (open) => {
62
31
  if (open) {
63
32
  await nextTick()
64
- inputFieldRef.value?.$el.querySelector('input')?.focus()
33
+ panelRef.value?.focus()
65
34
  }
66
35
  })
67
36
 
@@ -108,74 +77,23 @@ const transitionClasses = computed(() => {
108
77
  aria-haspopup="listbox"
109
78
  @click="isOpen = !isOpen"
110
79
  >
111
- <span class="flex gap-2"><slot>Combobox</slot><NexxtIcon name="chevron-down" /> </span>
80
+ <span class="flex gap-2"><slot>Combobox</slot><NexxtIcon name="chevron-down" /></span>
112
81
  </NexxtButton>
113
82
  <Transition v-bind="transitionClasses">
114
83
  <div
115
84
  v-if="isOpen"
116
- class="absolute top-full z-10 mt-2.5 min-w-80 rounded-lg border border-gray-200 bg-white whitespace-nowrap"
85
+ class="absolute top-full z-10 mt-2.5"
117
86
  :class="placement === 'right' ? 'right-0' : 'left-0'"
118
87
  >
119
- <div class="p-2">
120
- <NexxtInputField
121
- ref="inputFieldRef"
122
- v-model="search"
123
- class="w-full"
124
- :left-icon="icon"
125
- :placeholder
126
- />
127
- </div>
128
- <Transition
129
- enter-active-class="transition-all duration-200 ease-out overflow-hidden"
130
- enter-from-class="max-h-0 opacity-0"
131
- enter-to-class="max-h-40 opacity-100"
132
- leave-active-class="transition-all duration-150 ease-in overflow-hidden"
133
- leave-from-class="max-h-40 opacity-100"
134
- leave-to-class="max-h-0 opacity-0"
135
- >
136
- <TransitionGroup
137
- v-if="selectedLabels.length > 0"
138
- tag="div"
139
- class="flex flex-wrap gap-1 border-b border-gray-200 p-2 pt-0"
140
- enter-active-class="transition-all duration-150 ease-out"
141
- enter-from-class="opacity-0 scale-75"
142
- enter-to-class="opacity-100 scale-100"
143
- leave-active-class="transition-all duration-100 ease-in"
144
- leave-from-class="opacity-100 scale-100"
145
- leave-to-class="opacity-0 scale-75"
146
- >
147
- <button
148
- v-for="item in selectedLabels"
149
- :key="item.value"
150
- class="origin-left cursor-pointer"
151
- :aria-label="`Verwijder ${item.label}`"
152
- @click="removeSelected(item.value)"
153
- >
154
- <NexxtChip variant="ghost">
155
- {{ item.label }}
156
- <span>
157
- <NexxtIcon name="xmark" class="text-xs" aria-hidden="true" />
158
- </span>
159
- </NexxtChip>
160
- </button>
161
- </TransitionGroup>
162
- </Transition>
163
- <ul
164
- role="listbox"
165
- aria-multiselectable="true"
166
- class="max-h-56 overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-cornflower-blue-200 [&::-webkit-scrollbar-thumb:hover]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"
167
- >
168
- <li
169
- v-for="option in filteredOptions"
170
- :key="getValue(option)"
171
- role="option"
172
- aria-selected="false"
173
- class="flex w-full min-w-42 cursor-pointer items-center gap-2 px-6 py-3 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
174
- @click="selectOption(option)"
175
- >
176
- {{ getLabel(option) }}
177
- </li>
178
- </ul>
88
+ <ComboboxPanel
89
+ ref="panelRef"
90
+ v-model="selected"
91
+ :options="props.options"
92
+ :allow-new="props.allowNew"
93
+ :icon
94
+ :placeholder
95
+ :floating="false"
96
+ />
179
97
  </div>
180
98
  </Transition>
181
99
  </div>
@@ -0,0 +1,293 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
+ import NexxtChip from '../Chip/Chip.vue'
4
+ import NexxtIcon from '../Icon/Icon.vue'
5
+ import NexxtInputField from '../InputField/InputField.vue'
6
+ import type { ComboboxOptions } from './combobox-types'
7
+
8
+ type ComboboxPanelProps = {
9
+ icon?: InstanceType<typeof NexxtIcon>['name']
10
+ placeholder?: string
11
+ floating?: boolean
12
+ } & ComboboxOptions
13
+
14
+ const props = defineProps<ComboboxPanelProps>()
15
+
16
+ const { icon = 'tag', placeholder = 'Zoeken...', floating = true } = props
17
+
18
+ const selected = defineModel<(string | number)[]>({ default: [] })
19
+
20
+ const inputFieldRef = ref<InstanceType<typeof NexxtInputField> | null>(null)
21
+ const wrapperRef = ref<HTMLElement | null>(null)
22
+ const search = ref('')
23
+ const isFocused = ref(false)
24
+
25
+ const handleClickOutside = (event: MouseEvent) => {
26
+ if (wrapperRef.value && !wrapperRef.value.contains(event.target as Node)) {
27
+ isFocused.value = false
28
+ }
29
+ }
30
+
31
+ const handleKeydown = (event: KeyboardEvent) => {
32
+ if (event.key === 'Escape') isFocused.value = false
33
+ }
34
+
35
+ onMounted(() => {
36
+ document.addEventListener('mousedown', handleClickOutside)
37
+ document.addEventListener('keydown', handleKeydown)
38
+ })
39
+
40
+ onUnmounted(() => {
41
+ document.removeEventListener('mousedown', handleClickOutside)
42
+ document.removeEventListener('keydown', handleKeydown)
43
+ })
44
+
45
+ const getLabel = (option: string | { label: string; value: string | number }) =>
46
+ typeof option === 'string' ? option : option.label
47
+
48
+ const getValue = (option: string | { label: string; value: string | number }) =>
49
+ typeof option === 'string' ? option : option.value
50
+
51
+ const selectedLabels = computed(() =>
52
+ selected.value.map((val) => {
53
+ const match = props.options.find((o) => getValue(o) === val)
54
+ return { value: val, label: match ? getLabel(match) : String(val), isCustom: !match }
55
+ }),
56
+ )
57
+
58
+ const filteredOptions = computed(() => {
59
+ const q = search.value.toLowerCase()
60
+ return props.options
61
+ .filter((o) => !selected.value.includes(getValue(o)))
62
+ .filter((o) => getLabel(o).toLowerCase().includes(q))
63
+ })
64
+
65
+ const showAddHint = computed(() => {
66
+ if (!props.allowNew || !search.value) return false
67
+ if (filteredOptions.value.length > 0) return false
68
+ const trimmed = search.value.trim().toLowerCase()
69
+ return !selectedLabels.value.some((item) => item.label.toLowerCase() === trimmed)
70
+ })
71
+
72
+ const showSuggestions = computed(() => filteredOptions.value.length > 0 || showAddHint.value)
73
+
74
+ const selectOption = (option: string | { label: string; value: string | number }) => {
75
+ selected.value = [...selected.value, getValue(option)]
76
+ search.value = ''
77
+ }
78
+
79
+ const handleEnter = () => {
80
+ if (filteredOptions.value.length === 1) {
81
+ selectOption(filteredOptions.value[0])
82
+ } else if (props.allowNew) {
83
+ addNewItem()
84
+ }
85
+ }
86
+
87
+ const addNewItem = () => {
88
+ const trimmed = search.value.trim()
89
+ if (!trimmed) return
90
+ const lower = trimmed.toLowerCase()
91
+ const alreadySelected = selectedLabels.value.some((item) => item.label.toLowerCase() === lower)
92
+ const existsAsOption = props.options.some((o) => getLabel(o).toLowerCase() === lower)
93
+ if (alreadySelected || existsAsOption) return
94
+ selected.value = [...selected.value, trimmed]
95
+ search.value = ''
96
+ }
97
+
98
+ const removeSelected = (val: string | number) => {
99
+ selected.value = selected.value.filter((v) => v !== val)
100
+ }
101
+
102
+ const focus = () => {
103
+ inputFieldRef.value?.$el.querySelector('input')?.focus()
104
+ }
105
+
106
+ defineExpose({ focus })
107
+ </script>
108
+
109
+ <template>
110
+ <!-- Standalone (floating=true): input+chips are a static card, suggestions float below -->
111
+ <div v-if="floating" ref="wrapperRef" class="relative min-w-80 whitespace-nowrap">
112
+ <div class="rounded-lg border border-gray-200 bg-white">
113
+ <div class="p-2">
114
+ <NexxtInputField
115
+ ref="inputFieldRef"
116
+ v-model="search"
117
+ class="w-full"
118
+ :left-icon="icon"
119
+ :placeholder
120
+ :aria-label="placeholder"
121
+ @focus="isFocused = true"
122
+ @keydown.enter="handleEnter"
123
+ />
124
+ </div>
125
+ <Transition
126
+ enter-active-class="transition-all duration-200 ease-out overflow-hidden"
127
+ enter-from-class="max-h-0 opacity-0"
128
+ enter-to-class="max-h-40 opacity-100"
129
+ leave-active-class="transition-all duration-150 ease-in overflow-hidden"
130
+ leave-from-class="max-h-40 opacity-100"
131
+ leave-to-class="max-h-0 opacity-0"
132
+ >
133
+ <TransitionGroup
134
+ v-if="selectedLabels.length > 0"
135
+ tag="div"
136
+ class="flex flex-wrap gap-1 p-2 pt-0"
137
+ enter-active-class="transition-all duration-150 ease-out"
138
+ enter-from-class="opacity-0 scale-75"
139
+ enter-to-class="opacity-100 scale-100"
140
+ leave-active-class="transition-all duration-100 ease-in"
141
+ leave-from-class="opacity-100 scale-100"
142
+ leave-to-class="opacity-0 scale-75"
143
+ >
144
+ <button
145
+ v-for="item in selectedLabels"
146
+ :key="item.value"
147
+ class="origin-left cursor-pointer"
148
+ :aria-label="`Verwijder ${item.label}`"
149
+ @click="removeSelected(item.value)"
150
+ >
151
+ <NexxtChip :ghost="true" class="group" :variant="item.isCustom ? 'success' : 'default'">
152
+ {{ item.label }}
153
+ <template #close>
154
+ <span>
155
+ <NexxtIcon
156
+ name="xmark"
157
+ class="text-xs group-hover:text-brick-500"
158
+ aria-hidden="true"
159
+ />
160
+ </span>
161
+ </template>
162
+ </NexxtChip>
163
+ </button>
164
+ </TransitionGroup>
165
+ </Transition>
166
+ </div>
167
+ <Transition
168
+ enter-active-class="transition-all duration-200 ease-out origin-top"
169
+ leave-active-class="transition-all duration-150 ease-in origin-top"
170
+ enter-from-class="opacity-0 scale-95 -translate-y-2"
171
+ leave-to-class="opacity-0 scale-95 -translate-y-2"
172
+ enter-to-class="opacity-100 scale-100 translate-y-0"
173
+ leave-from-class="opacity-100 scale-100 translate-y-0"
174
+ >
175
+ <div
176
+ v-if="isFocused && showSuggestions"
177
+ class="absolute top-full left-0 z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white"
178
+ @mousedown.prevent
179
+ >
180
+ <ul
181
+ role="listbox"
182
+ aria-multiselectable="true"
183
+ tabindex="0"
184
+ class="max-h-56 overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-cornflower-blue-200 [&::-webkit-scrollbar-thumb:hover]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"
185
+ >
186
+ <li
187
+ v-for="option in filteredOptions"
188
+ :key="getValue(option)"
189
+ role="option"
190
+ aria-selected="false"
191
+ class="flex w-full cursor-pointer items-center gap-2 px-6 py-3 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
192
+ @click="selectOption(option)"
193
+ >
194
+ {{ getLabel(option) }}
195
+ </li>
196
+ </ul>
197
+ <div
198
+ v-if="showAddHint"
199
+ class="flex grow-0 items-center gap-4 border-t border-gray-100 px-6 py-3 whitespace-normal text-gray-700"
200
+ @click="addNewItem()"
201
+ >
202
+ <NexxtIcon name="plus" class="cursor-pointer small-normal" aria-hidden="true" />
203
+ <div>
204
+ Druk op Enter om "<span class="small-semibold text-gray-800">{{ search }}</span
205
+ >" als kenmerk toe te voegen
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </Transition>
210
+ </div>
211
+
212
+ <!-- Inside Combobox (floating=false): everything in one flat card -->
213
+ <div v-else class="min-w-80 rounded-lg border border-gray-200 bg-white whitespace-nowrap">
214
+ <div class="p-2">
215
+ <NexxtInputField
216
+ ref="inputFieldRef"
217
+ v-model="search"
218
+ class="w-full"
219
+ :left-icon="icon"
220
+ :placeholder
221
+ @keydown.enter="handleEnter"
222
+ />
223
+ </div>
224
+ <Transition
225
+ enter-active-class="transition-all duration-200 ease-out overflow-hidden"
226
+ enter-from-class="max-h-0 opacity-0"
227
+ enter-to-class="max-h-40 opacity-100"
228
+ leave-active-class="transition-all duration-150 ease-in overflow-hidden"
229
+ leave-from-class="max-h-40 opacity-100"
230
+ leave-to-class="max-h-0 opacity-0"
231
+ >
232
+ <TransitionGroup
233
+ v-if="selectedLabels.length > 0"
234
+ tag="div"
235
+ class="flex flex-wrap gap-1 border-b border-gray-200 p-2 pt-0"
236
+ enter-active-class="transition-all duration-150 ease-out"
237
+ enter-from-class="opacity-0 scale-75"
238
+ enter-to-class="opacity-100 scale-100"
239
+ leave-active-class="transition-all duration-100 ease-in"
240
+ leave-from-class="opacity-100 scale-100"
241
+ leave-to-class="opacity-0 scale-75"
242
+ >
243
+ <button
244
+ v-for="item in selectedLabels"
245
+ :key="item.value"
246
+ class="origin-left cursor-pointer"
247
+ :aria-label="`Verwijder ${item.label}`"
248
+ @click="removeSelected(item.value)"
249
+ >
250
+ <NexxtChip :ghost="true" class="group" :variant="item.isCustom ? 'success' : 'default'">
251
+ {{ item.label }}
252
+ <template #close>
253
+ <span>
254
+ <NexxtIcon
255
+ name="xmark"
256
+ class="text-xs group-hover:text-brick-500"
257
+ aria-hidden="true"
258
+ />
259
+ </span>
260
+ </template>
261
+ </NexxtChip>
262
+ </button>
263
+ </TransitionGroup>
264
+ </Transition>
265
+ <ul
266
+ role="listbox"
267
+ aria-multiselectable="true"
268
+ class="max-h-56 overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-cornflower-blue-200 [&::-webkit-scrollbar-thumb:hover]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"
269
+ >
270
+ <li
271
+ v-for="option in filteredOptions"
272
+ :key="getValue(option)"
273
+ role="option"
274
+ aria-selected="false"
275
+ class="flex w-full min-w-42 cursor-pointer items-center gap-2 px-6 py-3 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
276
+ @click="selectOption(option)"
277
+ >
278
+ {{ getLabel(option) }}
279
+ </li>
280
+ </ul>
281
+ <div
282
+ v-if="showAddHint"
283
+ class="flex grow-0 items-center gap-4 border-t border-gray-100 px-6 py-3 whitespace-normal text-gray-700"
284
+ @click="addNewItem()"
285
+ >
286
+ <NexxtIcon name="plus" class="cursor-pointer small-normal" aria-hidden="true" />
287
+ <div>
288
+ Druk op Enter om "<span class="small-semibold text-gray-800">{{ search }}</span
289
+ >" als kenmerk toe te voegen
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </template>
@@ -0,0 +1,5 @@
1
+ export type ComboboxOption = string | { label: string; value: string | number }
2
+
3
+ export type ComboboxOptions =
4
+ | { options: string[]; allowNew?: boolean }
5
+ | { options: { label: string; value: string | number }[]; allowNew?: never }
@@ -2,7 +2,7 @@
2
2
 
3
3
  <template>
4
4
  <div class="rounded-xl border border-gray-200">
5
- <div class="border-b border-gray-200 px-8 py-2.5">
5
+ <div class="border-b border-gray-200 py-2.5 pr-3 pl-8">
6
6
  <slot name="title">Title</slot>
7
7
  </div>
8
8
  <div class="px-8 py-5">
@@ -7,6 +7,7 @@
7
7
  "NexxtCheckbox": "components/Checkbox/Checkbox.vue",
8
8
  "NexxtChip": "components/Chip/Chip.vue",
9
9
  "NexxtCombobox": "components/Combobox/Combobox.vue",
10
+ "NexxtComboboxPanel": "components/ComboboxPanel/ComboboxPanel.vue",
10
11
  "NexxtContactInfoCard": "components/ContactInfoCard/ContactInfoCard.vue",
11
12
  "NexxtCustomerJourneyTile": "components/CustomerJourneyTile/CustomerJourneyTile.vue",
12
13
  "NexxtDatePicker": "components/DatePicker/DatePicker.vue",
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { default as NexxtCard } from './components/Card/Card.vue'
7
7
  export { default as NexxtCheckbox } from './components/Checkbox/Checkbox.vue'
8
8
  export { default as NexxtChip } from './components/Chip/Chip.vue'
9
9
  export { default as NexxtCombobox } from './components/Combobox/Combobox.vue'
10
+ export { default as NexxtComboboxPanel } from './components/ComboboxPanel/ComboboxPanel.vue'
10
11
  export { default as NexxtContactInfoCard } from './components/ContactInfoCard/ContactInfoCard.vue'
11
12
  export { default as NexxtCustomerJourneyTile } from './components/CustomerJourneyTile/CustomerJourneyTile.vue'
12
13
  export { default as NexxtDatePicker } from './components/DatePicker/DatePicker.vue'