@nuxt/ui 3.0.0-alpha.5 → 3.0.0-alpha.7
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 +4 -2
- package/dist/module.d.mts +19 -5
- package/dist/module.d.ts +19 -5
- package/dist/module.json +2 -1
- package/dist/module.mjs +514 -338
- package/dist/runtime/components/Accordion.vue +13 -5
- package/dist/runtime/components/Alert.vue +2 -2
- package/dist/runtime/components/Breadcrumb.vue +16 -8
- package/dist/runtime/components/Button.vue +4 -2
- package/dist/runtime/components/Carousel.vue +306 -0
- package/dist/runtime/components/Checkbox.vue +3 -6
- package/dist/runtime/components/CommandPalette.vue +27 -18
- package/dist/runtime/components/ContextMenu.vue +33 -9
- package/dist/runtime/components/ContextMenuContent.vue +36 -8
- package/dist/runtime/components/DropdownMenu.vue +33 -9
- package/dist/runtime/components/DropdownMenuContent.vue +38 -8
- package/dist/runtime/components/Form.vue +6 -2
- package/dist/runtime/components/Input.vue +6 -4
- package/dist/runtime/components/InputMenu.vue +40 -24
- package/dist/runtime/components/LinkBase.vue +4 -2
- package/dist/runtime/components/Modal.vue +2 -2
- package/dist/runtime/components/NavigationMenu.vue +80 -43
- package/dist/runtime/components/Pagination.vue +2 -2
- package/dist/runtime/components/RadioGroup.vue +22 -3
- package/dist/runtime/components/Select.vue +22 -10
- package/dist/runtime/components/SelectMenu.vue +43 -31
- package/dist/runtime/components/Slideover.vue +2 -2
- package/dist/runtime/components/Table.vue +232 -0
- package/dist/runtime/components/Tabs.vue +15 -7
- package/dist/runtime/components/Toast.vue +2 -2
- package/dist/runtime/components/Toaster.vue +1 -1
- package/dist/runtime/composables/defineShortcuts.js +1 -1
- package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
- package/dist/runtime/composables/useModal.d.ts +1 -1
- package/dist/runtime/composables/useSlideover.d.ts +1 -1
- package/dist/runtime/index.css +1 -1
- package/dist/runtime/keyframes.css +1 -0
- package/dist/runtime/plugins/colors.js +11 -7
- package/dist/runtime/types/form.d.ts +3 -1
- package/dist/runtime/types/index.d.ts +2 -0
- package/dist/runtime/types/index.js +2 -0
- package/dist/runtime/utils/form.d.ts +7 -1
- package/dist/runtime/utils/form.js +25 -1
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +3 -0
- package/dist/runtime/utils/link.d.ts +1 -1
- package/package.json +30 -14
- package/dist/runtime/types/component.d.ts +0 -17
- package/dist/runtime/types/component.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- eslint-disable vue/block-tag-newline -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { tv } from 'tailwind-variants'
|
|
4
|
-
import type { AccordionRootProps, AccordionRootEmits
|
|
4
|
+
import type { AccordionRootProps, AccordionRootEmits } from 'radix-vue'
|
|
5
5
|
import type { AppConfig } from '@nuxt/schema'
|
|
6
6
|
import _appConfig from '#build/app.config'
|
|
7
7
|
import theme from '#build/ui/accordion'
|
|
@@ -11,7 +11,7 @@ const appConfig = _appConfig as AppConfig & { ui: { accordion: Partial<typeof th
|
|
|
11
11
|
|
|
12
12
|
const accordion = tv({ extend: tv(theme), ...(appConfig.ui?.accordion || {}) })
|
|
13
13
|
|
|
14
|
-
export interface AccordionItem
|
|
14
|
+
export interface AccordionItem {
|
|
15
15
|
label?: string
|
|
16
16
|
icon?: string
|
|
17
17
|
trailingIcon?: string
|
|
@@ -19,6 +19,7 @@ export interface AccordionItem extends Pick<AccordionItemProps, 'disabled'> {
|
|
|
19
19
|
content?: string
|
|
20
20
|
/** A unique value for the accordion item. Defaults to the index. */
|
|
21
21
|
value?: string
|
|
22
|
+
disabled?: boolean
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled'> {
|
|
@@ -33,6 +34,11 @@ export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible
|
|
|
33
34
|
* @defaultValue appConfig.ui.icons.chevronDown
|
|
34
35
|
*/
|
|
35
36
|
trailingIcon?: string
|
|
37
|
+
/**
|
|
38
|
+
* The key used to get the label from the item.
|
|
39
|
+
* @defaultValue 'label'
|
|
40
|
+
*/
|
|
41
|
+
labelKey?: string
|
|
36
42
|
class?: any
|
|
37
43
|
ui?: Partial<typeof accordion.slots>
|
|
38
44
|
}
|
|
@@ -56,10 +62,12 @@ import { computed } from 'vue'
|
|
|
56
62
|
import { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'radix-vue'
|
|
57
63
|
import { reactivePick } from '@vueuse/core'
|
|
58
64
|
import { useAppConfig } from '#imports'
|
|
65
|
+
import { get } from '../utils'
|
|
59
66
|
|
|
60
67
|
const props = withDefaults(defineProps<AccordionProps<T>>(), {
|
|
61
68
|
type: 'single',
|
|
62
|
-
collapsible: true
|
|
69
|
+
collapsible: true,
|
|
70
|
+
labelKey: 'label'
|
|
63
71
|
})
|
|
64
72
|
const emits = defineEmits<AccordionEmits>()
|
|
65
73
|
const slots = defineSlots<AccordionSlots<T>>()
|
|
@@ -88,8 +96,8 @@ const ui = computed(() => accordion({
|
|
|
88
96
|
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
89
97
|
</slot>
|
|
90
98
|
|
|
91
|
-
<span v-if="item.
|
|
92
|
-
<slot :item="item" :index="index" :open="open">{{ item.
|
|
99
|
+
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
|
|
100
|
+
<slot :item="item" :index="index" :open="open">{{ get(item, props.labelKey as string) }}</slot>
|
|
93
101
|
</span>
|
|
94
102
|
|
|
95
103
|
<slot name="trailing" :item="item" :index="index" :open="open">
|
|
@@ -31,7 +31,7 @@ export interface AlertProps {
|
|
|
31
31
|
actions?: ButtonProps[]
|
|
32
32
|
/**
|
|
33
33
|
* Display a close button to dismiss the alert.
|
|
34
|
-
* `{ size: 'md', color: '
|
|
34
|
+
* `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
|
|
35
35
|
* @emits `close`
|
|
36
36
|
* @defaultValue false
|
|
37
37
|
*/
|
|
@@ -117,7 +117,7 @@ const ui = computed(() => alert({
|
|
|
117
117
|
v-if="close"
|
|
118
118
|
:icon="closeIcon || appConfig.ui.icons.close"
|
|
119
119
|
size="md"
|
|
120
|
-
color="
|
|
120
|
+
color="neutral"
|
|
121
121
|
variant="link"
|
|
122
122
|
aria-label="Close"
|
|
123
123
|
v-bind="typeof close === 'object' ? close : undefined"
|
|
@@ -5,7 +5,7 @@ import type { AppConfig } from '@nuxt/schema'
|
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/breadcrumb'
|
|
7
7
|
import type { AvatarProps, LinkProps } from '../types'
|
|
8
|
-
import type { DynamicSlots } from '../types/utils'
|
|
8
|
+
import type { DynamicSlots, PartialString } from '../types/utils'
|
|
9
9
|
|
|
10
10
|
const appConfig = _appConfig as AppConfig & { ui: { breadcrumb: Partial<typeof theme> } }
|
|
11
11
|
|
|
@@ -30,8 +30,13 @@ export interface BreadcrumbProps<T> {
|
|
|
30
30
|
* @defaultValue appConfig.ui.icons.chevronRight
|
|
31
31
|
*/
|
|
32
32
|
separatorIcon?: string
|
|
33
|
+
/**
|
|
34
|
+
* The key used to get the label from the item.
|
|
35
|
+
* @defaultValue 'label'
|
|
36
|
+
*/
|
|
37
|
+
labelKey?: string
|
|
33
38
|
class?: any
|
|
34
|
-
ui?:
|
|
39
|
+
ui?: PartialString<typeof breadcrumb.slots>
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
|
|
@@ -49,13 +54,16 @@ export type BreadcrumbSlots<T extends { slot?: string }> = {
|
|
|
49
54
|
<script setup lang="ts" generic="T extends BreadcrumbItem">
|
|
50
55
|
import { Primitive } from 'radix-vue'
|
|
51
56
|
import { useAppConfig } from '#imports'
|
|
57
|
+
import { get } from '../utils'
|
|
58
|
+
import { pickLinkProps } from '../utils/link'
|
|
52
59
|
import UIcon from './Icon.vue'
|
|
53
60
|
import UAvatar from './Avatar.vue'
|
|
54
61
|
import ULinkBase from './LinkBase.vue'
|
|
55
62
|
import ULink from './Link.vue'
|
|
56
|
-
import { pickLinkProps } from '../utils/link'
|
|
57
63
|
|
|
58
|
-
const props = defineProps<BreadcrumbProps<T>>()
|
|
64
|
+
const props = withDefaults(defineProps<BreadcrumbProps<T>>(), {
|
|
65
|
+
labelKey: 'label'
|
|
66
|
+
})
|
|
59
67
|
const slots = defineSlots<BreadcrumbSlots<T>>()
|
|
60
68
|
|
|
61
69
|
const appConfig = useAppConfig()
|
|
@@ -73,13 +81,13 @@ const ui = breadcrumb()
|
|
|
73
81
|
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
|
|
74
82
|
<slot :name="item.slot || 'item'" :item="item" :index="index">
|
|
75
83
|
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="item" :active="index === items!.length - 1" :index="index">
|
|
76
|
-
<
|
|
77
|
-
<
|
|
84
|
+
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
|
|
85
|
+
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
|
|
78
86
|
</slot>
|
|
79
87
|
|
|
80
|
-
<span v-if="item.
|
|
88
|
+
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
|
81
89
|
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="index === items!.length - 1" :index="index">
|
|
82
|
-
{{ item.
|
|
90
|
+
{{ get(item, props.labelKey as string) }}
|
|
83
91
|
</slot>
|
|
84
92
|
</span>
|
|
85
93
|
|
|
@@ -5,6 +5,7 @@ import _appConfig from '#build/app.config'
|
|
|
5
5
|
import theme from '#build/ui/button'
|
|
6
6
|
import type { LinkProps } from './Link.vue'
|
|
7
7
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
|
8
|
+
import type { AvatarProps } from '../types'
|
|
8
9
|
import type { PartialString } from '../types/utils'
|
|
9
10
|
import { formLoadingInjectionKey } from '../composables/useFormField'
|
|
10
11
|
|
|
@@ -42,10 +43,10 @@ import { type Ref, computed, ref, inject } from 'vue'
|
|
|
42
43
|
import { useForwardProps } from 'radix-vue'
|
|
43
44
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
44
45
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
45
|
-
import UIcon from './Icon.vue'
|
|
46
|
-
import ULink from './Link.vue'
|
|
47
46
|
import { omit } from '../utils'
|
|
48
47
|
import { pickLinkProps } from '../utils/link'
|
|
48
|
+
import UIcon from './Icon.vue'
|
|
49
|
+
import ULink from './Link.vue'
|
|
49
50
|
|
|
50
51
|
const props = defineProps<ButtonProps>()
|
|
51
52
|
const slots = defineSlots<ButtonSlots>()
|
|
@@ -99,6 +100,7 @@ const ui = computed(() => button({
|
|
|
99
100
|
>
|
|
100
101
|
<slot name="leading">
|
|
101
102
|
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
103
|
+
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
|
|
102
104
|
</slot>
|
|
103
105
|
|
|
104
106
|
<slot>
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<!-- eslint-disable vue/block-tag-newline -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
4
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
5
|
+
import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'
|
|
6
|
+
import type { AutoplayOptionsType } from 'embla-carousel-autoplay'
|
|
7
|
+
import type { AutoScrollOptionsType } from 'embla-carousel-auto-scroll'
|
|
8
|
+
import type { AutoHeightOptionsType } from 'embla-carousel-auto-height'
|
|
9
|
+
import type { ClassNamesOptionsType } from 'embla-carousel-class-names'
|
|
10
|
+
import type { FadeOptionsType } from 'embla-carousel-fade'
|
|
11
|
+
import type { WheelGesturesPluginOptions } from 'embla-carousel-wheel-gestures'
|
|
12
|
+
import _appConfig from '#build/app.config'
|
|
13
|
+
import theme from '#build/ui/carousel'
|
|
14
|
+
import type { ButtonProps } from '../types'
|
|
15
|
+
import type { AcceptableValue, PartialString } from '../types/utils'
|
|
16
|
+
|
|
17
|
+
const appConfig = _appConfig as AppConfig & { ui: { carousel: Partial<typeof theme> } }
|
|
18
|
+
|
|
19
|
+
const carousel = tv({ extend: tv(theme), ...(appConfig.ui?.carousel || {}) })
|
|
20
|
+
|
|
21
|
+
type CarouselVariants = VariantProps<typeof carousel>
|
|
22
|
+
|
|
23
|
+
export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
|
|
24
|
+
/**
|
|
25
|
+
* Configure the prev button when arrows are enabled.
|
|
26
|
+
* @defaultValue { size: 'md', color: 'neutral', variant: 'link' }
|
|
27
|
+
*/
|
|
28
|
+
prev?: ButtonProps
|
|
29
|
+
/**
|
|
30
|
+
* The icon displayed in the prev button.
|
|
31
|
+
* @defaultValue appConfig.ui.icons.arrowLeft
|
|
32
|
+
*/
|
|
33
|
+
prevIcon?: string
|
|
34
|
+
/**
|
|
35
|
+
* Configure the next button when arrows are enabled.
|
|
36
|
+
* @defaultValue { size: 'md', color: 'neutral', variant: 'link' }
|
|
37
|
+
*/
|
|
38
|
+
next?: ButtonProps
|
|
39
|
+
/**
|
|
40
|
+
* The icon displayed in the next button.
|
|
41
|
+
* @defaultValue appConfig.ui.icons.arrowRight
|
|
42
|
+
*/
|
|
43
|
+
nextIcon?: string
|
|
44
|
+
/**
|
|
45
|
+
* Display prev and next buttons to scroll the carousel.
|
|
46
|
+
* @defaultValue false
|
|
47
|
+
*/
|
|
48
|
+
arrows?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Display dots to scroll to a specific slide.
|
|
51
|
+
* @defaultValue false
|
|
52
|
+
*/
|
|
53
|
+
dots?: boolean
|
|
54
|
+
orientation?: CarouselVariants['orientation']
|
|
55
|
+
items?: T[]
|
|
56
|
+
/**
|
|
57
|
+
* Enable Autoplay plugin
|
|
58
|
+
* @link https://www.embla-carousel.com/plugins/autoplay/
|
|
59
|
+
*/
|
|
60
|
+
autoplay?: boolean | AutoplayOptionsType
|
|
61
|
+
/**
|
|
62
|
+
* Enable Auto Scroll plugin
|
|
63
|
+
* @link https://www.embla-carousel.com/plugins/auto-scroll/
|
|
64
|
+
*/
|
|
65
|
+
autoScroll?: boolean | AutoScrollOptionsType
|
|
66
|
+
/**
|
|
67
|
+
* Enable Auto Height plugin
|
|
68
|
+
* @link https://www.embla-carousel.com/plugins/auto-height/
|
|
69
|
+
*/
|
|
70
|
+
autoHeight?: boolean | AutoHeightOptionsType
|
|
71
|
+
/**
|
|
72
|
+
* Enable Class Names plugin
|
|
73
|
+
* @link https://www.embla-carousel.com/plugins/class-names/
|
|
74
|
+
*/
|
|
75
|
+
classNames?: boolean | ClassNamesOptionsType
|
|
76
|
+
/**
|
|
77
|
+
* Enable Fade plugin
|
|
78
|
+
* @link https://www.embla-carousel.com/plugins/fade/
|
|
79
|
+
*/
|
|
80
|
+
fade?: boolean | FadeOptionsType
|
|
81
|
+
/**
|
|
82
|
+
* Enable Wheel Gestures plugin
|
|
83
|
+
* @link https://www.embla-carousel.com/plugins/wheel-gestures/
|
|
84
|
+
*/
|
|
85
|
+
wheelGestures?: boolean | WheelGesturesPluginOptions
|
|
86
|
+
class?: any
|
|
87
|
+
ui?: PartialString<typeof carousel.slots>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type CarouselSlots<T> = {
|
|
91
|
+
default(props: { item: T, index: number }): any
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<script setup lang="ts" generic="T extends AcceptableValue">
|
|
97
|
+
import { computed, ref, watch, onMounted } from 'vue'
|
|
98
|
+
import useEmblaCarousel from 'embla-carousel-vue'
|
|
99
|
+
import { useForwardProps } from 'radix-vue'
|
|
100
|
+
import { reactivePick, computedAsync } from '@vueuse/core'
|
|
101
|
+
import { useAppConfig } from '#imports'
|
|
102
|
+
import UButton from './Button.vue'
|
|
103
|
+
|
|
104
|
+
const props = withDefaults(defineProps<CarouselProps<T>>(), {
|
|
105
|
+
orientation: 'horizontal',
|
|
106
|
+
arrows: false,
|
|
107
|
+
dots: false,
|
|
108
|
+
// Embla Options
|
|
109
|
+
active: true,
|
|
110
|
+
align: 'center',
|
|
111
|
+
breakpoints: () => ({}),
|
|
112
|
+
containScroll: 'trimSnaps',
|
|
113
|
+
dragFree: false,
|
|
114
|
+
dragThreshold: 10,
|
|
115
|
+
duration: 25,
|
|
116
|
+
inViewThreshold: 0,
|
|
117
|
+
loop: false,
|
|
118
|
+
skipSnaps: false,
|
|
119
|
+
slidesToScroll: 1,
|
|
120
|
+
startIndex: 0,
|
|
121
|
+
watchDrag: true,
|
|
122
|
+
watchResize: true,
|
|
123
|
+
watchSlides: true,
|
|
124
|
+
watchFocus: true,
|
|
125
|
+
// Embla Plugins
|
|
126
|
+
autoplay: false,
|
|
127
|
+
autoScroll: false,
|
|
128
|
+
autoHeight: false,
|
|
129
|
+
classNames: false,
|
|
130
|
+
fade: false,
|
|
131
|
+
wheelGestures: false
|
|
132
|
+
})
|
|
133
|
+
defineSlots<CarouselSlots<T>>()
|
|
134
|
+
|
|
135
|
+
const appConfig = useAppConfig()
|
|
136
|
+
const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
|
|
137
|
+
|
|
138
|
+
const ui = computed(() => carousel({
|
|
139
|
+
orientation: props.orientation
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
const options = computed<EmblaOptionsType>(() => ({
|
|
143
|
+
...(props.fade ? { align: 'center', containScroll: false } : {}),
|
|
144
|
+
...rootProps.value,
|
|
145
|
+
axis: props.orientation === 'horizontal' ? 'x' : 'y',
|
|
146
|
+
// TODO: Get from ConfigProvider
|
|
147
|
+
direction: 'ltr'
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
const plugins = computedAsync<EmblaPluginType[]>(async () => {
|
|
151
|
+
const plugins = []
|
|
152
|
+
|
|
153
|
+
if (props.autoplay) {
|
|
154
|
+
const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default)
|
|
155
|
+
plugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (props.autoScroll) {
|
|
159
|
+
const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default)
|
|
160
|
+
plugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (props.autoHeight) {
|
|
164
|
+
const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default)
|
|
165
|
+
plugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (props.classNames) {
|
|
169
|
+
const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default)
|
|
170
|
+
plugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (props.fade) {
|
|
174
|
+
const FadePlugin = await import('embla-carousel-fade').then(r => r.default)
|
|
175
|
+
plugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (props.wheelGestures) {
|
|
179
|
+
const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures')
|
|
180
|
+
plugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return plugins
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value)
|
|
187
|
+
|
|
188
|
+
watch([options, plugins], () => {
|
|
189
|
+
emblaApi.value?.reInit(options.value, plugins.value)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
function scrollPrev() {
|
|
193
|
+
emblaApi.value?.scrollPrev()
|
|
194
|
+
}
|
|
195
|
+
function scrollNext() {
|
|
196
|
+
emblaApi.value?.scrollNext()
|
|
197
|
+
}
|
|
198
|
+
function scrollTo(index: number) {
|
|
199
|
+
emblaApi.value?.scrollTo(index)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
203
|
+
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
|
|
204
|
+
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
|
|
205
|
+
|
|
206
|
+
if (event.key === prevKey) {
|
|
207
|
+
event.preventDefault()
|
|
208
|
+
scrollPrev()
|
|
209
|
+
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (event.key === nextKey) {
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
scrollNext()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const canScrollNext = ref(false)
|
|
220
|
+
const canScrollPrev = ref(false)
|
|
221
|
+
const selectedIndex = ref<number>(0)
|
|
222
|
+
const scrollSnaps = ref<number[]>([])
|
|
223
|
+
|
|
224
|
+
function onInit(api: EmblaCarouselType) {
|
|
225
|
+
scrollSnaps.value = api?.scrollSnapList() || []
|
|
226
|
+
}
|
|
227
|
+
function onSelect(api: EmblaCarouselType) {
|
|
228
|
+
canScrollNext.value = api?.canScrollNext() || false
|
|
229
|
+
canScrollPrev.value = api?.canScrollPrev() || false
|
|
230
|
+
selectedIndex.value = api?.selectedScrollSnap() || 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
onMounted(() => {
|
|
234
|
+
if (!emblaApi.value) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
emblaApi.value?.on('init', onInit)
|
|
239
|
+
emblaApi.value?.on('init', onSelect)
|
|
240
|
+
emblaApi.value?.on('reInit', onInit)
|
|
241
|
+
emblaApi.value?.on('reInit', onSelect)
|
|
242
|
+
emblaApi.value?.on('select', onSelect)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
defineExpose({
|
|
246
|
+
emblaRef,
|
|
247
|
+
emblaApi
|
|
248
|
+
})
|
|
249
|
+
</script>
|
|
250
|
+
|
|
251
|
+
<template>
|
|
252
|
+
<div
|
|
253
|
+
role="region"
|
|
254
|
+
aria-roledescription="carousel"
|
|
255
|
+
tabindex="0"
|
|
256
|
+
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
|
257
|
+
@keydown="onKeyDown"
|
|
258
|
+
>
|
|
259
|
+
<div ref="emblaRef" :class="ui.viewport({ class: props.ui?.viewport })">
|
|
260
|
+
<div :class="ui.container({ class: props.ui?.container })">
|
|
261
|
+
<div
|
|
262
|
+
v-for="(item, index) in items"
|
|
263
|
+
:key="index"
|
|
264
|
+
role="group"
|
|
265
|
+
aria-roledescription="slide"
|
|
266
|
+
:class="ui.item({ class: props.ui?.item })"
|
|
267
|
+
>
|
|
268
|
+
<slot :item="item" :index="index" />
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div v-if="arrows || dots" :class="ui.controls({ class: props.ui?.controls })">
|
|
274
|
+
<div v-if="arrows" :class="ui.arrows({ class: props.ui?.arrows })">
|
|
275
|
+
<UButton
|
|
276
|
+
:disabled="!canScrollPrev"
|
|
277
|
+
:icon="prevIcon || appConfig.ui.icons.arrowLeft"
|
|
278
|
+
size="md"
|
|
279
|
+
color="neutral"
|
|
280
|
+
variant="outline"
|
|
281
|
+
aria-label="Prev"
|
|
282
|
+
v-bind="typeof prev === 'object' ? prev : undefined"
|
|
283
|
+
:class="ui.prev({ class: props.ui?.prev })"
|
|
284
|
+
@click="scrollPrev"
|
|
285
|
+
/>
|
|
286
|
+
<UButton
|
|
287
|
+
:disabled="!canScrollNext"
|
|
288
|
+
:icon="nextIcon || appConfig.ui.icons.arrowRight"
|
|
289
|
+
size="md"
|
|
290
|
+
color="neutral"
|
|
291
|
+
variant="outline"
|
|
292
|
+
aria-label="Next"
|
|
293
|
+
v-bind="typeof next === 'object' ? next : undefined"
|
|
294
|
+
:class="ui.next({ class: props.ui?.next })"
|
|
295
|
+
@click="scrollNext"
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
|
|
300
|
+
<template v-for="(_, index) in scrollSnaps" :key="index">
|
|
301
|
+
<button :class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })" @click="scrollTo(index)" />
|
|
302
|
+
</template>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</template>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { InputHTMLAttributes } from 'vue'
|
|
3
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
4
3
|
import type { CheckboxRootProps } from 'radix-vue'
|
|
5
4
|
import type { AppConfig } from '@nuxt/schema'
|
|
@@ -22,7 +21,7 @@ export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'req
|
|
|
22
21
|
* @defaultValue appConfig.ui.icons.check
|
|
23
22
|
*/
|
|
24
23
|
icon?: string
|
|
25
|
-
indeterminate?:
|
|
24
|
+
indeterminate?: boolean
|
|
26
25
|
/**
|
|
27
26
|
* The icon displayed when the checkbox is indeterminate.
|
|
28
27
|
* @defaultValue appConfig.ui.icons.minus
|
|
@@ -64,11 +63,9 @@ const appConfig = useAppConfig()
|
|
|
64
63
|
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<CheckboxProps>(props)
|
|
65
64
|
const id = _id.value ?? useId()
|
|
66
65
|
|
|
67
|
-
const indeterminate = computed(() => (modelValue.value === undefined && props.indeterminate))
|
|
68
|
-
|
|
69
66
|
const checked = computed({
|
|
70
67
|
get() {
|
|
71
|
-
return indeterminate
|
|
68
|
+
return props.indeterminate ? 'indeterminate' : modelValue.value
|
|
72
69
|
},
|
|
73
70
|
set(value) {
|
|
74
71
|
modelValue.value = value === 'indeterminate' ? undefined : value
|
|
@@ -80,7 +77,7 @@ const ui = computed(() => checkbox({
|
|
|
80
77
|
color: color.value,
|
|
81
78
|
required: props.required,
|
|
82
79
|
disabled: disabled.value,
|
|
83
|
-
checked: (modelValue.value ?? props.defaultValue) || indeterminate
|
|
80
|
+
checked: (modelValue.value ?? props.defaultValue) || props.indeterminate
|
|
84
81
|
}))
|
|
85
82
|
|
|
86
83
|
function onUpdate(value: any) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- eslint-disable vue/block-tag-newline -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { tv } from 'tailwind-variants'
|
|
4
|
-
import type { ComboboxRootProps, ComboboxRootEmits
|
|
4
|
+
import type { ComboboxRootProps, ComboboxRootEmits } from 'radix-vue'
|
|
5
5
|
import type { FuseResult } from 'fuse.js'
|
|
6
6
|
import type { AppConfig } from '@nuxt/schema'
|
|
7
7
|
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
|
@@ -15,7 +15,7 @@ const appConfig = _appConfig as AppConfig & { ui: { commandPalette: Partial<type
|
|
|
15
15
|
|
|
16
16
|
const commandPalette = tv({ extend: tv(theme), ...(appConfig.ui?.commandPalette || {}) })
|
|
17
17
|
|
|
18
|
-
export interface CommandPaletteItem
|
|
18
|
+
export interface CommandPaletteItem {
|
|
19
19
|
prefix?: string
|
|
20
20
|
label?: string
|
|
21
21
|
suffix?: string
|
|
@@ -23,8 +23,10 @@ export interface CommandPaletteItem extends Pick<ComboboxItemProps, 'disabled'>
|
|
|
23
23
|
avatar?: AvatarProps
|
|
24
24
|
chip?: ChipProps
|
|
25
25
|
kbds?: KbdProps['value'][] | KbdProps[]
|
|
26
|
+
loading?: boolean
|
|
27
|
+
disabled?: boolean
|
|
26
28
|
slot?: string
|
|
27
|
-
|
|
29
|
+
onSelect?(e?: Event): void
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface CommandPaletteGroup<T> {
|
|
@@ -67,7 +69,7 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'mult
|
|
|
67
69
|
placeholder?: InputProps['placeholder']
|
|
68
70
|
/**
|
|
69
71
|
* Display a close button in the input (useful when inside a Modal for example).
|
|
70
|
-
* `{ size: 'md', color: '
|
|
72
|
+
* `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
|
|
71
73
|
* @defaultValue false
|
|
72
74
|
*/
|
|
73
75
|
close?: ButtonProps | boolean
|
|
@@ -90,6 +92,11 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'mult
|
|
|
90
92
|
}
|
|
91
93
|
*/
|
|
92
94
|
fuse?: UseFuseOptions<T>
|
|
95
|
+
/**
|
|
96
|
+
* The key used to get the label from the item.
|
|
97
|
+
* @defaultValue 'label'
|
|
98
|
+
*/
|
|
99
|
+
labelKey?: string
|
|
93
100
|
class?: any
|
|
94
101
|
ui?: PartialString<typeof commandPalette.slots>
|
|
95
102
|
}
|
|
@@ -116,17 +123,18 @@ import { defu } from 'defu'
|
|
|
116
123
|
import { reactivePick } from '@vueuse/core'
|
|
117
124
|
import { useFuse } from '@vueuse/integrations/useFuse'
|
|
118
125
|
import { useAppConfig } from '#imports'
|
|
126
|
+
import { omit, get } from '../utils'
|
|
127
|
+
import { highlight } from '../utils/fuse'
|
|
119
128
|
import UIcon from './Icon.vue'
|
|
120
129
|
import UAvatar from './Avatar.vue'
|
|
121
130
|
import UChip from './Chip.vue'
|
|
122
131
|
import UKbd from './Kbd.vue'
|
|
123
132
|
import UInput from './Input.vue'
|
|
124
|
-
import { omit } from '../utils'
|
|
125
|
-
import { highlight } from '../utils/fuse'
|
|
126
133
|
|
|
127
134
|
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
|
|
128
135
|
modelValue: '',
|
|
129
|
-
placeholder: 'Type a command or search...'
|
|
136
|
+
placeholder: 'Type a command or search...',
|
|
137
|
+
labelKey: 'label'
|
|
130
138
|
})
|
|
131
139
|
const emits = defineEmits<CommandPaletteEmits<T>>()
|
|
132
140
|
const slots = defineSlots<CommandPaletteSlots<G, T>>()
|
|
@@ -144,7 +152,7 @@ const fuse = computed(() => defu({}, props.fuse, {
|
|
|
144
152
|
fuseOptions: {
|
|
145
153
|
ignoreLocation: true,
|
|
146
154
|
threshold: 0.1,
|
|
147
|
-
keys: [
|
|
155
|
+
keys: [props.labelKey, 'suffix']
|
|
148
156
|
},
|
|
149
157
|
resultLimit: 12,
|
|
150
158
|
matchAllWhenSearchEmpty: true
|
|
@@ -175,8 +183,8 @@ function getGroupWithItems(group: G, items: (T & { matches?: FuseResult<T>['matc
|
|
|
175
183
|
items: items.slice(0, fuse.value.resultLimit).map((item) => {
|
|
176
184
|
return {
|
|
177
185
|
...item,
|
|
178
|
-
labelHtml: highlight<T>(item, searchTerm.value,
|
|
179
|
-
suffixHtml: highlight<T>(item, searchTerm.value, undefined, [
|
|
186
|
+
labelHtml: highlight<T>(item, searchTerm.value, props.labelKey),
|
|
187
|
+
suffixHtml: highlight<T>(item, searchTerm.value, undefined, [props.labelKey])
|
|
180
188
|
}
|
|
181
189
|
})
|
|
182
190
|
}
|
|
@@ -233,7 +241,7 @@ const groups = computed(() => {
|
|
|
233
241
|
v-if="close"
|
|
234
242
|
:icon="closeIcon || appConfig.ui.icons.close"
|
|
235
243
|
size="md"
|
|
236
|
-
color="
|
|
244
|
+
color="neutral"
|
|
237
245
|
variant="ghost"
|
|
238
246
|
aria-label="Close"
|
|
239
247
|
v-bind="typeof close === 'object' ? close : undefined"
|
|
@@ -255,22 +263,23 @@ const groups = computed(() => {
|
|
|
255
263
|
|
|
256
264
|
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
|
257
265
|
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
258
|
-
<ComboboxLabel v-if="group.
|
|
259
|
-
{{ group.
|
|
266
|
+
<ComboboxLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
|
|
267
|
+
{{ get(group, props.labelKey as string) }}
|
|
260
268
|
</ComboboxLabel>
|
|
261
269
|
|
|
262
270
|
<ComboboxItem
|
|
263
271
|
v-for="(item, index) in group.items"
|
|
264
272
|
:key="`group-${groupIndex}-${index}`"
|
|
265
|
-
:value="omit(item, ['matches' as any, 'group' as any, '
|
|
273
|
+
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
|
|
266
274
|
:disabled="item.disabled"
|
|
267
275
|
:class="ui.item({ class: props.ui?.item })"
|
|
268
|
-
@select="item.
|
|
276
|
+
@select="item.onSelect"
|
|
269
277
|
>
|
|
270
278
|
<slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
|
|
271
279
|
<slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :item="item" :index="index">
|
|
272
|
-
<
|
|
280
|
+
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
|
|
273
281
|
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
|
282
|
+
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
274
283
|
<UChip
|
|
275
284
|
v-else-if="item.chip"
|
|
276
285
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
|
@@ -281,11 +290,11 @@ const groups = computed(() => {
|
|
|
281
290
|
/>
|
|
282
291
|
</slot>
|
|
283
292
|
|
|
284
|
-
<span v-if="item.
|
|
293
|
+
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
285
294
|
<slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
|
|
286
295
|
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
|
|
287
296
|
|
|
288
|
-
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase })" v-html="item.labelHtml || item.
|
|
297
|
+
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
|
|
289
298
|
|
|
290
299
|
<span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix })" v-html="item.suffixHtml || item.suffix" />
|
|
291
300
|
</slot>
|