vunor 0.0.2
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/README.md +33 -0
- package/dist/theme.d.ts +133 -0
- package/dist/theme.mjs +1149 -0
- package/dist/vite.d.ts +5 -0
- package/dist/vite.mjs +13 -0
- package/package.json +83 -0
- package/src/components/AppLayout/AppLayout.vue +114 -0
- package/src/components/Button/Button.vue +49 -0
- package/src/components/Button/ButtonBase.vue +43 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Button/shortcuts.ts +29 -0
- package/src/components/Card/Card.vue +47 -0
- package/src/components/Card/CardHeader.vue +26 -0
- package/src/components/Card/CardInner.vue +5 -0
- package/src/components/Card/index.ts +3 -0
- package/src/components/Card/pi.ts +46 -0
- package/src/components/Card/shortcuts.ts +19 -0
- package/src/components/Checkbox/Checkbox.vue +59 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/Checkbox/shortcuts.ts +27 -0
- package/src/components/Combobox/Combobox.vue +502 -0
- package/src/components/Combobox/index.ts +2 -0
- package/src/components/Combobox/shortcuts.ts +12 -0
- package/src/components/Combobox/types.ts +33 -0
- package/src/components/Icon/Icon.vue +9 -0
- package/src/components/Icon/index.ts +1 -0
- package/src/components/Input/Input.vue +109 -0
- package/src/components/Input/InputShell.vue +133 -0
- package/src/components/Input/index.ts +4 -0
- package/src/components/Input/pi.ts +18 -0
- package/src/components/Input/types.ts +52 -0
- package/src/components/Input/utils.ts +108 -0
- package/src/components/Label/Label.vue +6 -0
- package/src/components/Label/index.ts +1 -0
- package/src/components/Loading/LoadingIndicator.vue +23 -0
- package/src/components/Loading/index.ts +1 -0
- package/src/components/Loading/shortcuts.ts +9 -0
- package/src/components/Menu/Menu.vue +100 -0
- package/src/components/Menu/MenuItem.vue +20 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/Menu/shortcuts.ts +6 -0
- package/src/components/OverflowContainer/OverflowContainer.vue +120 -0
- package/src/components/OverflowContainer/index.ts +1 -0
- package/src/components/Pagination/Pagination.vue +71 -0
- package/src/components/Popover/Popover.vue +58 -0
- package/src/components/Popover/index.ts +1 -0
- package/src/components/RadioGroup/RadioGroup.vue +83 -0
- package/src/components/RadioGroup/index.ts +1 -0
- package/src/components/RadioGroup/shortcuts.ts +34 -0
- package/src/components/Select/Select.vue +93 -0
- package/src/components/Select/SelectBase.vue +148 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/shortcuts.ts +30 -0
- package/src/components/Select/types.ts +30 -0
- package/src/components/Slider/Slider.vue +73 -0
- package/src/components/Slider/index.ts +1 -0
- package/src/components/Slider/shortcuts.ts +23 -0
- package/src/components/shortcuts.ts +21 -0
- package/src/components/utils/index.ts +1 -0
- package/src/components/utils/provide-inject.ts +39 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useInputPi } from './pi'
|
|
3
|
+
import type { TInputShellProps, TInputShellEmits } from './types'
|
|
4
|
+
import { useInputDataAttrs, useHtmlInputAttrs } from './utils'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<TInputShellProps>(), {
|
|
7
|
+
design: 'flat',
|
|
8
|
+
rows: 3,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<TInputShellEmits>()
|
|
12
|
+
|
|
13
|
+
const modelValue = defineModel<string | number>()
|
|
14
|
+
|
|
15
|
+
const focused = ref<boolean>(false)
|
|
16
|
+
|
|
17
|
+
useInputPi().inject(focused)
|
|
18
|
+
|
|
19
|
+
const attrs = useInputDataAttrs()
|
|
20
|
+
const inputAttrs = useHtmlInputAttrs()
|
|
21
|
+
|
|
22
|
+
function onFocus(event: FocusEvent) {
|
|
23
|
+
focused.value = true
|
|
24
|
+
emit('focus', event)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function onBlur(event: FocusEvent) {
|
|
28
|
+
focused.value = false
|
|
29
|
+
emit('blur', event)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function taGrow(event: Event) {
|
|
33
|
+
if (props.autoGrow) {
|
|
34
|
+
const ta = event.target as HTMLTextAreaElement | undefined
|
|
35
|
+
if (ta) {
|
|
36
|
+
ta.style.height = 'auto'
|
|
37
|
+
ta.style.height = ta.scrollHeight + 'px'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function focusInput(event: MouseEvent) {
|
|
43
|
+
const input = (event.target as HTMLDivElement | undefined)?.querySelector(
|
|
44
|
+
'input, textarea'
|
|
45
|
+
) as HTMLInputElement
|
|
46
|
+
if (input) {
|
|
47
|
+
input.focus()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<Primitive
|
|
54
|
+
@click="focusInput"
|
|
55
|
+
class="i8 group/i8 flex-grow"
|
|
56
|
+
:class="{
|
|
57
|
+
'i8-flat': design === 'flat',
|
|
58
|
+
'i8-filled': design === 'filled' || design === 'round',
|
|
59
|
+
'i8-round': design === 'round',
|
|
60
|
+
'segmented': groupItem,
|
|
61
|
+
}"
|
|
62
|
+
v-bind="attrs"
|
|
63
|
+
:data-active="focused || active"
|
|
64
|
+
>
|
|
65
|
+
<span class="i8-underline" />
|
|
66
|
+
<span class="absolute left-0 right-0 top-0 bottom-0" v-if="!!$slots.overlay">
|
|
67
|
+
<slot name="overlay"></slot>
|
|
68
|
+
</span>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
v-if="$slots.prepend || !!iconPrepend"
|
|
72
|
+
class="i8-prepend"
|
|
73
|
+
:class="{
|
|
74
|
+
'i8-icon-clickable': !!onPrependClick,
|
|
75
|
+
}"
|
|
76
|
+
>
|
|
77
|
+
<slot name="prepend" v-bind="attrs">
|
|
78
|
+
<VuIcon :name="iconPrepend!" @click="emit('prependClick', $event)" />
|
|
79
|
+
</slot>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="i8-input-wrapper">
|
|
83
|
+
<div
|
|
84
|
+
v-if="!!label"
|
|
85
|
+
class="i8-label-wrapper"
|
|
86
|
+
:data-has-prepend="inputAttrs?.['data-has-prepend']"
|
|
87
|
+
:data-has-append="inputAttrs?.['data-has-append']"
|
|
88
|
+
>
|
|
89
|
+
<label v-if="!!label" class="i8-label" :data-required="required">{{ label }}</label>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<slot v-bind="inputAttrs!" :onFocus :onBlur>
|
|
93
|
+
<div v-if="type === 'textarea'" class="i8-ta-wrapper">
|
|
94
|
+
<textarea
|
|
95
|
+
v-bind="inputAttrs"
|
|
96
|
+
style="resize: none"
|
|
97
|
+
class="i8-textarea"
|
|
98
|
+
v-model="modelValue"
|
|
99
|
+
@input="taGrow"
|
|
100
|
+
@focus="onFocus"
|
|
101
|
+
@blur="onBlur"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
<input
|
|
105
|
+
v-else
|
|
106
|
+
v-bind="inputAttrs"
|
|
107
|
+
class="i8-input"
|
|
108
|
+
v-model="modelValue"
|
|
109
|
+
@focus="onFocus"
|
|
110
|
+
@blur="onBlur"
|
|
111
|
+
/>
|
|
112
|
+
</slot>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div
|
|
116
|
+
v-if="$slots.append || !!iconAppend || loading"
|
|
117
|
+
class="i8-append"
|
|
118
|
+
:class="{
|
|
119
|
+
'i8-icon-clickable': !!onAppendClick,
|
|
120
|
+
}"
|
|
121
|
+
>
|
|
122
|
+
<VuLoadingIndicator v-if="loading" class="text-grey" />
|
|
123
|
+
<slot
|
|
124
|
+
name="append"
|
|
125
|
+
v-bind="attrs"
|
|
126
|
+
:emitClick="(event: MouseEvent) => emit('appendClick', event)"
|
|
127
|
+
:iconAppend
|
|
128
|
+
>
|
|
129
|
+
<VuIcon :name="iconAppend!" @click="emit('appendClick', $event)" />
|
|
130
|
+
</slot>
|
|
131
|
+
</div>
|
|
132
|
+
</Primitive>
|
|
133
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { computed, onUnmounted, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { useProvideInject } from '../utils'
|
|
4
|
+
|
|
5
|
+
export const useInputPi = () =>
|
|
6
|
+
useProvideInject('__vunor_input_PI', () => {
|
|
7
|
+
const focused = computed(() => !!inputs.value.some(a => a.value))
|
|
8
|
+
const inputs = ref<Array<Ref<boolean>>>([])
|
|
9
|
+
return {
|
|
10
|
+
_provide: () => ({ focused }),
|
|
11
|
+
_inject: (a: Ref<boolean>) => {
|
|
12
|
+
inputs.value.push(a)
|
|
13
|
+
onUnmounted(() => {
|
|
14
|
+
inputs.value = inputs.value.filter(i => i !== a)
|
|
15
|
+
})
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface TInputAttrs {
|
|
2
|
+
'placeholder'?: string
|
|
3
|
+
'type'?: string
|
|
4
|
+
'rows'?: number
|
|
5
|
+
'required'?: boolean
|
|
6
|
+
'maxlength'?: number
|
|
7
|
+
'data-has-prepend': boolean
|
|
8
|
+
'data-has-append': boolean
|
|
9
|
+
'data-has-label': boolean
|
|
10
|
+
'disabled'?: boolean
|
|
11
|
+
'readonly'?: boolean
|
|
12
|
+
'autocomplete'?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TInputShellProps
|
|
16
|
+
extends Omit<TInputAttrs, 'data-has-prepend' | 'data-has-append' | 'data-has-label'> {
|
|
17
|
+
label?: string
|
|
18
|
+
design?: 'flat' | 'filled' | 'round'
|
|
19
|
+
iconPrepend?: string
|
|
20
|
+
iconAppend?: string
|
|
21
|
+
groupItem?: boolean
|
|
22
|
+
autoGrow?: boolean
|
|
23
|
+
active?: boolean
|
|
24
|
+
loading?: boolean
|
|
25
|
+
onAppendClick?: (event: MouseEvent) => void
|
|
26
|
+
onPrependClick?: (event: MouseEvent) => void
|
|
27
|
+
onBlur?: (event: FocusEvent) => void
|
|
28
|
+
onFocus?: (event: FocusEvent) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TInputProps extends TInputShellProps {
|
|
32
|
+
iconBefore?: string
|
|
33
|
+
iconAfter?: string
|
|
34
|
+
error?: string | boolean
|
|
35
|
+
hint?: string
|
|
36
|
+
groupTemplate?: string
|
|
37
|
+
stackLabel?: boolean
|
|
38
|
+
onBeforeClick?: (event: MouseEvent) => void
|
|
39
|
+
onAfterClick?: (event: MouseEvent) => void
|
|
40
|
+
onClick?: (event: MouseEvent) => void
|
|
41
|
+
onBlur?: (event: FocusEvent) => void
|
|
42
|
+
onFocus?: (event: FocusEvent) => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TInputShellEmits {
|
|
46
|
+
(e: 'prependClick' | 'appendClick', event: MouseEvent): void
|
|
47
|
+
(e: 'blur' | 'focus', event: FocusEvent): void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TInputEmits extends TInputShellEmits {
|
|
51
|
+
(e: 'beforeClick' | 'afterClick' | 'click', event: MouseEvent): void
|
|
52
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
|
2
|
+
import type { ComputedRef } from 'vue'
|
|
3
|
+
import { computed, getCurrentInstance } from 'vue'
|
|
4
|
+
|
|
5
|
+
import type { TInputAttrs, TInputProps, TInputShellProps } from './types'
|
|
6
|
+
|
|
7
|
+
export function useHtmlInputAttrs(): ComputedRef<TInputAttrs> | undefined {
|
|
8
|
+
const instance = getCurrentInstance()
|
|
9
|
+
if (instance) {
|
|
10
|
+
const props = instance.props as unknown as TInputShellProps
|
|
11
|
+
return computed(() => ({
|
|
12
|
+
'placeholder': props.placeholder,
|
|
13
|
+
'type': props.type ?? 'text',
|
|
14
|
+
'rows': props.rows,
|
|
15
|
+
'required': props.required,
|
|
16
|
+
'disabled': props.disabled,
|
|
17
|
+
'readonly': props.readonly,
|
|
18
|
+
'autocomplete': props.autocomplete,
|
|
19
|
+
'data-has-prepend': !!instance.slots.prepend || !!instance.props.iconPrepend,
|
|
20
|
+
'data-has-append':
|
|
21
|
+
!!instance.slots.append || !!instance.props.iconAppend || !!instance.props.loading,
|
|
22
|
+
'data-has-label': !!props.label,
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useInputProps(): ComputedRef<TInputProps> | undefined {
|
|
28
|
+
const instance = getCurrentInstance()
|
|
29
|
+
if (instance) {
|
|
30
|
+
const props = instance.props as unknown as TInputProps
|
|
31
|
+
return computed(() => ({
|
|
32
|
+
label: props.label,
|
|
33
|
+
stackLabel: props.stackLabel,
|
|
34
|
+
placeholder: props.placeholder,
|
|
35
|
+
design: props.design,
|
|
36
|
+
readonly: props.readonly,
|
|
37
|
+
disabled: props.disabled,
|
|
38
|
+
iconPrepend: props.iconPrepend,
|
|
39
|
+
iconAppend: props.iconAppend,
|
|
40
|
+
groupItem: props.groupItem,
|
|
41
|
+
type: props.type,
|
|
42
|
+
rows: props.type === 'textarea' ? props.rows : undefined,
|
|
43
|
+
autoGrow: props.autoGrow,
|
|
44
|
+
maxlength: props.maxlength,
|
|
45
|
+
required: props.required,
|
|
46
|
+
active: props.active,
|
|
47
|
+
loading: props.loading,
|
|
48
|
+
onAppendClick: props.onAppendClick,
|
|
49
|
+
onPrependClick: props.onPrependClick,
|
|
50
|
+
onBlur: props.onBlur,
|
|
51
|
+
onFocus: props.onFocus,
|
|
52
|
+
iconBefore: props.iconBefore,
|
|
53
|
+
iconAfter: props.iconAfter,
|
|
54
|
+
error: props.error,
|
|
55
|
+
hint: props.hint,
|
|
56
|
+
autocomplete: props.autocomplete,
|
|
57
|
+
onBeforeClick: props.onBeforeClick,
|
|
58
|
+
onAfterClick: props.onAfterClick,
|
|
59
|
+
onClick: props.onClick,
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function useInputShellProps(): ComputedRef<TInputShellProps> | undefined {
|
|
64
|
+
const instance = getCurrentInstance()
|
|
65
|
+
if (instance) {
|
|
66
|
+
const props = instance.props as unknown as TInputProps
|
|
67
|
+
return computed(() => ({
|
|
68
|
+
label: props.stackLabel ? undefined : props.label,
|
|
69
|
+
placeholder: props.placeholder,
|
|
70
|
+
readonly: props.readonly,
|
|
71
|
+
design: props.design,
|
|
72
|
+
disabled: props.disabled,
|
|
73
|
+
loading: props.loading,
|
|
74
|
+
iconPrepend: props.iconPrepend,
|
|
75
|
+
iconAppend: props.iconAppend,
|
|
76
|
+
groupItem: props.groupItem,
|
|
77
|
+
type: props.type,
|
|
78
|
+
rows: props.type === 'textarea' ? props.rows : undefined,
|
|
79
|
+
autoGrow: props.autoGrow,
|
|
80
|
+
maxlength: props.maxlength,
|
|
81
|
+
required: props.required,
|
|
82
|
+
active: props.active,
|
|
83
|
+
autocomplete: props.autocomplete,
|
|
84
|
+
onAppendClick: props.onAppendClick,
|
|
85
|
+
onPrependClick: props.onPrependClick,
|
|
86
|
+
onBlur: props.onBlur,
|
|
87
|
+
onFocus: props.onFocus,
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function useInputDataAttrs() {
|
|
94
|
+
const instance = getCurrentInstance() as unknown as
|
|
95
|
+
| {
|
|
96
|
+
props: TInputProps
|
|
97
|
+
setupState?: { modelValue: string | number | undefined }
|
|
98
|
+
}
|
|
99
|
+
| undefined
|
|
100
|
+
return computed(() => ({
|
|
101
|
+
'data-has-label': instance?.props.label ? '' : undefined,
|
|
102
|
+
'data-has-placeholder': instance?.props.placeholder ? '' : undefined,
|
|
103
|
+
'data-has-value':
|
|
104
|
+
instance?.setupState?.modelValue || instance?.setupState?.modelValue === 0 ? '' : undefined,
|
|
105
|
+
'data-type': instance?.props.type ?? 'text',
|
|
106
|
+
'aria-disabled': instance?.props.disabled ? true : undefined,
|
|
107
|
+
}))
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as VuLabel } from './Label.vue'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{ size: string }>(), { size: '1em' })
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<svg
|
|
7
|
+
:width="size"
|
|
8
|
+
:height="size"
|
|
9
|
+
viewBox="0 0 14 14"
|
|
10
|
+
fill="none"
|
|
11
|
+
class="loading-indicator loading-indicator-ring"
|
|
12
|
+
>
|
|
13
|
+
<circle
|
|
14
|
+
cx="50%"
|
|
15
|
+
cy="50%"
|
|
16
|
+
r="6"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
stroke-dasharray="38 38"
|
|
19
|
+
stroke-width="1"
|
|
20
|
+
class=""
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as VuLoadingIndicator } from './LoadingIndicator.vue'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { scFromObject } from '../../theme/utils/shortcut-obj'
|
|
2
|
+
|
|
3
|
+
export const loadingShortcuts = {
|
|
4
|
+
'loading-indicator': 'cursor-wait',
|
|
5
|
+
'loading-indicator-ring': scFromObject({
|
|
6
|
+
'': 'animate-spin animate-duration-1500',
|
|
7
|
+
'[&>circle]:': 'animate-loading-dashoffset animate-count-infinite animate-duration-2500',
|
|
8
|
+
}),
|
|
9
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import VuMenuItem from './MenuItem.vue'
|
|
3
|
+
import VuInput from '../Input/Input.vue'
|
|
4
|
+
|
|
5
|
+
type AcceptableValue = string | number | boolean | Record<string, any>
|
|
6
|
+
type TItem = { label: string; value: AcceptableValue; icon?: string; group?: string }
|
|
7
|
+
type Props = {
|
|
8
|
+
items: (string | TItem)[]
|
|
9
|
+
emptyText?: string
|
|
10
|
+
}
|
|
11
|
+
const props = defineProps<Props>()
|
|
12
|
+
|
|
13
|
+
const modelValue = defineModel<AcceptableValue>()
|
|
14
|
+
|
|
15
|
+
const groups = computed(() => {
|
|
16
|
+
const grps = {} as Record<string, TItem[]>
|
|
17
|
+
for (const item of props.items) {
|
|
18
|
+
const _item: TItem =
|
|
19
|
+
typeof item === 'string'
|
|
20
|
+
? {
|
|
21
|
+
label: item,
|
|
22
|
+
value: item,
|
|
23
|
+
icon: '',
|
|
24
|
+
group: '',
|
|
25
|
+
}
|
|
26
|
+
: item
|
|
27
|
+
if (!_item.group) {
|
|
28
|
+
_item.group = ''
|
|
29
|
+
}
|
|
30
|
+
grps[_item.group] = grps[_item.group] || []
|
|
31
|
+
grps[_item.group].push(_item)
|
|
32
|
+
}
|
|
33
|
+
return Object.entries(grps)
|
|
34
|
+
.sort(([a], [b]) => (a === '' ? -1 : b === '' ? 1 : 0))
|
|
35
|
+
.map(([group, items]) => ({ group, items }))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function handleHomeEnd(event: KeyboardEvent) {
|
|
39
|
+
const target = event.target as HTMLInputElement
|
|
40
|
+
const length = event.key === 'Home' ? 0 : target.value.length
|
|
41
|
+
if (event.shiftKey) {
|
|
42
|
+
target.setSelectionRange(
|
|
43
|
+
event.key === 'Home' ? 0 : target.selectionEnd ?? target.value.length,
|
|
44
|
+
event.key === 'Home' ? target.selectionStart ?? 0 : target.value.length
|
|
45
|
+
)
|
|
46
|
+
} else {
|
|
47
|
+
target.setSelectionRange(length, length)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<ComboboxRoot
|
|
54
|
+
:open="true"
|
|
55
|
+
model-value=""
|
|
56
|
+
@update:model-value="modelValue = $event"
|
|
57
|
+
class="menu-root"
|
|
58
|
+
>
|
|
59
|
+
<!-- input -->
|
|
60
|
+
<ComboboxInput
|
|
61
|
+
auto-focus
|
|
62
|
+
design="flat"
|
|
63
|
+
icon-prepend="i--search"
|
|
64
|
+
class="px-$m mb-$s"
|
|
65
|
+
placeholder="Search"
|
|
66
|
+
:as="VuInput"
|
|
67
|
+
@keydown.home.end="handleHomeEnd"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<!-- list -->
|
|
71
|
+
<ComboboxContent class="overflow-y-auto overflow-x-hidden" :dismissable="false">
|
|
72
|
+
<div role="presentation">
|
|
73
|
+
<!-- empty -->
|
|
74
|
+
<ComboboxEmpty v-if="$slots.empty || emptyText">
|
|
75
|
+
<slot name="empty">
|
|
76
|
+
<div class="w-full text-center py-$xs" v-if="!!emptyText">
|
|
77
|
+
{{ emptyText }}
|
|
78
|
+
</div>
|
|
79
|
+
</slot>
|
|
80
|
+
</ComboboxEmpty>
|
|
81
|
+
<!-- group -->
|
|
82
|
+
<ComboboxGroup v-for="grp of groups" class="">
|
|
83
|
+
<ComboboxLabel v-if="grp.group" class="px-$m text-mt-$l text-mb-$m">
|
|
84
|
+
<span class="text-caption fw-bold">{{ grp.group }}</span>
|
|
85
|
+
</ComboboxLabel>
|
|
86
|
+
<!-- command item -->
|
|
87
|
+
<ComboboxItem
|
|
88
|
+
v-for="item of grp.items"
|
|
89
|
+
:value="item.value"
|
|
90
|
+
:icon="item.icon"
|
|
91
|
+
:label="item.label"
|
|
92
|
+
:selected="modelValue === item.value"
|
|
93
|
+
:as="VuMenuItem"
|
|
94
|
+
/>
|
|
95
|
+
</ComboboxGroup>
|
|
96
|
+
<!-- -->
|
|
97
|
+
</div>
|
|
98
|
+
</ComboboxContent>
|
|
99
|
+
</ComboboxRoot>
|
|
100
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
icon?: string
|
|
4
|
+
selected?: boolean
|
|
5
|
+
label?: string
|
|
6
|
+
ariaSelected?: boolean
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<VuButton
|
|
12
|
+
:icon="icon"
|
|
13
|
+
:selected="selected"
|
|
14
|
+
:label="label"
|
|
15
|
+
:class="{
|
|
16
|
+
'scope-grey': !selected,
|
|
17
|
+
}"
|
|
18
|
+
class="menu-item"
|
|
19
|
+
/>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T">
|
|
2
|
+
const props = defineProps<{ items: T[]; maxVisible?: number }>()
|
|
3
|
+
|
|
4
|
+
const hiddenCount = ref(0)
|
|
5
|
+
|
|
6
|
+
const visibleItems = computed(() => {
|
|
7
|
+
return props.items.slice(0, props.items.length - hiddenCount.value)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const hiddenItems = computed(() => {
|
|
11
|
+
return props.items.slice(props.items.length - hiddenCount.value)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const root = ref<{ $el: HTMLDivElement }>()
|
|
15
|
+
|
|
16
|
+
let mObserver: MutationObserver
|
|
17
|
+
let rObserver: ResizeObserver
|
|
18
|
+
|
|
19
|
+
watch(
|
|
20
|
+
() => [props.items],
|
|
21
|
+
() => fit()
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
onMounted(() => {
|
|
25
|
+
mObserver = new MutationObserver(() => {
|
|
26
|
+
if (!busy) {
|
|
27
|
+
fit()
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
rObserver = new ResizeObserver(() => {
|
|
31
|
+
if (!busy) {
|
|
32
|
+
fit()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
if (root.value) {
|
|
36
|
+
rObserver.observe(root.value.$el, {
|
|
37
|
+
box: 'content-box',
|
|
38
|
+
})
|
|
39
|
+
mObserver.observe(root.value.$el, {
|
|
40
|
+
childList: true,
|
|
41
|
+
subtree: true,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
fit()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
onBeforeUnmount(() => {
|
|
48
|
+
if (root.value) {
|
|
49
|
+
mObserver?.disconnect()
|
|
50
|
+
rObserver?.disconnect()
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
let busy = false
|
|
55
|
+
|
|
56
|
+
async function fit(i?: number) {
|
|
57
|
+
if (root.value && props.items.length > 1) {
|
|
58
|
+
busy = true
|
|
59
|
+
hiddenCount.value =
|
|
60
|
+
typeof i === 'number' ? i : Math.max(0, props.items.length - (props.maxVisible ?? 999))
|
|
61
|
+
await nextTick()
|
|
62
|
+
|
|
63
|
+
const rootWidth = root.value.$el.getBoundingClientRect().width
|
|
64
|
+
|
|
65
|
+
let itFits = false
|
|
66
|
+
let additional = 1
|
|
67
|
+
while (!itFits) {
|
|
68
|
+
itFits = true
|
|
69
|
+
const children = root.value.$el.children
|
|
70
|
+
const firstLeft = children[0]?.getBoundingClientRect().left
|
|
71
|
+
for (let j = 0; j < children.length; j++) {
|
|
72
|
+
const { left, width } = children[j].getBoundingClientRect()
|
|
73
|
+
const w = left + width - firstLeft
|
|
74
|
+
if (w >= rootWidth) {
|
|
75
|
+
itFits = false
|
|
76
|
+
hiddenCount.value =
|
|
77
|
+
props.items.length - (j + 1) + (hiddenCount.value > 0 ? additional : 1)
|
|
78
|
+
if (additional === 1) {
|
|
79
|
+
additional = 2
|
|
80
|
+
}
|
|
81
|
+
await nextTick()
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
busy = false
|
|
87
|
+
|
|
88
|
+
// if (!doesItFit()) {
|
|
89
|
+
// busy = true
|
|
90
|
+
// fit(hiddenCount.value + 1)
|
|
91
|
+
// } else {
|
|
92
|
+
// busy = false
|
|
93
|
+
// }
|
|
94
|
+
}
|
|
95
|
+
if (props.items.length < 2) {
|
|
96
|
+
hiddenCount.value = 0
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function doesItFit() {
|
|
101
|
+
if (root.value) {
|
|
102
|
+
const c = root.value.$el
|
|
103
|
+
const { width } = c.getBoundingClientRect()
|
|
104
|
+
const overflowWidth = c.scrollWidth
|
|
105
|
+
return overflowWidth <= width
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<Primitive class="flex" ref="root">
|
|
113
|
+
<slot v-for="item of visibleItems" :item>
|
|
114
|
+
<span>{{ item }}</span>
|
|
115
|
+
</slot>
|
|
116
|
+
<slot v-if="hiddenCount > 0" name="overflow" :count="hiddenCount" :items="hiddenItems">
|
|
117
|
+
<span class="whitespace-nowrap">{{ hiddenCount }} more...</span>
|
|
118
|
+
</slot>
|
|
119
|
+
</Primitive>
|
|
120
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as VuOverflowContainer } from './OverflowContainer.vue'
|