@nexxtmove/ui 1.38.0 → 1.40.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/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue.d.ts +18 -3
- package/dist/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue.d.ts +15 -3
- package/dist/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +11 -6
- package/dist/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue.d.ts +13 -3
- package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +55 -6
- package/dist/components/editor/molecules/EditorHeadingType/EditorHeadingType.vue.d.ts +16 -12
- package/dist/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue.d.ts +18 -3
- package/dist/components/editor/molecules/EditorPadding/EditorPadding.vue.d.ts +106 -0
- package/dist/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue.d.ts +18 -3
- package/dist/components/organisms/ComboboxPanel/_ComboboxPanelOptions.vue.d.ts +24 -0
- package/dist/components/organisms/ComboboxPanel/combobox-types.d.ts +17 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +5079 -4894
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue +35 -11
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue +33 -6
- package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +19 -12
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue +40 -5
- package/src/components/editor/molecules/EditorGap/EditorGap.vue +30 -15
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.vue +19 -15
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue +35 -15
- package/src/components/editor/molecules/EditorPadding/EditorPadding.vue +61 -19
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue +35 -7
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +58 -46
- package/src/components/organisms/ComboboxPanel/_ComboboxPanelOptions.vue +96 -0
- package/src/components/organisms/ComboboxPanel/combobox-types.ts +19 -0
- package/src/index.ts +14 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { ref, useTemplateRef, computed, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { ref, useTemplateRef, computed, onMounted, onUnmounted, useId } from 'vue'
|
|
3
3
|
import NexxtChip from '../../atoms/Chip/Chip.vue'
|
|
4
4
|
import NexxtIcon from '../../atoms/Icon/Icon.vue'
|
|
5
5
|
import NexxtInputField from '../../molecules/InputField/InputField.vue'
|
|
6
|
-
import
|
|
6
|
+
import ComboboxPanelOptions, { type OptionSection } from './_ComboboxPanelOptions.vue'
|
|
7
|
+
import type { ComboboxOption, ComboboxOptions, NexxtComboboxGroup } from './combobox-types'
|
|
7
8
|
|
|
8
9
|
type ComboboxPanelProps = {
|
|
9
10
|
icon?: InstanceType<typeof NexxtIcon>['name']
|
|
@@ -57,20 +58,53 @@ const getLabel = (option: string | { label: string; value: string | number }) =>
|
|
|
57
58
|
const getValue = (option: string | { label: string; value: string | number }) =>
|
|
58
59
|
typeof option === 'string' ? option : option.value
|
|
59
60
|
|
|
61
|
+
const isGroup = (item: ComboboxOption | NexxtComboboxGroup): item is NexxtComboboxGroup =>
|
|
62
|
+
typeof item === 'object' && 'options' in item
|
|
63
|
+
|
|
64
|
+
const groupIdBase = useId()
|
|
65
|
+
|
|
66
|
+
// Flat options become one untitled section, so everything below runs on one shape. Ids come from
|
|
67
|
+
// the group's position in `options`, not in the filtered list, so a heading keeps its id while
|
|
68
|
+
// searching
|
|
69
|
+
const sections = computed((): OptionSection[] => {
|
|
70
|
+
const items: (ComboboxOption | NexxtComboboxGroup)[] = props.options
|
|
71
|
+
const groups = items.filter(isGroup)
|
|
72
|
+
if (groups.length === 0) {
|
|
73
|
+
return [
|
|
74
|
+
{ id: groupIdBase, options: items.filter((item): item is ComboboxOption => !isGroup(item)) },
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
return groups.map((group, index) => ({
|
|
78
|
+
id: `${groupIdBase}-group-${index}`,
|
|
79
|
+
label: group.label,
|
|
80
|
+
options: group.options,
|
|
81
|
+
}))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const allOptions = computed(() => sections.value.flatMap((section) => section.options))
|
|
85
|
+
|
|
60
86
|
const selectedLabels = computed(() =>
|
|
61
87
|
selected.value.map((val) => {
|
|
62
|
-
const match =
|
|
88
|
+
const match = allOptions.value.find((o) => getValue(o) === val)
|
|
63
89
|
return { value: val, label: match ? getLabel(match) : String(val), isCustom: !match }
|
|
64
90
|
}),
|
|
65
91
|
)
|
|
66
92
|
|
|
67
|
-
|
|
93
|
+
// Matches on the option label only; a group without matches drops out, heading included
|
|
94
|
+
const filteredSections = computed(() => {
|
|
68
95
|
const q = search.value.toLowerCase()
|
|
69
|
-
return
|
|
70
|
-
.
|
|
71
|
-
|
|
96
|
+
return sections.value
|
|
97
|
+
.map((section) => ({
|
|
98
|
+
...section,
|
|
99
|
+
options: section.options
|
|
100
|
+
.filter((o) => props.singleSelect || !selected.value.includes(getValue(o)))
|
|
101
|
+
.filter((o) => getLabel(o).toLowerCase().includes(q)),
|
|
102
|
+
}))
|
|
103
|
+
.filter((section) => section.options.length > 0)
|
|
72
104
|
})
|
|
73
105
|
|
|
106
|
+
const filteredOptions = computed(() => filteredSections.value.flatMap((section) => section.options))
|
|
107
|
+
|
|
74
108
|
const showAddHint = computed(() => {
|
|
75
109
|
if (!props.allowNew || !search.value) return false
|
|
76
110
|
if (filteredOptions.value.length > 0) return false
|
|
@@ -92,6 +126,9 @@ const toggleSingle = (option: string | { label: string; value: string | number }
|
|
|
92
126
|
emit('select')
|
|
93
127
|
}
|
|
94
128
|
|
|
129
|
+
const pickOption = (option: ComboboxOption) =>
|
|
130
|
+
props.singleSelect ? toggleSingle(option) : selectOption(option)
|
|
131
|
+
|
|
95
132
|
const handleEnter = () => {
|
|
96
133
|
if (props.singleSelect) {
|
|
97
134
|
if (filteredOptions.value.length > 0) toggleSingle(filteredOptions.value[0])
|
|
@@ -110,7 +147,7 @@ const addNewItem = () => {
|
|
|
110
147
|
if (!trimmed) return
|
|
111
148
|
const lower = trimmed.toLowerCase()
|
|
112
149
|
const alreadySelected = selectedLabels.value.some((item) => item.label.toLowerCase() === lower)
|
|
113
|
-
const existsAsOption =
|
|
150
|
+
const existsAsOption = allOptions.value.some((o) => getLabel(o).toLowerCase() === lower)
|
|
114
151
|
if (alreadySelected || existsAsOption) return
|
|
115
152
|
if (props.singleSelect) {
|
|
116
153
|
selected.value = [trimmed]
|
|
@@ -210,25 +247,12 @@ defineExpose({ focus })
|
|
|
210
247
|
tabindex="0"
|
|
211
248
|
class="scrollbar-thin max-h-56 overflow-y-auto"
|
|
212
249
|
>
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
:class="{
|
|
220
|
-
'font-medium text-cornflower-blue-600':
|
|
221
|
-
singleSelect && selected.includes(getValue(option)),
|
|
222
|
-
}"
|
|
223
|
-
@click="singleSelect ? toggleSingle(option) : selectOption(option)"
|
|
224
|
-
>
|
|
225
|
-
{{ getLabel(option) }}
|
|
226
|
-
<NexxtIcon
|
|
227
|
-
v-if="singleSelect && selected.includes(getValue(option))"
|
|
228
|
-
name="check"
|
|
229
|
-
class="ml-auto shrink-0 text-xs"
|
|
230
|
-
/>
|
|
231
|
-
</li>
|
|
250
|
+
<ComboboxPanelOptions
|
|
251
|
+
:sections="filteredSections"
|
|
252
|
+
:selected
|
|
253
|
+
:single-select
|
|
254
|
+
@pick="pickOption"
|
|
255
|
+
/>
|
|
232
256
|
</ul>
|
|
233
257
|
<div
|
|
234
258
|
v-if="showAddHint"
|
|
@@ -299,25 +323,13 @@ defineExpose({ focus })
|
|
|
299
323
|
</TransitionGroup>
|
|
300
324
|
</Transition>
|
|
301
325
|
<ul role="listbox" aria-multiselectable="true" class="scrollbar-thin max-h-56 overflow-y-auto">
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
'font-medium text-cornflower-blue-600':
|
|
310
|
-
singleSelect && selected.includes(getValue(option)),
|
|
311
|
-
}"
|
|
312
|
-
@click="singleSelect ? toggleSingle(option) : selectOption(option)"
|
|
313
|
-
>
|
|
314
|
-
{{ getLabel(option) }}
|
|
315
|
-
<NexxtIcon
|
|
316
|
-
v-if="singleSelect && selected.includes(getValue(option))"
|
|
317
|
-
name="check"
|
|
318
|
-
class="ml-auto shrink-0 text-xs"
|
|
319
|
-
/>
|
|
320
|
-
</li>
|
|
326
|
+
<ComboboxPanelOptions
|
|
327
|
+
:sections="filteredSections"
|
|
328
|
+
:selected
|
|
329
|
+
:single-select
|
|
330
|
+
inline
|
|
331
|
+
@pick="pickOption"
|
|
332
|
+
/>
|
|
321
333
|
</ul>
|
|
322
334
|
<div
|
|
323
335
|
v-if="showAddHint"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComboboxOption } from './combobox-types'
|
|
3
|
+
|
|
4
|
+
/** A run of options in the listbox; `label` is set for a titled group only. */
|
|
5
|
+
export type OptionSection = { id: string; label?: string; options: ComboboxOption[] }
|
|
6
|
+
|
|
7
|
+
export interface ComboboxPanelOptionsProps {
|
|
8
|
+
/** Visible options per section. An untitled section renders its options straight into the list. */
|
|
9
|
+
sections: OptionSection[]
|
|
10
|
+
/** Selected values. */
|
|
11
|
+
selected: (string | number)[]
|
|
12
|
+
/** Marks the selected option with a tint and a check. */
|
|
13
|
+
singleSelect?: boolean
|
|
14
|
+
/** Inline layout (inside `NexxtCombobox`): rows get a minimum width. */
|
|
15
|
+
inline?: boolean
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts" setup>
|
|
20
|
+
import { createReusableTemplate } from '@vueuse/core'
|
|
21
|
+
import NexxtIcon from '../../atoms/Icon/Icon.vue'
|
|
22
|
+
|
|
23
|
+
defineOptions({ name: 'NexxtComboboxPanelOptions' })
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
sections,
|
|
27
|
+
selected,
|
|
28
|
+
singleSelect = false,
|
|
29
|
+
inline = false,
|
|
30
|
+
} = defineProps<ComboboxPanelOptionsProps>()
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{ pick: [option: ComboboxOption] }>()
|
|
33
|
+
|
|
34
|
+
const getLabel = (option: ComboboxOption) => (typeof option === 'string' ? option : option.label)
|
|
35
|
+
|
|
36
|
+
const getValue = (option: ComboboxOption) => (typeof option === 'string' ? option : option.value)
|
|
37
|
+
|
|
38
|
+
const isSelected = (option: ComboboxOption) => selected.includes(getValue(option))
|
|
39
|
+
|
|
40
|
+
const isActive = (option: ComboboxOption) => singleSelect && isSelected(option)
|
|
41
|
+
|
|
42
|
+
// One option row, shared by grouped and flat sections. Both bindings are declared as props:
|
|
43
|
+
// undeclared, they would be attrs and fall through onto the `<li>`
|
|
44
|
+
const [DefineOption, ReuseOption] = createReusableTemplate<{
|
|
45
|
+
option: ComboboxOption
|
|
46
|
+
indented?: boolean
|
|
47
|
+
}>({
|
|
48
|
+
props: { option: { type: [String, Object], required: true }, indented: Boolean },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const OPTION_CLASS =
|
|
52
|
+
'flex w-full cursor-pointer items-center gap-2 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'
|
|
53
|
+
|
|
54
|
+
// Options in a group step in 12px past their heading, so the hierarchy reads at a glance
|
|
55
|
+
const optionClass = (option: ComboboxOption, indented = false) => [
|
|
56
|
+
OPTION_CLASS,
|
|
57
|
+
indented ? 'pr-6 pl-9' : 'px-6',
|
|
58
|
+
{ 'min-w-42': inline, 'font-medium text-cornflower-blue-600': isActive(option) },
|
|
59
|
+
]
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<DefineOption v-slot="{ option, indented }">
|
|
64
|
+
<li
|
|
65
|
+
role="option"
|
|
66
|
+
:aria-selected="isSelected(option)"
|
|
67
|
+
:class="optionClass(option, indented)"
|
|
68
|
+
@click="emit('pick', option)"
|
|
69
|
+
>
|
|
70
|
+
{{ getLabel(option) }}
|
|
71
|
+
<NexxtIcon v-if="isActive(option)" name="check" class="ml-auto shrink-0 text-xs" />
|
|
72
|
+
</li>
|
|
73
|
+
</DefineOption>
|
|
74
|
+
|
|
75
|
+
<template v-for="section in sections" :key="section.id">
|
|
76
|
+
<!-- `<li>` may not carry role="group", so the group sits in a presentational item. A listbox
|
|
77
|
+
may only hold options and groups: the heading is presentational too, and names the group
|
|
78
|
+
through `aria-labelledby` -->
|
|
79
|
+
<li v-if="section.label !== undefined" role="none">
|
|
80
|
+
<div role="group" :aria-labelledby="section.id">
|
|
81
|
+
<div
|
|
82
|
+
role="presentation"
|
|
83
|
+
class="px-6 pt-3 pb-1 extra-small-semibold tracking-wide text-gray-700 uppercase select-none"
|
|
84
|
+
>
|
|
85
|
+
<span :id="section.id">{{ section.label }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<ul role="none">
|
|
88
|
+
<ReuseOption v-for="option in section.options" :key="getValue(option)" :option indented />
|
|
89
|
+
</ul>
|
|
90
|
+
</div>
|
|
91
|
+
</li>
|
|
92
|
+
<template v-else>
|
|
93
|
+
<ReuseOption v-for="option in section.options" :key="getValue(option)" :option />
|
|
94
|
+
</template>
|
|
95
|
+
</template>
|
|
96
|
+
</template>
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
export type ComboboxOption = string | { label: string; value: string | number }
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A titled group of options. Option values must be unique across **all** groups: selection is
|
|
5
|
+
* stored by value only, so a value that appears twice cannot tell its groups apart.
|
|
6
|
+
*/
|
|
7
|
+
export type NexxtComboboxGroup = {
|
|
8
|
+
/** Heading shown above the group, and the accessible name of its `role="group"`. */
|
|
9
|
+
label: string
|
|
10
|
+
/** The options of this group. */
|
|
11
|
+
options: ComboboxOption[]
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export type ComboboxOptions =
|
|
4
15
|
| { options: string[]; allowNew?: boolean }
|
|
5
16
|
| { options: { label: string; value: string | number }[]; allowNew?: never }
|
|
17
|
+
| {
|
|
18
|
+
/**
|
|
19
|
+
* Options in titled groups. Values must be unique across all groups. Groups cannot be
|
|
20
|
+
* combined with `allowNew`.
|
|
21
|
+
*/
|
|
22
|
+
options: NexxtComboboxGroup[]
|
|
23
|
+
allowNew?: never
|
|
24
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type { EnergyLabelLetter } from './components/atoms/EnergyLabel/EnergyLab
|
|
|
13
13
|
export type { IconType, NexxtIconName } from './components/atoms/Icon/Icon.vue'
|
|
14
14
|
export type { CustomIconName } from './components/atoms/Icon/customIcons'
|
|
15
15
|
export type { NexxtTabItem } from './components/molecules/Tabs/Tabs.vue'
|
|
16
|
+
export type { NexxtComboboxGroup } from './components/organisms/ComboboxPanel/combobox-types'
|
|
16
17
|
export type {
|
|
17
18
|
NexxtPageCardProps,
|
|
18
19
|
NexxtPageCardSlots,
|
|
@@ -33,18 +34,22 @@ export type {
|
|
|
33
34
|
NexxtPlaceholderInputMessages,
|
|
34
35
|
NexxtPlaceholderInputProps,
|
|
35
36
|
} from './components/molecules/PlaceholderInput/PlaceholderInput.vue'
|
|
37
|
+
export { EDITOR_ALIGN_ITEMS_OPTIONS } from './components/editor/molecules/EditorAlignItems/EditorAlignItems.vue'
|
|
36
38
|
export type {
|
|
37
39
|
EditorAlignItemsMessages,
|
|
38
40
|
EditorAlignItemsValue,
|
|
39
41
|
} from './components/editor/molecules/EditorAlignItems/EditorAlignItems.vue'
|
|
42
|
+
export { EDITOR_BACKGROUND_SIZE_OPTIONS } from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
|
|
40
43
|
export type {
|
|
41
44
|
EditorBackgroundSizeMessages,
|
|
42
45
|
EditorBackgroundSizeValue,
|
|
43
46
|
} from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
|
|
47
|
+
export { EDITOR_FLEX_DIRECTION_OPTIONS } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
|
|
44
48
|
export type {
|
|
45
49
|
EditorFlexDirectionMessages,
|
|
46
50
|
EditorFlexDirectionValue,
|
|
47
51
|
} from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
|
|
52
|
+
export { EDITOR_GAP_STEPS } from './components/editor/molecules/EditorGap/EditorGap.vue'
|
|
48
53
|
export type {
|
|
49
54
|
EditorGapMessages,
|
|
50
55
|
EditorGapValue,
|
|
@@ -58,10 +63,12 @@ export type {
|
|
|
58
63
|
EditorColorThemeRole,
|
|
59
64
|
EditorColorValue,
|
|
60
65
|
} from './components/editor/molecules/EditorColor/EditorColor.vue'
|
|
66
|
+
export { EDITOR_BORDER_STEPS } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
|
|
61
67
|
export type {
|
|
62
68
|
EditorBorderMessages,
|
|
63
69
|
EditorBorderWidth,
|
|
64
70
|
} from './components/editor/molecules/EditorBorder/EditorBorder.vue'
|
|
71
|
+
export { EDITOR_HEADING_TYPE_OPTIONS } from './components/editor/molecules/EditorHeadingType/EditorHeadingType.vue'
|
|
65
72
|
export type {
|
|
66
73
|
EditorHeadingTypeMessages,
|
|
67
74
|
EditorHeadingTypeValue,
|
|
@@ -71,10 +78,12 @@ export type {
|
|
|
71
78
|
EditorRadiusMessages,
|
|
72
79
|
EditorRadiusValue,
|
|
73
80
|
} from './components/editor/molecules/EditorRadius/EditorRadius.vue'
|
|
81
|
+
export { EDITOR_TEXT_ALIGN_OPTIONS } from './components/editor/molecules/EditorTextAlign/EditorTextAlign.vue'
|
|
74
82
|
export type {
|
|
75
83
|
EditorTextAlignMessages,
|
|
76
84
|
EditorTextAlignValue,
|
|
77
85
|
} from './components/editor/molecules/EditorTextAlign/EditorTextAlign.vue'
|
|
86
|
+
export { EDITOR_JUSTIFY_CONTENT_OPTIONS } from './components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue'
|
|
78
87
|
export type {
|
|
79
88
|
EditorJustifyContentMessages,
|
|
80
89
|
EditorJustifyContentValue,
|
|
@@ -94,7 +103,11 @@ export type {
|
|
|
94
103
|
EditorLetterSpacingMessages,
|
|
95
104
|
EditorLetterSpacingValue,
|
|
96
105
|
} from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
|
|
97
|
-
export
|
|
106
|
+
export { EDITOR_PADDING_STEPS } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
|
|
107
|
+
export type {
|
|
108
|
+
EditorPaddingMessages,
|
|
109
|
+
EditorPaddingStep,
|
|
110
|
+
} from './components/editor/molecules/EditorPadding/EditorPadding.vue'
|
|
98
111
|
export type {
|
|
99
112
|
NexxtTemplateCardContentFeature,
|
|
100
113
|
NexxtTemplateCardProps,
|