frappe-ui 0.1.172 → 0.1.174

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.172",
3
+ "version": "0.1.174",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -19,11 +19,11 @@
19
19
  :class="[
20
20
  config.negativeIsBetter
21
21
  ? config.delta >= 0
22
- ? 'text-red-500'
23
- : 'text-green-500'
22
+ ? 'text-ink-red-4'
23
+ : 'text-ink-green-3'
24
24
  : config.delta >= 0
25
- ? 'text-green-500'
26
- : 'text-red-500',
25
+ ? 'text-ink-green-3'
26
+ : 'text-ink-red-4',
27
27
  ]"
28
28
  >
29
29
  <span class="">
@@ -94,7 +94,7 @@ export default function useDonutChartOptions(config: DonutChartConfig) {
94
94
  },
95
95
  textStyle: {
96
96
  padding: [0, 0, 0, -5],
97
- color: '#000',
97
+ color: 'var(--ink-gray-8)',
98
98
  },
99
99
  icon: 'circle',
100
100
  pageIcons: {
@@ -103,11 +103,11 @@ export default function useDonutChartOptions(config: DonutChartConfig) {
103
103
  'M 12 27 h -2 c -0.386 0 -0.738 -0.223 -0.904 -0.572 s -0.115 -0.762 0.13 -1.062 L 17.708 15 L 9.226 4.633 c -0.245 -0.299 -0.295 -0.712 -0.13 -1.062 S 9.614 3 10 3 h 2 c 0.3 0 0.584 0.135 0.774 0.367 l 9 11 c 0.301 0.369 0.301 0.898 0 1.267 l -9 11 C 12.584 26.865 12.3 27 12 27 Z',
104
104
  ],
105
105
  },
106
- pageIconColor: '#64748B',
107
- pageInactiveColor: '#C0CCDA',
106
+ pageIconColor: 'var(--ink-gray-6)',
107
+ pageInactiveColor: 'var(--ink-gray-4)',
108
108
  pageIconSize: 10,
109
109
  pageTextStyle: {
110
- color: '#64748B',
110
+ color: 'var(--ink-gray-6)',
111
111
  },
112
112
  animationDurationUpdate: 300,
113
113
  }
@@ -110,7 +110,7 @@ export default function useEchartsOptions(config: AxisChartConfig) {
110
110
  },
111
111
  textStyle: {
112
112
  padding: [0, 0, 0, -5],
113
- color: '#000',
113
+ color: 'var(--ink-gray-8)',
114
114
  },
115
115
  icon: 'circle',
116
116
  pageIcons: {
@@ -119,11 +119,11 @@ export default function useEchartsOptions(config: AxisChartConfig) {
119
119
  'M 12 27 h -2 c -0.386 0 -0.738 -0.223 -0.904 -0.572 s -0.115 -0.762 0.13 -1.062 L 17.708 15 L 9.226 4.633 c -0.245 -0.299 -0.295 -0.712 -0.13 -1.062 S 9.614 3 10 3 h 2 c 0.3 0 0.584 0.135 0.774 0.367 l 9 11 c 0.301 0.369 0.301 0.898 0 1.267 l -9 11 C 12.584 26.865 12.3 27 12 27 Z',
120
120
  ],
121
121
  },
122
- pageIconColor: '#64748B',
123
- pageInactiveColor: '#C0CCDA',
122
+ pageIconColor: 'var(--ink-gray-6)',
123
+ pageInactiveColor: 'var(--ink-gray-4)',
124
124
  pageIconSize: 10,
125
125
  pageTextStyle: {
126
- color: '#64748B',
126
+ color: 'var(--ink-gray-6)',
127
127
  },
128
128
  animationDurationUpdate: 300,
129
129
  },
@@ -142,13 +142,13 @@ export function getTitleOptions(title: string, subtitle?: string) {
142
142
  fontSize: 14,
143
143
  fontWeight: 500,
144
144
  lineHeight: 24,
145
- // color: titleColor
145
+ color: 'var(--ink-gray-8)',
146
146
  },
147
147
  subtextStyle: {
148
148
  fontSize: 13,
149
149
  fontWeight: 400,
150
150
  lineHeight: 20,
151
- // color: subtitleColor,
151
+ color: 'var(--ink-gray-6)',
152
152
  },
153
153
  }
