@polymarbot/nuxt-layer-shadcn-ui 0.4.2 → 0.5.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/app/components/ui/Dropdown/ItemContent.vue +23 -0
- package/app/components/ui/Dropdown/ItemIcon.vue +41 -0
- package/app/components/ui/Dropdown/MenuItems.vue +160 -0
- package/app/components/ui/Dropdown/SlotRenderer.vue +18 -0
- package/app/components/ui/Dropdown/en.json +3 -0
- package/app/components/ui/Dropdown/index.stories.ts +91 -1
- package/app/components/ui/Dropdown/index.vue +43 -159
- package/app/components/ui/Dropdown/types.ts +47 -5
- package/app/components/ui/Progress/index.stories.ts +87 -0
- package/app/components/ui/Progress/index.vue +10 -0
- package/app/components/ui/Progress/types.ts +3 -0
- package/app/components/ui/Surface/index.stories.ts +3 -0
- package/i18n/messages/ar.json +3 -0
- package/i18n/messages/de.json +3 -0
- package/i18n/messages/en.json +3 -0
- package/i18n/messages/es.json +3 -0
- package/i18n/messages/fr.json +3 -0
- package/i18n/messages/hi.json +3 -0
- package/i18n/messages/id.json +3 -0
- package/i18n/messages/it.json +3 -0
- package/i18n/messages/ja.json +3 -0
- package/i18n/messages/ko.json +3 -0
- package/i18n/messages/nl.json +3 -0
- package/i18n/messages/pl.json +3 -0
- package/i18n/messages/pt.json +3 -0
- package/i18n/messages/ru.json +3 -0
- package/i18n/messages/th.json +3 -0
- package/i18n/messages/tr.json +3 -0
- package/i18n/messages/vi.json +3 -0
- package/i18n/messages/zh-CN.json +3 -0
- package/i18n/messages/zh-TW.json +3 -0
- package/nuxt.config.ts +9 -3
- package/package.json +2 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import ItemIcon from './ItemIcon.vue'
|
|
3
|
+
import type { DropdownActionItem } from './types'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
item: DropdownActionItem
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<ItemIcon
|
|
12
|
+
:icon="item.icon"
|
|
13
|
+
:iconColor="item.iconColor"
|
|
14
|
+
/>
|
|
15
|
+
<span class="flex-1">
|
|
16
|
+
{{ item.label }}
|
|
17
|
+
</span>
|
|
18
|
+
<Icon
|
|
19
|
+
v-if="item.active"
|
|
20
|
+
name="check"
|
|
21
|
+
class="size-4"
|
|
22
|
+
/>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { cva } from 'class-variance-authority'
|
|
3
|
+
import type { DropdownActionItem } from './types'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
/** Icon name (lucide kebab-case) or a Vue component. */
|
|
7
|
+
icon: DropdownActionItem['icon']
|
|
8
|
+
/** Override icon color independently of the surrounding item color. */
|
|
9
|
+
iconColor?: DropdownActionItem['iconColor']
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const iconColorVariants = cva('', {
|
|
13
|
+
variants: {
|
|
14
|
+
color: {
|
|
15
|
+
default: '',
|
|
16
|
+
primary: 'text-primary',
|
|
17
|
+
success: 'text-success',
|
|
18
|
+
info: 'text-info',
|
|
19
|
+
help: 'text-help',
|
|
20
|
+
warn: 'text-warn',
|
|
21
|
+
danger: 'text-danger',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: { color: 'default' },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const colorClass = computed(() => iconColorVariants({ color: props.iconColor }))
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<Icon
|
|
32
|
+
v-if="typeof icon === 'string'"
|
|
33
|
+
:name="icon"
|
|
34
|
+
:class="colorClass"
|
|
35
|
+
/>
|
|
36
|
+
<component
|
|
37
|
+
:is="icon"
|
|
38
|
+
v-else-if="icon"
|
|
39
|
+
:class="cn('size-4', colorClass)"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenuItem,
|
|
4
|
+
DropdownMenuLabel,
|
|
5
|
+
DropdownMenuSeparator,
|
|
6
|
+
DropdownMenuSub,
|
|
7
|
+
DropdownMenuSubContent,
|
|
8
|
+
DropdownMenuSubTrigger,
|
|
9
|
+
} from '../../shadcn/dropdown-menu'
|
|
10
|
+
import { cva } from 'class-variance-authority'
|
|
11
|
+
import ItemContent from './ItemContent.vue'
|
|
12
|
+
import SlotRenderer from './SlotRenderer.vue'
|
|
13
|
+
import type {
|
|
14
|
+
DropdownActionItem,
|
|
15
|
+
DropdownCustomActionItem,
|
|
16
|
+
DropdownItem,
|
|
17
|
+
} from './types'
|
|
18
|
+
|
|
19
|
+
const actionColorVariants = cva('', {
|
|
20
|
+
variants: {
|
|
21
|
+
color: {
|
|
22
|
+
default: '',
|
|
23
|
+
primary: `
|
|
24
|
+
text-primary
|
|
25
|
+
focus:bg-primary/10 focus:text-primary
|
|
26
|
+
data-[state=open]:bg-primary/10 data-[state=open]:text-primary
|
|
27
|
+
`,
|
|
28
|
+
success: `
|
|
29
|
+
text-success
|
|
30
|
+
focus:bg-success/10 focus:text-success
|
|
31
|
+
data-[state=open]:bg-success/10 data-[state=open]:text-success
|
|
32
|
+
`,
|
|
33
|
+
info: `
|
|
34
|
+
text-info
|
|
35
|
+
focus:bg-info/10 focus:text-info
|
|
36
|
+
data-[state=open]:bg-info/10 data-[state=open]:text-info
|
|
37
|
+
`,
|
|
38
|
+
help: `
|
|
39
|
+
text-help
|
|
40
|
+
focus:bg-help/10 focus:text-help
|
|
41
|
+
data-[state=open]:bg-help/10 data-[state=open]:text-help
|
|
42
|
+
`,
|
|
43
|
+
warn: `
|
|
44
|
+
text-warn
|
|
45
|
+
focus:bg-warn/10 focus:text-warn
|
|
46
|
+
data-[state=open]:bg-warn/10 data-[state=open]:text-warn
|
|
47
|
+
`,
|
|
48
|
+
danger: `
|
|
49
|
+
text-danger
|
|
50
|
+
focus:bg-danger/10 focus:text-danger
|
|
51
|
+
data-[state=open]:bg-danger/10 data-[state=open]:text-danger
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultVariants: { color: 'default' },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
defineProps<{
|
|
59
|
+
menus: DropdownItem[]
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
const ctx = inject(dropdownContextKey)
|
|
63
|
+
|
|
64
|
+
const handleItemAction = (
|
|
65
|
+
item: DropdownActionItem | DropdownCustomActionItem,
|
|
66
|
+
event?: Event,
|
|
67
|
+
) => {
|
|
68
|
+
if (item.disabled) {
|
|
69
|
+
event?.preventDefault()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
item.command?.()
|
|
73
|
+
ctx?.hide()
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<template
|
|
79
|
+
v-for="(menu, index) in menus"
|
|
80
|
+
:key="index"
|
|
81
|
+
>
|
|
82
|
+
<!-- Built-in: separator -->
|
|
83
|
+
<DropdownMenuSeparator v-if="menu.type === 'separator'" />
|
|
84
|
+
<!-- Built-in: group label -->
|
|
85
|
+
<DropdownMenuLabel
|
|
86
|
+
v-else-if="menu.type === 'label'"
|
|
87
|
+
class="text-xs font-normal text-muted-foreground"
|
|
88
|
+
>
|
|
89
|
+
{{ menu.label }}
|
|
90
|
+
</DropdownMenuLabel>
|
|
91
|
+
<!-- Custom label: content via named slot -->
|
|
92
|
+
<DropdownMenuLabel
|
|
93
|
+
v-else-if="menu.type === 'custom-label'"
|
|
94
|
+
:class="cn('p-0 font-normal', menu.class)"
|
|
95
|
+
>
|
|
96
|
+
<SlotRenderer
|
|
97
|
+
:slotName="menu.slot"
|
|
98
|
+
:item="menu"
|
|
99
|
+
/>
|
|
100
|
+
</DropdownMenuLabel>
|
|
101
|
+
<!-- Custom action: content via named slot -->
|
|
102
|
+
<DropdownMenuItem
|
|
103
|
+
v-else-if="menu.type === 'custom-action'"
|
|
104
|
+
:disabled="menu.disabled"
|
|
105
|
+
:class="cn(actionColorVariants({ color: menu.color }), menu.class)"
|
|
106
|
+
@click="handleItemAction(menu, $event)"
|
|
107
|
+
>
|
|
108
|
+
<SlotRenderer
|
|
109
|
+
:slotName="menu.slot"
|
|
110
|
+
:item="menu"
|
|
111
|
+
/>
|
|
112
|
+
<Icon
|
|
113
|
+
v-if="menu.active"
|
|
114
|
+
name="check"
|
|
115
|
+
class="size-4 ml-auto"
|
|
116
|
+
/>
|
|
117
|
+
</DropdownMenuItem>
|
|
118
|
+
<!-- Action with sub-menu -->
|
|
119
|
+
<DropdownMenuSub v-else-if="menu.subMenus?.length">
|
|
120
|
+
<DropdownMenuSubTrigger
|
|
121
|
+
:disabled="menu.disabled"
|
|
122
|
+
:class="cn(actionColorVariants({ color: menu.color }), menu.class)"
|
|
123
|
+
>
|
|
124
|
+
<ItemContent :item="menu" />
|
|
125
|
+
</DropdownMenuSubTrigger>
|
|
126
|
+
<DropdownMenuSubContent :style="ctx?.contentStyle.value">
|
|
127
|
+
<MenuItems :menus="menu.subMenus" />
|
|
128
|
+
</DropdownMenuSubContent>
|
|
129
|
+
</DropdownMenuSub>
|
|
130
|
+
<!-- Built-in: action (default). When `href` is set, asChild merges
|
|
131
|
+
our props (incl. @click) onto the WebLink, so the click handler
|
|
132
|
+
only needs to live here. -->
|
|
133
|
+
<DropdownMenuItem
|
|
134
|
+
v-else
|
|
135
|
+
:disabled="menu.disabled"
|
|
136
|
+
:asChild="!!menu.href"
|
|
137
|
+
:class="cn(actionColorVariants({ color: menu.color }), menu.class)"
|
|
138
|
+
@click="handleItemAction(menu, $event)"
|
|
139
|
+
>
|
|
140
|
+
<WebLink
|
|
141
|
+
v-if="menu.href"
|
|
142
|
+
unstyled
|
|
143
|
+
:href="menu.href"
|
|
144
|
+
:target="menu.target"
|
|
145
|
+
class="gap-2 flex w-full items-center"
|
|
146
|
+
>
|
|
147
|
+
<ItemContent :item="menu" />
|
|
148
|
+
<Icon
|
|
149
|
+
v-if="isUrl(menu.href)"
|
|
150
|
+
name="external-link"
|
|
151
|
+
class="size-3.5 text-muted-foreground"
|
|
152
|
+
/>
|
|
153
|
+
</WebLink>
|
|
154
|
+
<ItemContent
|
|
155
|
+
v-else
|
|
156
|
+
:item="menu"
|
|
157
|
+
/>
|
|
158
|
+
</DropdownMenuItem>
|
|
159
|
+
</template>
|
|
160
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
/** Name of the slot on the root Dropdown to render. */
|
|
4
|
+
slotName: string
|
|
5
|
+
/** Forwarded to the slot scope as `{ item }`. */
|
|
6
|
+
item: object
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const ctx = inject(dropdownContextKey)
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<component
|
|
14
|
+
:is="ctx.slots[slotName]"
|
|
15
|
+
v-if="ctx?.slots[slotName]"
|
|
16
|
+
:item="item"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
@@ -23,7 +23,7 @@ const accountMenus: DropdownItem[] = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
const linkMenus: DropdownItem[] = [
|
|
26
|
-
{ label: 'Documentation', icon: 'book-open', href: 'https://example.com/docs', target: '_blank' },
|
|
26
|
+
{ label: 'Documentation', icon: 'book-open', href: 'https://example.com/docs', target: '_blank', active: true },
|
|
27
27
|
{ label: 'Support', icon: 'life-buoy', href: 'https://example.com/support', target: '_blank' },
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -56,6 +56,53 @@ const customMenus: DropdownItem[] = [
|
|
|
56
56
|
},
|
|
57
57
|
]
|
|
58
58
|
|
|
59
|
+
const iconColorMenus: DropdownItem[] = [
|
|
60
|
+
{ label: 'Default item', icon: 'circle' },
|
|
61
|
+
{ label: 'Primary icon only', icon: 'star', iconColor: 'primary' },
|
|
62
|
+
{ label: 'Success icon only', icon: 'circle-check', iconColor: 'success' },
|
|
63
|
+
{ label: 'Warn icon only', icon: 'triangle-alert', iconColor: 'warn' },
|
|
64
|
+
{ label: 'Danger icon only', icon: 'shield-alert', iconColor: 'danger' },
|
|
65
|
+
{ type: 'separator' },
|
|
66
|
+
{ label: 'Both danger', icon: 'trash-2', color: 'danger', iconColor: 'danger' },
|
|
67
|
+
{ label: 'Danger label, info icon', icon: 'info', color: 'danger', iconColor: 'info' },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const subMenus: DropdownItem[] = [
|
|
71
|
+
{ label: 'New File', icon: 'file-plus' },
|
|
72
|
+
{ label: 'New Folder', icon: 'folder-plus' },
|
|
73
|
+
{ type: 'separator' },
|
|
74
|
+
{
|
|
75
|
+
label: 'Share',
|
|
76
|
+
icon: 'share-2',
|
|
77
|
+
subMenus: [
|
|
78
|
+
{ label: 'Email link', icon: 'mail' },
|
|
79
|
+
{ label: 'Copy link', icon: 'link' },
|
|
80
|
+
{ type: 'separator' },
|
|
81
|
+
{
|
|
82
|
+
label: 'Social',
|
|
83
|
+
icon: 'globe',
|
|
84
|
+
subMenus: [
|
|
85
|
+
{ label: 'Twitter', icon: 'twitter' },
|
|
86
|
+
{ label: 'Facebook', icon: 'facebook' },
|
|
87
|
+
{ label: 'LinkedIn', icon: 'linkedin' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: 'Move to',
|
|
94
|
+
icon: 'folder-symlink',
|
|
95
|
+
active: true,
|
|
96
|
+
subMenus: [
|
|
97
|
+
{ label: 'Documents', icon: 'folder' },
|
|
98
|
+
{ label: 'Downloads', icon: 'folder', active: true },
|
|
99
|
+
{ label: 'Trash', icon: 'trash-2', color: 'danger', iconColor: 'danger' },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{ type: 'separator' },
|
|
103
|
+
{ label: 'Delete', icon: 'trash-2', color: 'danger' },
|
|
104
|
+
]
|
|
105
|
+
|
|
59
106
|
const meta = {
|
|
60
107
|
title: 'UI/Dropdown',
|
|
61
108
|
component: Dropdown,
|
|
@@ -65,6 +112,7 @@ const meta = {
|
|
|
65
112
|
side: { control: 'select', options: sides },
|
|
66
113
|
align: { control: 'select', options: aligns },
|
|
67
114
|
sideOffset: { control: 'number' },
|
|
115
|
+
minWidth: { control: 'text' },
|
|
68
116
|
},
|
|
69
117
|
args: {
|
|
70
118
|
menus: basicMenus,
|
|
@@ -72,6 +120,7 @@ const meta = {
|
|
|
72
120
|
side: undefined,
|
|
73
121
|
align: undefined,
|
|
74
122
|
sideOffset: undefined,
|
|
123
|
+
minWidth: undefined,
|
|
75
124
|
},
|
|
76
125
|
render: args => ({
|
|
77
126
|
components: { Dropdown, Button },
|
|
@@ -123,6 +172,47 @@ export const WithGroups: Story = {
|
|
|
123
172
|
},
|
|
124
173
|
}
|
|
125
174
|
|
|
175
|
+
export const WithIconColor: Story = {
|
|
176
|
+
parameters: noControls,
|
|
177
|
+
args: {
|
|
178
|
+
menus: iconColorMenus,
|
|
179
|
+
trigger: 'click',
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const WithSubMenus: Story = {
|
|
184
|
+
parameters: noControls,
|
|
185
|
+
args: {
|
|
186
|
+
menus: subMenus,
|
|
187
|
+
trigger: 'click',
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const WithSubMenusHover: Story = {
|
|
192
|
+
parameters: noControls,
|
|
193
|
+
args: {
|
|
194
|
+
menus: subMenus,
|
|
195
|
+
trigger: 'hover',
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const WithMinWidth: Story = {
|
|
200
|
+
parameters: noControls,
|
|
201
|
+
args: {
|
|
202
|
+
menus: subMenus,
|
|
203
|
+
trigger: 'click',
|
|
204
|
+
minWidth: 240,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const EmptyMenus: Story = {
|
|
209
|
+
parameters: noControls,
|
|
210
|
+
args: {
|
|
211
|
+
menus: [],
|
|
212
|
+
trigger: 'click',
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
126
216
|
export const CustomSlots: Story = {
|
|
127
217
|
parameters: {
|
|
128
218
|
...noControls,
|
|
@@ -2,50 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
DropdownMenu,
|
|
4
4
|
DropdownMenuContent,
|
|
5
|
-
DropdownMenuItem,
|
|
6
|
-
DropdownMenuLabel,
|
|
7
|
-
DropdownMenuSeparator,
|
|
8
5
|
DropdownMenuTrigger,
|
|
9
6
|
} from '../../shadcn/dropdown-menu'
|
|
10
|
-
import
|
|
11
|
-
import type {
|
|
12
|
-
DropdownActionItem,
|
|
13
|
-
DropdownCustomActionItem,
|
|
14
|
-
DropdownProps,
|
|
15
|
-
} from './types'
|
|
16
|
-
|
|
17
|
-
const actionColorVariants = cva('', {
|
|
18
|
-
variants: {
|
|
19
|
-
color: {
|
|
20
|
-
default: '',
|
|
21
|
-
primary: `
|
|
22
|
-
text-primary
|
|
23
|
-
focus:bg-primary/10 focus:text-primary
|
|
24
|
-
`,
|
|
25
|
-
success: `
|
|
26
|
-
text-success
|
|
27
|
-
focus:bg-success/10 focus:text-success
|
|
28
|
-
`,
|
|
29
|
-
info: `
|
|
30
|
-
text-info
|
|
31
|
-
focus:bg-info/10 focus:text-info
|
|
32
|
-
`,
|
|
33
|
-
help: `
|
|
34
|
-
text-help
|
|
35
|
-
focus:bg-help/10 focus:text-help
|
|
36
|
-
`,
|
|
37
|
-
warn: `
|
|
38
|
-
text-warn
|
|
39
|
-
focus:bg-warn/10 focus:text-warn
|
|
40
|
-
`,
|
|
41
|
-
danger: `
|
|
42
|
-
text-danger
|
|
43
|
-
focus:bg-danger/10 focus:text-danger
|
|
44
|
-
`,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
defaultVariants: { color: 'default' },
|
|
48
|
-
})
|
|
7
|
+
import MenuItems from './MenuItems.vue'
|
|
8
|
+
import type { DropdownProps } from './types'
|
|
49
9
|
|
|
50
10
|
defineOptions({ inheritAttrs: false })
|
|
51
11
|
|
|
@@ -53,14 +13,23 @@ const props = withDefaults(defineProps<DropdownProps>(), {
|
|
|
53
13
|
menus: () => [],
|
|
54
14
|
trigger: 'hover',
|
|
55
15
|
class: undefined,
|
|
16
|
+
minWidth: undefined,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const contentStyle = computed<{ minWidth?: string } | undefined>(() => {
|
|
20
|
+
if (props.minWidth == null) return undefined
|
|
21
|
+
const value = typeof props.minWidth === 'number' ? `${props.minWidth}px` : props.minWidth
|
|
22
|
+
return { minWidth: value }
|
|
56
23
|
})
|
|
57
24
|
|
|
58
|
-
defineSlots<{
|
|
59
|
-
default?: () =>
|
|
60
|
-
popup?: (props: { hide: () => void }) =>
|
|
61
|
-
|
|
25
|
+
const slots = defineSlots<{
|
|
26
|
+
default?: () => any
|
|
27
|
+
popup?: (props: { hide: () => void }) => any
|
|
28
|
+
empty?: () => any
|
|
29
|
+
[key: string]: ((props?: any) => any) | undefined
|
|
62
30
|
}>()
|
|
63
31
|
|
|
32
|
+
const T = useTranslations('components.ui.Dropdown')
|
|
64
33
|
const { isMobile } = useDevice()
|
|
65
34
|
|
|
66
35
|
// Force click trigger on mobile devices for better touch experience
|
|
@@ -107,17 +76,11 @@ const handleMenuLeave = () => {
|
|
|
107
76
|
}
|
|
108
77
|
}
|
|
109
78
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
event?.preventDefault()
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
item.command?.()
|
|
119
|
-
hide()
|
|
120
|
-
}
|
|
79
|
+
provide(dropdownContextKey, {
|
|
80
|
+
hide,
|
|
81
|
+
slots,
|
|
82
|
+
contentStyle,
|
|
83
|
+
})
|
|
121
84
|
|
|
122
85
|
onBeforeUnmount(() => {
|
|
123
86
|
clearHideTimeout()
|
|
@@ -139,6 +102,7 @@ onBeforeUnmount(() => {
|
|
|
139
102
|
<DropdownMenuContent
|
|
140
103
|
v-bind="$attrs"
|
|
141
104
|
:class="props.class"
|
|
105
|
+
:style="contentStyle"
|
|
142
106
|
@mouseenter="handleMenuEnter"
|
|
143
107
|
@mouseleave="handleMenuLeave"
|
|
144
108
|
>
|
|
@@ -151,109 +115,29 @@ onBeforeUnmount(() => {
|
|
|
151
115
|
</template>
|
|
152
116
|
|
|
153
117
|
<!-- Default menu dropdown -->
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
118
|
+
<MenuItems
|
|
119
|
+
v-else-if="menus.length"
|
|
120
|
+
:menus="menus"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<!-- Empty placeholder. Default content is wrapped; #empty slot is not. -->
|
|
124
|
+
<slot
|
|
125
|
+
v-else
|
|
126
|
+
name="empty"
|
|
127
|
+
>
|
|
128
|
+
<div
|
|
129
|
+
class="
|
|
130
|
+
gap-2 px-2 py-4 text-sm text-muted-foreground flex flex-col
|
|
131
|
+
items-center
|
|
132
|
+
"
|
|
158
133
|
>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{{ item.label }}
|
|
167
|
-
</DropdownMenuLabel>
|
|
168
|
-
<!-- Custom label: content via named slot -->
|
|
169
|
-
<DropdownMenuLabel
|
|
170
|
-
v-else-if="item.type === 'custom-label'"
|
|
171
|
-
:class="cn('p-0 font-normal', item.class)"
|
|
172
|
-
>
|
|
173
|
-
<slot
|
|
174
|
-
:name="item.slot"
|
|
175
|
-
:item="item"
|
|
176
|
-
/>
|
|
177
|
-
</DropdownMenuLabel>
|
|
178
|
-
<!-- Custom action: content via named slot -->
|
|
179
|
-
<DropdownMenuItem
|
|
180
|
-
v-else-if="item.type === 'custom-action'"
|
|
181
|
-
:disabled="item.disabled"
|
|
182
|
-
:class="cn(actionColorVariants({ color: item.color }), item.class)"
|
|
183
|
-
@click="handleItemAction(item, $event)"
|
|
184
|
-
>
|
|
185
|
-
<slot
|
|
186
|
-
:name="item.slot"
|
|
187
|
-
:item="item"
|
|
188
|
-
/>
|
|
189
|
-
<Icon
|
|
190
|
-
v-if="item.active"
|
|
191
|
-
name="check"
|
|
192
|
-
class="size-4 ml-auto"
|
|
193
|
-
/>
|
|
194
|
-
</DropdownMenuItem>
|
|
195
|
-
<!-- Built-in: action (default) -->
|
|
196
|
-
<DropdownMenuItem
|
|
197
|
-
v-else
|
|
198
|
-
:disabled="item.disabled"
|
|
199
|
-
:asChild="!!item.href"
|
|
200
|
-
:class="cn(actionColorVariants({ color: item.color }), item.class)"
|
|
201
|
-
@click="!item.href && handleItemAction(item, $event)"
|
|
202
|
-
>
|
|
203
|
-
<template v-if="item.href">
|
|
204
|
-
<WebLink
|
|
205
|
-
unstyled
|
|
206
|
-
:href="item.href"
|
|
207
|
-
:target="item.target"
|
|
208
|
-
class="gap-2 flex w-full items-center"
|
|
209
|
-
@click="handleItemAction(item, $event)"
|
|
210
|
-
>
|
|
211
|
-
<Icon
|
|
212
|
-
v-if="typeof item.icon === 'string'"
|
|
213
|
-
:name="item.icon"
|
|
214
|
-
/>
|
|
215
|
-
<component
|
|
216
|
-
:is="item.icon"
|
|
217
|
-
v-else-if="item.icon"
|
|
218
|
-
class="size-4"
|
|
219
|
-
/>
|
|
220
|
-
<span class="flex-1">
|
|
221
|
-
{{ item.label }}
|
|
222
|
-
</span>
|
|
223
|
-
<Icon
|
|
224
|
-
v-if="isUrl(item.href)"
|
|
225
|
-
name="external-link"
|
|
226
|
-
class="size-3.5 text-muted-foreground"
|
|
227
|
-
/>
|
|
228
|
-
<Icon
|
|
229
|
-
v-if="item.active"
|
|
230
|
-
name="check"
|
|
231
|
-
class="size-4"
|
|
232
|
-
/>
|
|
233
|
-
</WebLink>
|
|
234
|
-
</template>
|
|
235
|
-
<template v-else>
|
|
236
|
-
<Icon
|
|
237
|
-
v-if="typeof item.icon === 'string'"
|
|
238
|
-
:name="item.icon"
|
|
239
|
-
/>
|
|
240
|
-
<component
|
|
241
|
-
:is="item.icon"
|
|
242
|
-
v-else-if="item.icon"
|
|
243
|
-
class="size-4"
|
|
244
|
-
/>
|
|
245
|
-
<span class="flex-1">
|
|
246
|
-
{{ item.label }}
|
|
247
|
-
</span>
|
|
248
|
-
<Icon
|
|
249
|
-
v-if="item.active"
|
|
250
|
-
name="check"
|
|
251
|
-
class="size-4"
|
|
252
|
-
/>
|
|
253
|
-
</template>
|
|
254
|
-
</DropdownMenuItem>
|
|
255
|
-
</template>
|
|
256
|
-
</template>
|
|
134
|
+
<Icon
|
|
135
|
+
name="inbox"
|
|
136
|
+
class="size-6"
|
|
137
|
+
/>
|
|
138
|
+
<span>{{ T('empty') }}</span>
|
|
139
|
+
</div>
|
|
140
|
+
</slot>
|
|
257
141
|
</DropdownMenuContent>
|
|
258
142
|
</DropdownMenu>
|
|
259
143
|
</template>
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
import type { DropdownMenuContentProps } from 'reka-ui'
|
|
2
|
-
import type { Component } from 'vue'
|
|
2
|
+
import type { Component, ComputedRef, InjectionKey, Slots } from 'vue'
|
|
3
3
|
|
|
4
|
+
/** Semantic color, matches project-wide color scheme. */
|
|
4
5
|
export type DropdownItemColor = 'default' | 'primary' | 'success' | 'info' | 'help' | 'warn' | 'danger'
|
|
5
6
|
|
|
6
7
|
export interface DropdownActionItem {
|
|
7
|
-
/** Defaults to 'action' when omitted. */
|
|
8
|
+
/** Item kind. Defaults to 'action' when omitted. */
|
|
8
9
|
type?: 'action'
|
|
9
|
-
/**
|
|
10
|
+
/** Foreground color of the whole item (label + focus background). */
|
|
10
11
|
color?: DropdownItemColor
|
|
12
|
+
/** Override icon color independently of `color`. */
|
|
13
|
+
iconColor?: DropdownItemColor
|
|
14
|
+
/** Display text shown in the item. */
|
|
11
15
|
label?: string
|
|
16
|
+
/** Icon name (lucide kebab-case) or a Vue component. */
|
|
12
17
|
icon?: string | Component
|
|
18
|
+
/** Click handler. Ignored when `subMenus` is set. */
|
|
13
19
|
command?: () => void
|
|
20
|
+
/** Disabled items are non-interactive and visually muted. */
|
|
14
21
|
disabled?: boolean
|
|
22
|
+
/** Renders a trailing check icon to indicate selected/active state. */
|
|
15
23
|
active?: boolean
|
|
24
|
+
/** Extra class merged onto the item element. */
|
|
16
25
|
class?: ClassValue
|
|
26
|
+
/** Render the item as a link. Ignored when `subMenus` is set. */
|
|
17
27
|
href?: string
|
|
28
|
+
/** Anchor target. Only meaningful with `href`. */
|
|
18
29
|
target?: string
|
|
30
|
+
/** Nested sub-menu items. When provided, `command` / `href` are ignored. */
|
|
31
|
+
subMenus?: DropdownItem[]
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export interface DropdownSeparatorItem {
|
|
35
|
+
/** Item kind. */
|
|
22
36
|
type: 'separator'
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
export interface DropdownLabelItem {
|
|
40
|
+
/** Item kind. */
|
|
26
41
|
type: 'label'
|
|
42
|
+
/** Group header text. */
|
|
27
43
|
label: string
|
|
28
44
|
}
|
|
29
45
|
|
|
@@ -33,14 +49,21 @@ export interface DropdownLabelItem {
|
|
|
33
49
|
* to the slot as `item` for rendering.
|
|
34
50
|
*/
|
|
35
51
|
export interface DropdownCustomActionItem {
|
|
52
|
+
/** Item kind. */
|
|
36
53
|
type: 'custom-action'
|
|
37
|
-
/**
|
|
54
|
+
/** Foreground color of the whole item (label + focus background). */
|
|
38
55
|
color?: DropdownItemColor
|
|
56
|
+
/** Name of the slot that renders this item's content. */
|
|
39
57
|
slot: string
|
|
58
|
+
/** Click handler. */
|
|
40
59
|
command?: () => void
|
|
60
|
+
/** Disabled items are non-interactive and visually muted. */
|
|
41
61
|
disabled?: boolean
|
|
62
|
+
/** Renders a trailing check icon to indicate selected/active state. */
|
|
42
63
|
active?: boolean
|
|
64
|
+
/** Extra class merged onto the item element. */
|
|
43
65
|
class?: ClassValue
|
|
66
|
+
/** Arbitrary extra data forwarded to the slot as `item`. */
|
|
44
67
|
[field: string]: unknown
|
|
45
68
|
}
|
|
46
69
|
|
|
@@ -50,9 +73,13 @@ export interface DropdownCustomActionItem {
|
|
|
50
73
|
* to the slot as `item` for rendering.
|
|
51
74
|
*/
|
|
52
75
|
export interface DropdownCustomLabelItem {
|
|
76
|
+
/** Item kind. */
|
|
53
77
|
type: 'custom-label'
|
|
78
|
+
/** Name of the slot that renders this label's content. */
|
|
54
79
|
slot: string
|
|
80
|
+
/** Extra class merged onto the label element. */
|
|
55
81
|
class?: ClassValue
|
|
82
|
+
/** Arbitrary extra data forwarded to the slot as `item`. */
|
|
56
83
|
[field: string]: unknown
|
|
57
84
|
}
|
|
58
85
|
|
|
@@ -73,10 +100,25 @@ export type DropdownItem
|
|
|
73
100
|
| DropdownCustomLabelItem
|
|
74
101
|
|
|
75
102
|
export interface DropdownProps extends /* @vue-ignore */ DropdownMenuContentProps {
|
|
76
|
-
/** Menu items to display in the dropdown
|
|
103
|
+
/** Menu items to display in the dropdown. Not required when using the `popup` slot. */
|
|
77
104
|
menus?: DropdownItem[]
|
|
78
105
|
/** Trigger mode for showing the dropdown. Defaults to 'hover'. */
|
|
79
106
|
trigger?: 'click' | 'hover'
|
|
80
107
|
/** Extra class for the dropdown content container. */
|
|
81
108
|
class?: ClassValue
|
|
109
|
+
/** Min-width applied to the root content and all sub-menus. Numbers are treated as px. */
|
|
110
|
+
minWidth?: string | number
|
|
82
111
|
}
|
|
112
|
+
|
|
113
|
+
/** Context shared from the root Dropdown to nested MenuItems via provide/inject. */
|
|
114
|
+
export interface DropdownContext {
|
|
115
|
+
/** Closes the entire dropdown (root + any open sub-menus). */
|
|
116
|
+
hide: () => void
|
|
117
|
+
/** The root Dropdown's slots, used to render `custom-label` / `custom-action` items. */
|
|
118
|
+
slots: Slots
|
|
119
|
+
/** Inline style applied to root content and all sub-menus (currently min-width). */
|
|
120
|
+
contentStyle: ComputedRef<{ minWidth?: string } | undefined>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Provide/inject key for the shared DropdownContext. */
|
|
124
|
+
export const dropdownContextKey: InjectionKey<DropdownContext> = Symbol('dropdown-context')
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import Progress from './index.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'UI/Progress',
|
|
6
|
+
component: Progress,
|
|
7
|
+
argTypes: {
|
|
8
|
+
modelValue: { control: { type: 'number', min: 0, max: 100 }},
|
|
9
|
+
max: { control: 'number' },
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
modelValue: 50,
|
|
13
|
+
max: 100,
|
|
14
|
+
},
|
|
15
|
+
render: args => ({
|
|
16
|
+
components: { Progress },
|
|
17
|
+
setup: () => ({ args }),
|
|
18
|
+
template: `
|
|
19
|
+
<div class="max-w-sm">
|
|
20
|
+
<Progress v-bind="args" />
|
|
21
|
+
</div>
|
|
22
|
+
`,
|
|
23
|
+
}),
|
|
24
|
+
} satisfies Meta<typeof Progress>
|
|
25
|
+
|
|
26
|
+
export default meta
|
|
27
|
+
type Story = StoryObj<typeof meta>
|
|
28
|
+
|
|
29
|
+
const noControls = { controls: { disable: true }} satisfies Story['parameters']
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {}
|
|
32
|
+
|
|
33
|
+
export const CustomMax: Story = {
|
|
34
|
+
parameters: {
|
|
35
|
+
...noControls,
|
|
36
|
+
docs: {
|
|
37
|
+
source: {
|
|
38
|
+
code: `
|
|
39
|
+
<template>
|
|
40
|
+
<Progress :modelValue="150" :max="200" />
|
|
41
|
+
</template>
|
|
42
|
+
`.trim(),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
render: () => ({
|
|
47
|
+
components: { Progress },
|
|
48
|
+
template: `
|
|
49
|
+
<div class="max-w-sm space-y-2">
|
|
50
|
+
<Progress :modelValue="150" :max="200" />
|
|
51
|
+
<div class="text-sm text-muted-foreground">150 / 200</div>
|
|
52
|
+
</div>
|
|
53
|
+
`,
|
|
54
|
+
}),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Animated: Story = {
|
|
58
|
+
parameters: {
|
|
59
|
+
...noControls,
|
|
60
|
+
docs: {
|
|
61
|
+
source: {
|
|
62
|
+
code: `
|
|
63
|
+
<template>
|
|
64
|
+
<Progress :modelValue="value" />
|
|
65
|
+
</template>
|
|
66
|
+
`.trim(),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
render: () => ({
|
|
71
|
+
components: { Progress },
|
|
72
|
+
setup () {
|
|
73
|
+
const value = ref(0)
|
|
74
|
+
const id = setInterval(() => {
|
|
75
|
+
value.value = (value.value + 5) % 105
|
|
76
|
+
}, 300)
|
|
77
|
+
onUnmounted(() => clearInterval(id))
|
|
78
|
+
return { value }
|
|
79
|
+
},
|
|
80
|
+
template: `
|
|
81
|
+
<div class="max-w-sm space-y-2">
|
|
82
|
+
<Progress :modelValue="value" />
|
|
83
|
+
<div class="text-sm text-muted-foreground">{{ value }}%</div>
|
|
84
|
+
</div>
|
|
85
|
+
`,
|
|
86
|
+
}),
|
|
87
|
+
}
|
package/i18n/messages/ar.json
CHANGED
package/i18n/messages/de.json
CHANGED
package/i18n/messages/en.json
CHANGED
package/i18n/messages/es.json
CHANGED
package/i18n/messages/fr.json
CHANGED
package/i18n/messages/hi.json
CHANGED
package/i18n/messages/id.json
CHANGED
package/i18n/messages/it.json
CHANGED
package/i18n/messages/ja.json
CHANGED
package/i18n/messages/ko.json
CHANGED
package/i18n/messages/nl.json
CHANGED
package/i18n/messages/pl.json
CHANGED
package/i18n/messages/pt.json
CHANGED
package/i18n/messages/ru.json
CHANGED
package/i18n/messages/th.json
CHANGED
package/i18n/messages/tr.json
CHANGED
package/i18n/messages/vi.json
CHANGED
package/i18n/messages/zh-CN.json
CHANGED
package/i18n/messages/zh-TW.json
CHANGED
package/nuxt.config.ts
CHANGED
|
@@ -52,9 +52,15 @@ export default defineNuxtConfig({
|
|
|
52
52
|
css: [ join(currentDir, 'app/assets/styles/globals.css') ],
|
|
53
53
|
|
|
54
54
|
components: [
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// Auto-import only first-level UI components: `Foo.vue` or `Foo/index.vue`.
|
|
56
|
+
// Files nested inside a component folder (e.g. `Dropdown/MenuItems.vue`)
|
|
57
|
+
// are intentionally skipped — those should be imported explicitly by their
|
|
58
|
+
// owning component. shadcn/* is excluded by the pattern (no nested scan).
|
|
59
|
+
{
|
|
60
|
+
path: join(currentDir, 'app/components/ui'),
|
|
61
|
+
pathPrefix: true,
|
|
62
|
+
pattern: '{*.vue,*/index.vue}',
|
|
63
|
+
},
|
|
58
64
|
],
|
|
59
65
|
|
|
60
66
|
imports: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymarbot/nuxt-layer-shadcn-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Nuxt layer providing shadcn-vue based UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"vue-i18n": "^11",
|
|
43
43
|
"vue-router": "^4 || ^5"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "b25d6d9d16ad5ba72906936f2c672d65035df9ea"
|
|
46
46
|
}
|