154
154
  }
@@ -169,14 +169,17 @@ function getXAxisOptions(config: AxisChartConfig) {
169
169
  align: 'right',
170
170
  verticalAlign: 'bottom',
171
171
  padding: [0, 0, 26, 0],
172
- // color: '#000',
173
- backgroundColor: '#fff',
174
- borderColor: '#fff',
172
+ backgroundColor: 'var(--surface-white)',
173
+ borderColor: 'var(--surface-white)',
174
+ color: 'var(--ink-gray-8)',
175
175
  borderWidth: 4,
176
176
  },
177
177
  splitLine: {
178
178
  show: true,
179
179
  width: 1,
180
+ lineStyle: {
181
+ color: 'var(--ink-gray-3)',
182
+ },
180
183
  },
181
184
  axisLine: {
182
185
  show: false,
@@ -261,14 +264,17 @@ function getYAxisOptions(config: AxisChartConfig) {
261
264
  align: 'left',
262
265
  verticalAlign: 'top',
263
266
  padding: [0, 0, 0, -2],
264
- // color: '#000',
265
- backgroundColor: '#fff',
266
- borderColor: '#fff',
267
+ backgroundColor: 'var(--surface-white)',
268
+ borderColor: 'var(--surface-white)',
269
+ color: 'var(--ink-gray-8)',
267
270
  borderWidth: 4,
268
271
  },
269
272
  splitLine: {
270
273
  show: true,
271
274
  width: 1,
275
+ lineStyle: {
276
+ color: 'var(--ink-gray-3)',
277
+ },
272
278
  },
273
279
  axisLine: {
274
280
  show: false,
@@ -308,13 +314,17 @@ function getYAxisOptions(config: AxisChartConfig) {
308
314
  align: 'right',
309
315
  verticalAlign: 'top',
310
316
  padding: [0, 5, 0, 0],
311
- // color: '#000',
312
- backgroundColor: '#fff',
317
+ backgroundColor: 'var(--surface-white)',
318
+ borderColor: 'var(--surface-white)',
319
+ color: 'var(--ink-gray-8)',
313
320
  },
314
321
  nameGap: 6,
315
322
  splitLine: {
316
323
  show: true,
317
324
  width: 1,
325
+ lineStyle: {
326
+ color: 'var(--ink-gray-3)',
327
+ },
318
328
  },
319
329
  axisLine: {
320
330
  show: false,
@@ -1,98 +1,188 @@
1
1
  <script setup lang="ts">
2
- import { h } from 'vue'
3
- import Dropdown from './Dropdown.vue'
4
- import FeatherIcon from '../FeatherIcon.vue'
2
+ import { Dropdown } from './index'
5
3
  import { Button } from '../Button'
6
- </script>
7
4
 
8
- <template>
9
- <Story :layout="{ type: 'grid', width: 300 }">
10
- <Variant title="Basic">
11
- <Dropdown
12
- :options="[
5
+ const actions = [
6
+ {
7
+ label: 'Edit',
8
+ icon: 'edit',
9
+ onClick: () => console.log('Edit clicked'),
10
+ },
11
+ {
12
+ label: 'Delete',
13
+ icon: 'trash-2',
14
+ onClick: () => console.log('Delete clicked'),
15
+ },
16
+ ]
17
+
18
+ const groupedActions = [
19
+ {
20
+ group: 'Actions',
21
+ items: [
22
+ {
23
+ label: 'Edit',
24
+ icon: 'edit',
25
+ onClick: () => console.log('Edit clicked'),
26
+ },
27
+ {
28
+ label: 'Duplicate',
29
+ icon: 'copy',
30
+ onClick: () => console.log('Duplicate clicked'),
31
+ },
32
+ {
33
+ label: 'More Actions',
34
+ icon: 'more-horizontal',
35
+ submenu: [
13
36
  {
14
- label: 'Edit Title',
15
- onClick: () => {},
16
- icon: () => h(FeatherIcon, { name: 'edit-2' }),
37
+ label: 'Archive',
38
+ icon: 'archive',
39
+ onClick: () => console.log('Archive clicked'),
17
40
  },
18
41
  {
19
- label: 'Manage Members',
20
- onClick: () => {},
21
- icon: () => h(FeatherIcon, { name: 'users' }),
42
+ label: 'Export',
43
+ icon: 'download',
44
+ submenu: [
45
+ {
46
+ label: 'Export as PDF',
47
+ icon: 'file-text',
48
+ onClick: () => console.log('Export as PDF clicked'),
49
+ },
50
+ {
51
+ label: 'Export as CSV',
52
+ icon: 'file',
53
+ onClick: () => console.log('Export as CSV clicked'),
54
+ },
55
+ ],
22
56
  },
23
57
  {
24
- label: 'Delete this project',
25
- onClick: () => {},
26
- icon: () => h(FeatherIcon, { name: 'trash' }),
58
+ label: 'Share',
59
+ icon: 'share',
60
+ onClick: () => console.log('Share clicked'),
27
61
  },
28
- ]"
29
- />
30
- </Variant>
62
+ ],
63
+ },
64
+ ],
65
+ },
66
+ {
67
+ group: 'Danger',
68
+ items: [
69
+ {
70
+ label: 'Delete',
71
+ icon: 'trash-2',
72
+ onClick: () => console.log('Delete clicked'),
73
+ },
74
+ ],
75
+ },
76
+ ]
31
77
 
32
- <Variant title="Button prop">
33
- <Dropdown
34
- :options="[
78
+ const submenuActions = [
79
+ {
80
+ label: 'New',
81
+ icon: 'plus',
82
+ submenu: [
83
+ {
84
+ group: 'Documents',
85
+ items: [
35
86
  {
36
- label: 'Edit Title',
37
- onClick: () => {},
87
+ label: 'New Document',
88
+ icon: 'file-plus',
89
+ onClick: () => console.log('New Document clicked'),
38
90
  },
39
91
  {
40
- label: 'Manage Members',
41
- onClick: () => {},
92
+ label: 'New Template',
93
+ icon: 'file-text',
94
+ onClick: () => console.log('New Template clicked'),
42
95
  },
96
+ ],
97
+ },
98
+ {
99
+ group: 'Organization',
100
+ items: [
43
101
  {
44
- label: 'Delete this project',
45
- onClick: () => {},
102
+ label: 'New Folder',
103
+ icon: 'folder-plus',
104
+ onClick: () => console.log('New Folder clicked'),
46
105
  },
47
- ]"
48
- :button="{
49
- label: 'Actions',
50
- }"
51
- />
52
- </Variant>
53
-
54
- <Variant title="Custom Button and Groups">
55
- <Dropdown
56
- :options="[
57
106
  {
58
- group: 'Manage',
59
- items: [
60
- {
61
- label: 'Edit Title',
62
- icon: () => h(FeatherIcon, { name: 'edit' }),
63
- },
64
- {
65
- label: 'Manage Members',
66
- icon: () => h(FeatherIcon, { name: 'users' }),
67
- },
68
- ],
107
+ label: 'New Project',
108
+ icon: 'briefcase',
109
+ onClick: () => console.log('New Project clicked'),
69
110
  },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ label: 'Edit',
117
+ icon: 'edit',
118
+ onClick: () => console.log('Edit clicked'),
119
+ },
120
+ {
121
+ label: 'Share',
122
+ icon: 'share',
123
+ submenu: [
124
+ {
125
+ label: 'Share with Link',
126
+ icon: 'link',
127
+ onClick: () => console.log('Share with Link clicked'),
128
+ },
129
+ {
130
+ label: 'Share with Email',
131
+ icon: 'mail',
132
+ onClick: () => console.log('Share with Email clicked'),
133
+ },
134
+ {
135
+ group: 'Advanced',
136
+ items: [
70
137
  {
71
- group: 'Delete',
72
- items: [
73
- {
74
- label: 'Delete users',
75
- icon: () => h(FeatherIcon, { name: 'edit' }),
76
- },
77
- {
78
- label: 'Delete this project',
79
- icon: () => h(FeatherIcon, { name: 'trash' }),
80
- },
81
- ],
138
+ label: 'Share Settings',
139
+ icon: 'settings',
140
+ onClick: () => console.log('Share Settings clicked'),
82
141
  },
83
- ]"
84
- >
85
- <Button>
86
- <template #icon>
87
- <FeatherIcon name="more-horizontal" class="h-4 w-4" />
88
- </template>
89
- </Button>
142
+ {
143
+ label: 'Permission Management',
144
+ icon: 'shield',
145
+ onClick: () => console.log('Permission Management clicked'),
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ },
151
+ ]
152
+ </script>
153
+
154
+ <template>
155
+ <Story title="Dropdown" :layout="{ type: 'grid', width: '200px' }">
156
+ <Variant title="Default">
157
+ <div class="asdf">
158
+ <Dropdown :options="actions" />
159
+ </div>
160
+ </Variant>
161
+
162
+ <Variant title="With Custom Button">
163
+ <Dropdown :options="actions">
164
+ <Button variant="solid">Custom Trigger</Button>
90
165
  </Dropdown>
91
166
  </Variant>
92
167
 
93
- <template #controls>
94
- <!-- <HstText v-model="state.label" title="Label" />
95
- <HstSelect v-model="state.size" :options="sizes" title="Size" /> -->
96
- </template>
168
+ <Variant title="With Groups">
169
+ <Dropdown :options="groupedActions" />
170
+ </Variant>
171
+
172
+ <Variant title="Right Aligned">
173
+ <Dropdown :options="actions" placement="right" />
174
+ </Variant>
175
+
176
+ <Variant title="Center Aligned">
177
+ <Dropdown :options="actions" placement="center" />
178
+ </Variant>
179
+
180
+ <Variant title="With Submenus">
181
+ <Dropdown :options="submenuActions" />
182
+ </Variant>
183
+
184
+ <Variant title="With Nested Submenus">
185
+ <Dropdown :options="groupedActions" />
186
+ </Variant>
97
187
  </Story>
98
188
  </template>
@@ -1,79 +1,159 @@
1
1
  <template>
2
- <Menu as="div" class="relative inline-block text-left" v-slot="{ open }">
3
- <Popover
4
- :transition="dropdownTransition"
5
- :show="open"
6
- :placement="popoverPlacement"
7
- >
8
- <template #target>
9
- <MenuButton as="div">
10
- <slot v-if="$slots.default" v-bind="{ open }" />
11
- <Button v-else :active="open" v-bind="button">
12
- {{ button ? button?.label || null : 'Options' }}
13
- </Button>
14
- </MenuButton>
15
- </template>
16
-
17
- <template #body>
18
- <MenuItems
19
- class="mt-2 min-w-40 divide-y divide-outline-gray-modals rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
20
- :class="{
21
- 'left-0 origin-top-left': placement == 'left',
22
- 'right-0 origin-top-right': placement == 'right',
23
- 'inset-x-0 origin-top': placement == 'center',
24
- }"
2
+ <DropdownMenuRoot v-slot="{ open }">
3
+ <DropdownMenuTrigger as-child>
4
+ <slot v-if="$slots.default" v-bind="{ open }" />
5
+ <Button v-else :active="false" v-bind="button">
6
+ {{ button ? button?.label || null : 'Options' }}
7
+ </Button>
8
+ </DropdownMenuTrigger>
9
+
10
+ <DropdownMenuPortal>
11
+ <DropdownMenuContent
12
+ :class="[
13
+ cssClasses.dropdownContent,
14
+ {
15
+ 'origin-top-left': placement == 'left',
16
+ 'origin-top-right': placement == 'right',
17
+ 'origin-top': placement == 'center',
18
+ },
19
+ ]"
20
+ :side="contentSide"
21
+ :align="contentAlign"
22
+ :side-offset="4"
23
+ >
24
+ <div
25
+ v-for="group in groups"
26
+ :key="group.key"
27
+ :class="cssClasses.groupContainer"
25
28
  >
26
- <div v-for="group in groups" :key="group.key" class="p-1.5">
27
- <div
28
- v-if="group.group && !group.hideLabel"
29
- class="flex h-7 items-center px-2 text-sm font-medium text-ink-gray-6"
30
- >
31
- {{ group.group }}
32
- </div>
33
- <MenuItem
34
- v-for="item in group.items"
35
- :key="item.label"
36
- v-slot="{ active }"
37
- >
29
+ <DropdownMenuLabel
30
+ v-if="group.group && !group.hideLabel"
31
+ :class="cssClasses.groupLabel"
32
+ >
33
+ {{ group.group }}
34
+ </DropdownMenuLabel>
35
+
36
+ <DropdownMenuItem
37
+ v-for="item in group.items"
38
+ :key="item.label"
39
+ as-child
40
+ @select="item.onClick"
41
+ >
42
+ <component
43
+ v-if="item.component"
44
+ :is="item.component"
45
+ :active="false"
46
+ />
47
+ <DropdownMenuSub v-else-if="item.submenu">
48
+ <DropdownMenuSubTrigger as-child>
49
+ <button :class="cssClasses.submenuTrigger">
50
+ <FeatherIcon
51
+ v-if="item.icon && typeof item.icon === 'string'"
52
+ :name="item.icon"
53
+ :class="cssClasses.itemIcon"
54
+ aria-hidden="true"
55
+ />
56
+ <component
57
+ :class="cssClasses.itemIcon"
58
+ v-else-if="item.icon"
59
+ :is="item.icon"
60
+ />
61
+ <span :class="cssClasses.itemLabel">
62
+ {{ item.label }}
63
+ </span>
64
+ <FeatherIcon
65
+ name="chevron-right"
66
+ :class="cssClasses.chevronIcon"
67
+ aria-hidden="true"
68
+ />
69
+ </button>
70
+ </DropdownMenuSubTrigger>
71
+ <DropdownMenuPortal>
72
+ <DropdownMenuSubContent
73
+ :class="cssClasses.dropdownContent"
74
+ :side-offset="4"
75
+ >
76
+ <div
77
+ v-for="submenuGroup in getSubmenuGroups(item.submenu)"
78
+ :key="submenuGroup.key"
79
+ :class="cssClasses.groupContainer"
80
+ >
81
+ <DropdownMenuLabel
82
+ v-if="submenuGroup.group && !submenuGroup.hideLabel"
83
+ :class="cssClasses.groupLabel"
84
+ >
85
+ {{ submenuGroup.group }}
86
+ </DropdownMenuLabel>
87
+
88
+ <DropdownMenuItem
89
+ v-for="subItem in submenuGroup.items"
90
+ :key="subItem.label"
91
+ as-child
92
+ @select="() => handleItemClick(subItem)"
93
+ >
94
+ <component
95
+ v-if="subItem.component"
96
+ :is="subItem.component"
97
+ :active="false"
98
+ />
99
+ <button v-else :class="cssClasses.itemButton">
100
+ <FeatherIcon
101
+ v-if="
102
+ subItem.icon && typeof subItem.icon === 'string'
103
+ "
104
+ :name="subItem.icon"
105
+ :class="cssClasses.itemIcon"
106
+ aria-hidden="true"
107
+ />
108
+ <component
109
+ :class="cssClasses.itemIcon"
110
+ v-else-if="subItem.icon"
111
+ :is="subItem.icon"
112
+ />
113
+ <span :class="cssClasses.itemLabel">
114
+ {{ subItem.label }}
115
+ </span>
116
+ </button>
117
+ </DropdownMenuItem>
118
+ </div>
119
+ </DropdownMenuSubContent>
120
+ </DropdownMenuPortal>
121
+ </DropdownMenuSub>
122
+ <button v-else :class="cssClasses.itemButton">
123
+ <FeatherIcon
124
+ v-if="item.icon && typeof item.icon === 'string'"
125
+ :name="item.icon"
126
+ :class="cssClasses.itemIcon"
127
+ aria-hidden="true"
128
+ />
38
129
  <component
39
- v-if="item.component"
40
- :is="item.component"
41
- :active="active"
130
+ :class="cssClasses.itemIcon"
131
+ v-else-if="item.icon"
132
+ :is="item.icon"
42
133
  />
43
- <button
44
- v-else
45
- :class="[
46
- active ? 'bg-surface-gray-3' : 'text-ink-gray-6',
47
- 'group flex h-7 w-full items-center rounded px-2 text-base',
48
- ]"
49
- @click="item.onClick"
50
- >
51
- <FeatherIcon
52
- v-if="item.icon && typeof item.icon === 'string'"
53
- :name="item.icon"
54
- class="mr-2 h-4 w-4 flex-shrink-0 text-ink-gray-6"
55
- aria-hidden="true"
56
- />
57
- <component
58
- class="mr-2 h-4 w-4 flex-shrink-0 text-ink-gray-6"
59
- v-else-if="item.icon"
60
- :is="item.icon"
61
- />
62
- <span class="whitespace-nowrap text-ink-gray-7">
63
- {{ item.label }}
64
- </span>
65
- </button>
66
- </MenuItem>
67
- </div>
68
- </MenuItems>
69
- </template>
70
- </Popover>
71
- </Menu>
134
+ <span :class="cssClasses.itemLabel">
135
+ {{ item.label }}
136
+ </span>
137
+ </button>
138
+ </DropdownMenuItem>
139
+ </div>
140
+ </DropdownMenuContent>
141
+ </DropdownMenuPortal>
142
+ </DropdownMenuRoot>
72
143
  </template>
73
144
 
74
145
  <script setup lang="ts">
75
- import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
76
- import { Popover } from '../Popover'
146
+ import {
147
+ DropdownMenuRoot,
148
+ DropdownMenuTrigger,
149
+ DropdownMenuPortal,
150
+ DropdownMenuContent,
151
+ DropdownMenuLabel,
152
+ DropdownMenuItem,
153
+ DropdownMenuSub,
154
+ DropdownMenuSubTrigger,
155
+ DropdownMenuSubContent,
156
+ } from 'reka-ui'
77
157
  import { Button } from '../Button'
78
158
  import FeatherIcon from '../FeatherIcon.vue'
79
159
  import { computed } from 'vue'
@@ -82,6 +162,7 @@ import type {
82
162
  DropdownProps,
83
163
  DropdownOption,
84
164
  DropdownGroupOption,
165
+ DropdownOptions,
85
166
  } from './types'
86
167
 
87
168
  const router = useRouter()
@@ -91,36 +172,34 @@ const props = withDefaults(defineProps<DropdownProps>(), {
91
172
  placement: 'left',
92
173
  })
93
174
 
94
- const normalizeDropdownItem = (option: DropdownOption) => {
95
- let onClick = () => {
96
- if (option.route) {
97
- router.push(option.route)
98
- } else if (option.onClick) {
99
- option.onClick()
100
- }
175
+ // Unified click handling for all dropdown items
176
+ const handleItemClick = (item: DropdownOption) => {
177
+ if (item.route) {
178
+ router.push(item.route)
179
+ } else if (item.onClick) {
180
+ item.onClick()
101
181
  }
182
+ }
102
183
 
184
+ const normalizeDropdownItem = (option: DropdownOption) => {
103
185
  return {
104
186
  label: option.label,
105
187
  icon: option.icon,
106
188
  component: option.component,
107
- onClick,
189
+ onClick: () => handleItemClick(option),
190
+ submenu: option.submenu,
108
191
  }
109
192
  }
110
193
 
111
- const filterOptions = (options: DropdownOption[]) => {
112
- return (options || [])
113
- .filter(Boolean)
114
- .filter((option) => (option.condition ? option.condition() : true))
115
- .map((option) => normalizeDropdownItem(option))
116
- }
117
-
118
- const groups = computed(() => {
194
+ // Unified group processing for both main options and submenu options
195
+ const processOptionsIntoGroups = (
196
+ options: DropdownOptions,
197
+ ): DropdownGroupOption[] => {
119
198
  let groups: DropdownGroupOption[] = []
120
199
  let currentGroup: DropdownGroupOption | null = null
121
200
  let i = 0
122
201
 
123
- for (let option of props.options) {
202
+ for (let option of options) {
124
203
  if (option == null) {
125
204
  continue
126
205
  }
@@ -155,23 +234,85 @@ const groups = computed(() => {
155
234
  }
156
235
 
157
236
  return groups
237
+ }
238
+
239
+ const getSubmenuGroups = (submenuOptions: DropdownOptions) => {
240
+ return processOptionsIntoGroups(submenuOptions)
241
+ }
242
+
243
+ const filterOptions = (options: DropdownOption[]) => {
244
+ return (options || [])
245
+ .filter(Boolean)
246
+ .filter((option) => (option.condition ? option.condition() : true))
247
+ .map((option) => normalizeDropdownItem(option))
248
+ }
249
+
250
+ // Semantic CSS classes for consistent styling
251
+ const cssClasses = {
252
+ // Container classes
253
+ dropdownContent:
254
+ 'min-w-40 divide-y divide-outline-gray-modals rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none dropdown-content',
255
+ groupContainer: 'p-1.5',
256
+
257
+ // Label classes
258
+ groupLabel: 'flex h-7 items-center px-2 text-sm font-medium text-ink-gray-6',
259
+ itemLabel: 'whitespace-nowrap text-ink-gray-7',
260
+
261
+ // Icon classes
262
+ itemIcon: 'mr-2 h-4 w-4 flex-shrink-0 text-ink-gray-6',
263
+ chevronIcon: 'ml-auto h-4 w-4 flex-shrink-0 text-ink-gray-6',
264
+
265
+ // Button classes
266
+ itemButton:
267
+ 'group flex h-7 w-full items-center rounded px-2 text-base text-ink-gray-6 focus:bg-surface-gray-3 focus:outline-none data-[highlighted]:bg-surface-gray-3',
268
+ submenuTrigger:
269
+ 'group flex h-7 w-full items-center rounded px-2 text-base text-ink-gray-6 focus:bg-surface-gray-3 focus:outline-none data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3',
270
+ }
271
+
272
+ const groups = computed(() => {
273
+ return processOptionsIntoGroups(props.options)
158
274
  })
159
275
 
160
- const dropdownTransition = computed(() => {
161
- return {
162
- enterActiveClass: 'transition duration-100 ease-out',
163
- enterFromClass: 'transform scale-95 opacity-0',
164
- enterToClass: 'transform scale-100 opacity-100',
165
- leaveActiveClass: 'transition duration-75 ease-in',
166
- leaveFromClass: 'transform scale-100 opacity-100',
167
- leaveToClass: 'transform scale-95 opacity-0',
168
- }
276
+ const contentSide = computed(() => {
277
+ return 'bottom' as const
169
278
  })
170
279
 
171
- const popoverPlacement = computed(() => {
172
- if (props.placement === 'left') return 'bottom-start'
173
- if (props.placement === 'right') return 'bottom-end'
174
- if (props.placement === 'center') return 'bottom-center'
175
- return 'bottom'
280
+ const contentAlign = computed(() => {
281
+ if (props.placement === 'left') return 'start' as const
282
+ if (props.placement === 'right') return 'end' as const
283
+ if (props.placement === 'center') return 'center' as const
284
+ return 'start' as const
176
285
  })
177
286
  </script>
287
+
288
+ <style scoped>
289
+ @keyframes dropdown-in {
290
+ from {
291
+ opacity: 0;
292
+ transform: scale(0.95);
293
+ }
294
+ to {
295
+ opacity: 1;
296
+ transform: scale(1);
297
+ }
298
+ }
299
+
300
+ @keyframes dropdown-out {
301
+ from {
302
+ opacity: 1;
303
+ transform: scale(1);
304
+ }
305
+ to {
306
+ opacity: 0;
307
+ transform: scale(0.95);
308
+ }
309
+ }
310
+
311
+ :global(.dropdown-content[data-state='open']) {
312
+ animation: dropdown-in 100ms ease-out;
313
+ }
314
+
315
+ :global(.dropdown-content[data-state='closed']) {
316
+ animation: dropdown-out 75ms ease-in;
317
+ }
318
+ </style>
@@ -8,6 +8,7 @@ export type DropdownOption = {
8
8
  onClick?: () => void
9
9
  route?: RouterLinkProps['to']
10
10
  condition?: () => boolean
11
+ submenu?: DropdownOptions
11
12
  }
12
13
 
13
14
  export type DropdownGroupOption = {
@@ -23,4 +24,4 @@ export interface DropdownProps {
23
24
  button?: ButtonProps
24
25
  options?: DropdownOptions
25
26
  placement?: string
26
- }
27
+ }
@@ -39,6 +39,7 @@
39
39
  <script>
40
40
  import { RadioGroup, RadioGroupLabel, RadioGroupOption } from '@headlessui/vue'
41
41
  import FeatherIcon from '../FeatherIcon.vue'
42
+ import Button from '../Button/Button.vue'
42
43
 
43
44
  export default {
44
45
  name: 'TabButtons',
@@ -53,6 +54,7 @@ export default {
53
54
  },
54
55
  emits: ['update:modelValue'],
55
56
  components: {
57
+ Button,
56
58
  FeatherIcon,
57
59
  RadioGroup,
58
60
  RadioGroupOption,