@citizenplane/pimp 17.0.13 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CpContextualMenu.vue.d.ts.map +1 -1
- package/dist/components/CpItemActions.vue.d.ts.map +1 -1
- package/dist/components/CpMenu.vue.d.ts +32 -0
- package/dist/components/CpMenu.vue.d.ts.map +1 -0
- package/dist/components/CpMenuItem.vue.d.ts +20 -12
- package/dist/components/CpMenuItem.vue.d.ts.map +1 -1
- package/dist/components/CpMenuList.vue.d.ts +17 -0
- package/dist/components/CpMenuList.vue.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/pimp.es.js +11655 -9347
- package/dist/pimp.umd.js +1033 -44
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CpContextualMenu.vue +1 -1
- package/src/components/CpItemActions.vue +1 -1
- package/src/components/CpMenu.vue +245 -0
- package/src/components/CpMenuItem.vue +62 -82
- package/src/components/CpMenuList.vue +54 -0
- package/src/components/index.ts +6 -0
- package/src/stories/CpMenu.stories.ts +149 -0
- package/src/stories/CpMenuItem.stories.ts +328 -54
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
4
|
+
|
|
5
|
+
import CpButton from '@/components/CpButton.vue'
|
|
6
|
+
import CpMenu from '@/components/CpMenu.vue'
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Molecules/CpMenu',
|
|
10
|
+
component: CpMenu,
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'A popover-based menu built on top of `primevue/popover`. The trigger is provided through the `trigger` slot — clicking it toggles the menu, which is anchored to the trigger element. Items can be passed through the `items` prop (with icons, separators, async commands, critical styling) or rendered via the default slot for arbitrary content.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
items: {
|
|
21
|
+
control: 'object',
|
|
22
|
+
description: 'The menu items to display. Each item is a PrimeVue `MenuItem`.',
|
|
23
|
+
table: {
|
|
24
|
+
type: { summary: 'MenuItem[]' },
|
|
25
|
+
defaultValue: { summary: '[]' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
forcePopover: {
|
|
29
|
+
control: 'boolean',
|
|
30
|
+
description: 'Disable the bottom-drawer behavior on mobile and keep the popover anchored to the trigger.',
|
|
31
|
+
table: { defaultValue: { summary: 'false' } },
|
|
32
|
+
},
|
|
33
|
+
keepOpenOnClick: {
|
|
34
|
+
control: 'boolean',
|
|
35
|
+
description: 'Keep the menu open when clicking an item.',
|
|
36
|
+
table: { defaultValue: { summary: 'false' } },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
decorators: [() => ({ template: '<div style="min-height: 60vh; padding: 80px;"><story /></div>' })],
|
|
40
|
+
} satisfies Meta<typeof CpMenu>
|
|
41
|
+
|
|
42
|
+
export default meta
|
|
43
|
+
|
|
44
|
+
type Story = StoryObj<typeof meta>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Click the trigger to toggle a menu featuring an async "Download" action,
|
|
48
|
+
* a separator and a critical "Delete" action.
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
render: () => ({
|
|
52
|
+
components: { CpButton, CpMenu },
|
|
53
|
+
setup() {
|
|
54
|
+
const isLoading = ref(false)
|
|
55
|
+
|
|
56
|
+
const items = computed(() => [
|
|
57
|
+
{
|
|
58
|
+
label: 'Edit',
|
|
59
|
+
leadingIcon: 'edit',
|
|
60
|
+
command: () => alert('Edit clicked'),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Download',
|
|
64
|
+
leadingIcon: 'download',
|
|
65
|
+
isLoading: isLoading.value,
|
|
66
|
+
isAsync: true,
|
|
67
|
+
command: async () => {
|
|
68
|
+
isLoading.value = true
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
70
|
+
isLoading.value = false
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{ separator: true },
|
|
74
|
+
{
|
|
75
|
+
label: 'Delete',
|
|
76
|
+
leadingIcon: 'trash-2',
|
|
77
|
+
isCritical: true,
|
|
78
|
+
command: () => alert('Delete clicked'),
|
|
79
|
+
},
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
return { items }
|
|
83
|
+
},
|
|
84
|
+
template: `
|
|
85
|
+
<CpMenu :items="items" class="defaultMenu">
|
|
86
|
+
<template #trigger>
|
|
87
|
+
<CpButton>Open menu</CpButton>
|
|
88
|
+
</template>
|
|
89
|
+
</CpMenu>
|
|
90
|
+
`,
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* With `forcePopover`, the menu stays anchored to its trigger even on
|
|
96
|
+
* mobile viewports — the bottom-drawer behavior (slide-up, backdrop,
|
|
97
|
+
* swipe-to-dismiss) is disabled. Use it when the popover content is
|
|
98
|
+
* compact and you don't want a fullscreen-ish drawer.
|
|
99
|
+
*/
|
|
100
|
+
export const ForcePopoverOnMobile: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
forcePopover: true,
|
|
103
|
+
},
|
|
104
|
+
render: (args) => ({
|
|
105
|
+
components: { CpButton, CpMenu },
|
|
106
|
+
setup() {
|
|
107
|
+
const items = [
|
|
108
|
+
{ label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') },
|
|
109
|
+
{ label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') },
|
|
110
|
+
{ separator: true },
|
|
111
|
+
{ label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') },
|
|
112
|
+
]
|
|
113
|
+
return { args, items }
|
|
114
|
+
},
|
|
115
|
+
template: `
|
|
116
|
+
<CpMenu v-bind="args" :items="items">
|
|
117
|
+
<template #trigger>
|
|
118
|
+
<CpButton>Open menu</CpButton>
|
|
119
|
+
</template>
|
|
120
|
+
</CpMenu>
|
|
121
|
+
`,
|
|
122
|
+
}),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Use the default slot to render arbitrary content inside the popover —
|
|
127
|
+
* useful for forms, share panels, filters, etc.
|
|
128
|
+
*/
|
|
129
|
+
export const CustomContent: Story = {
|
|
130
|
+
render: () => ({
|
|
131
|
+
components: { CpButton, CpMenu },
|
|
132
|
+
template: `
|
|
133
|
+
<CpMenu>
|
|
134
|
+
<template #trigger>
|
|
135
|
+
<CpButton>Share</CpButton>
|
|
136
|
+
</template>
|
|
137
|
+
<div style="display: flex; flex-direction: column; gap: 12px; padding: 16px; min-width: 260px;">
|
|
138
|
+
<strong>Share this document</strong>
|
|
139
|
+
<input
|
|
140
|
+
value="https://example.com/doc/abc"
|
|
141
|
+
readonly
|
|
142
|
+
style="padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px;"
|
|
143
|
+
/>
|
|
144
|
+
<CpButton>Copy link</CpButton>
|
|
145
|
+
</div>
|
|
146
|
+
</CpMenu>
|
|
147
|
+
`,
|
|
148
|
+
}),
|
|
149
|
+
}
|
|
@@ -1,83 +1,184 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
2
|
|
|
3
|
+
import type { Args, Meta, StoryObj } from '@storybook/vue3-vite'
|
|
4
|
+
|
|
5
|
+
import CpButton from '@/components/CpButton.vue'
|
|
6
|
+
import CpIcon from '@/components/CpIcon.vue'
|
|
7
|
+
import CpMenu from '@/components/CpMenu.vue'
|
|
3
8
|
import CpMenuItem from '@/components/CpMenuItem.vue'
|
|
4
9
|
|
|
5
|
-
import { docLabelStyle,
|
|
10
|
+
import { docCellStyle, docLabelStyle, docRowWrapStyle } from '@/stories/documentationStyles'
|
|
6
11
|
|
|
7
|
-
const
|
|
8
|
-
|
|
12
|
+
const menuContainerStyle =
|
|
13
|
+
'display: flex; flex-direction: column; background: var(--cp-background-primary); border: 1px solid var(--cp-border-secondary, #e5e7eb); border-radius: 8px; padding: 4px 0; min-width: 240px;'
|
|
9
14
|
|
|
10
15
|
const meta = {
|
|
11
16
|
title: 'Atoms/CpMenuItem',
|
|
12
17
|
component: CpMenuItem,
|
|
18
|
+
parameters: {
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
'A single menu entry, typically used inside `CpMenu`. Renders a button with a leading and/or trailing icon, a label, and supports loading, critical, selected and disabled states. Sync and async commands are both supported through the `command` prop (set `isAsync` to keep the menu open and surface a loader while the promise resolves).',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
13
26
|
argTypes: {
|
|
14
27
|
label: {
|
|
15
28
|
control: 'text',
|
|
16
|
-
description: '
|
|
29
|
+
description: 'The text displayed inside the item.',
|
|
17
30
|
},
|
|
18
|
-
|
|
31
|
+
leadingIcon: {
|
|
19
32
|
control: 'text',
|
|
20
|
-
description: 'Icon name',
|
|
33
|
+
description: 'Icon name displayed before the label. Overridden by the `leading-icon` slot.',
|
|
21
34
|
},
|
|
22
|
-
|
|
23
|
-
control: '
|
|
24
|
-
description: '
|
|
35
|
+
trailingIcon: {
|
|
36
|
+
control: 'text',
|
|
37
|
+
description: 'Icon name displayed after the label. Overridden by the `trailing-icon` slot.',
|
|
38
|
+
},
|
|
39
|
+
tooltip: {
|
|
40
|
+
control: 'text',
|
|
41
|
+
description: 'Optional tooltip text shown on hover/focus of the label.',
|
|
25
42
|
},
|
|
26
43
|
isCritical: {
|
|
27
44
|
control: 'boolean',
|
|
28
|
-
description: '
|
|
45
|
+
description: 'Render the item with a destructive (red) styling.',
|
|
29
46
|
},
|
|
30
47
|
isLoading: {
|
|
31
48
|
control: 'boolean',
|
|
32
|
-
description: '
|
|
49
|
+
description: 'Show a loader in place of the leading icon and disable the item.',
|
|
33
50
|
},
|
|
34
|
-
|
|
51
|
+
isSelected: {
|
|
35
52
|
control: 'boolean',
|
|
36
|
-
description: '
|
|
53
|
+
description: 'Initial selected state. Toggled internally when clicked.',
|
|
37
54
|
},
|
|
38
|
-
|
|
55
|
+
isAsync: {
|
|
39
56
|
control: 'boolean',
|
|
40
|
-
description:
|
|
57
|
+
description:
|
|
58
|
+
'When the `command` returns a promise, set this to keep the parent menu open and emit `onAsyncCommandComplete` once it resolves.',
|
|
41
59
|
},
|
|
42
|
-
|
|
60
|
+
disabled: {
|
|
43
61
|
control: 'boolean',
|
|
44
|
-
description: '
|
|
45
|
-
},
|
|
46
|
-
tooltip: {
|
|
47
|
-
control: 'text',
|
|
48
|
-
description: 'Tooltip text',
|
|
62
|
+
description: 'Disable interactions and apply a muted style.',
|
|
49
63
|
},
|
|
50
64
|
},
|
|
51
|
-
|
|
65
|
+
decorators: [
|
|
66
|
+
() => ({
|
|
67
|
+
template: '<div style="padding: 16px;"><story /></div>',
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
52
70
|
} satisfies Meta<typeof CpMenuItem>
|
|
53
71
|
|
|
54
72
|
export default meta
|
|
73
|
+
|
|
55
74
|
type Story = StoryObj<typeof meta>
|
|
56
75
|
|
|
76
|
+
const defaultRender = (args: Args) => ({
|
|
77
|
+
components: { CpMenuItem },
|
|
78
|
+
setup() {
|
|
79
|
+
return { args, menuContainerStyle }
|
|
80
|
+
},
|
|
81
|
+
template: `
|
|
82
|
+
<div :style="menuContainerStyle">
|
|
83
|
+
<CpMenuItem v-bind="args" />
|
|
84
|
+
</div>
|
|
85
|
+
`,
|
|
86
|
+
})
|
|
87
|
+
|
|
57
88
|
/**
|
|
58
|
-
* Default
|
|
89
|
+
* Default item: a label with a leading icon. Click it to see the selected
|
|
90
|
+
* state toggle briefly before resetting.
|
|
59
91
|
*/
|
|
60
92
|
export const Default: Story = {
|
|
61
93
|
args: {
|
|
62
|
-
label: '
|
|
63
|
-
|
|
64
|
-
isDisabled: false,
|
|
94
|
+
label: 'Menu item',
|
|
95
|
+
leadingIcon: 'edit',
|
|
65
96
|
isCritical: false,
|
|
66
97
|
isLoading: false,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
hideLabel: false,
|
|
70
|
-
tooltip: '',
|
|
71
|
-
command: () => alert('Edit clicked'),
|
|
98
|
+
isSelected: false,
|
|
99
|
+
disabled: false,
|
|
72
100
|
},
|
|
73
|
-
render:
|
|
101
|
+
render: defaultRender,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* -------------------------------------------------------------------------- */
|
|
105
|
+
/* Icons */
|
|
106
|
+
/* -------------------------------------------------------------------------- */
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Different icon configurations: leading only, trailing only, both or none.
|
|
110
|
+
*/
|
|
111
|
+
export const Icons: Story = {
|
|
112
|
+
parameters: { controls: { disable: true } },
|
|
113
|
+
render: () => ({
|
|
74
114
|
components: { CpMenuItem },
|
|
75
115
|
setup() {
|
|
76
|
-
return {
|
|
116
|
+
return { docCellStyle, docLabelStyle, docRowWrapStyle, menuContainerStyle }
|
|
77
117
|
},
|
|
78
118
|
template: `
|
|
79
|
-
<div style="
|
|
80
|
-
<
|
|
119
|
+
<div :style="docRowWrapStyle">
|
|
120
|
+
<div :style="docCellStyle">
|
|
121
|
+
<span :style="docLabelStyle">Leading</span>
|
|
122
|
+
<div :style="menuContainerStyle">
|
|
123
|
+
<CpMenuItem label="Edit" leading-icon="edit" />
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div :style="docCellStyle">
|
|
127
|
+
<span :style="docLabelStyle">Trailing</span>
|
|
128
|
+
<div :style="menuContainerStyle">
|
|
129
|
+
<CpMenuItem label="Open" trailing-icon="external-link" />
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div :style="docCellStyle">
|
|
133
|
+
<span :style="docLabelStyle">Both</span>
|
|
134
|
+
<div :style="menuContainerStyle">
|
|
135
|
+
<CpMenuItem label="Settings" leading-icon="settings" trailing-icon="chevron-right" />
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div :style="docCellStyle">
|
|
139
|
+
<span :style="docLabelStyle">No icon</span>
|
|
140
|
+
<div :style="menuContainerStyle">
|
|
141
|
+
<CpMenuItem label="Plain item" />
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
`,
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The `leading-icon` and `trailing-icon` slots let you render arbitrary
|
|
151
|
+
* content (custom icons, badges, indicators...) instead of the default
|
|
152
|
+
* `CpIcon` rendered from the `leadingIcon` / `trailingIcon` props.
|
|
153
|
+
*/
|
|
154
|
+
export const CustomIconSlots: Story = {
|
|
155
|
+
parameters: { controls: { disable: true } },
|
|
156
|
+
render: () => ({
|
|
157
|
+
components: { CpMenuItem, CpIcon },
|
|
158
|
+
setup() {
|
|
159
|
+
return { menuContainerStyle }
|
|
160
|
+
},
|
|
161
|
+
template: `
|
|
162
|
+
<div :style="menuContainerStyle">
|
|
163
|
+
<CpMenuItem label="With custom slots">
|
|
164
|
+
<template #leading-icon>
|
|
165
|
+
<span style="
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
width: 16px;
|
|
170
|
+
height: 16px;
|
|
171
|
+
border-radius: 999px;
|
|
172
|
+
background: var(--cp-background-accent-solid, #2563eb);
|
|
173
|
+
color: white;
|
|
174
|
+
font-size: 10px;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
">A</span>
|
|
177
|
+
</template>
|
|
178
|
+
<template #trailing-icon>
|
|
179
|
+
<span style="font-size: 11px; color: var(--cp-foreground-secondary);">⌘K</span>
|
|
180
|
+
</template>
|
|
181
|
+
</CpMenuItem>
|
|
81
182
|
</div>
|
|
82
183
|
`,
|
|
83
184
|
}),
|
|
@@ -88,43 +189,216 @@ export const Default: Story = {
|
|
|
88
189
|
/* -------------------------------------------------------------------------- */
|
|
89
190
|
|
|
90
191
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
192
|
+
* The interactive states side by side: default, plus the explicit `disabled`,
|
|
193
|
+
* `isLoading`, `isSelected` and `isCritical` states. Hover/focus is shown by
|
|
194
|
+
* interacting with each item.
|
|
93
195
|
*/
|
|
94
196
|
export const States: Story = {
|
|
95
197
|
parameters: { controls: { disable: true } },
|
|
96
198
|
render: () => ({
|
|
97
199
|
components: { CpMenuItem },
|
|
98
200
|
setup() {
|
|
99
|
-
return {
|
|
201
|
+
return { docCellStyle, docLabelStyle, docRowWrapStyle, menuContainerStyle }
|
|
100
202
|
},
|
|
101
203
|
template: `
|
|
102
|
-
<div :style="
|
|
103
|
-
<div :style="
|
|
204
|
+
<div :style="docRowWrapStyle">
|
|
205
|
+
<div :style="docCellStyle">
|
|
104
206
|
<span :style="docLabelStyle">Default</span>
|
|
105
|
-
<div :style="
|
|
106
|
-
<CpMenuItem label="
|
|
207
|
+
<div :style="menuContainerStyle">
|
|
208
|
+
<CpMenuItem label="Default" leading-icon="edit" />
|
|
107
209
|
</div>
|
|
108
210
|
</div>
|
|
109
|
-
<div :style="
|
|
110
|
-
<span :style="docLabelStyle">
|
|
111
|
-
<div :style="
|
|
112
|
-
<CpMenuItem label="
|
|
211
|
+
<div :style="docCellStyle">
|
|
212
|
+
<span :style="docLabelStyle">Disabled</span>
|
|
213
|
+
<div :style="menuContainerStyle">
|
|
214
|
+
<CpMenuItem label="Disabled" leading-icon="edit" :disabled="true" />
|
|
113
215
|
</div>
|
|
114
216
|
</div>
|
|
115
|
-
<div :style="
|
|
116
|
-
<span :style="docLabelStyle">
|
|
117
|
-
<div :style="
|
|
118
|
-
<CpMenuItem label="
|
|
217
|
+
<div :style="docCellStyle">
|
|
218
|
+
<span :style="docLabelStyle">Critical</span>
|
|
219
|
+
<div :style="menuContainerStyle">
|
|
220
|
+
<CpMenuItem label="Delete" leading-icon="trash-2" :is-critical="true" />
|
|
119
221
|
</div>
|
|
120
222
|
</div>
|
|
121
|
-
<div :style="
|
|
223
|
+
<div :style="docCellStyle">
|
|
122
224
|
<span :style="docLabelStyle">Loading</span>
|
|
123
|
-
<div :style="
|
|
124
|
-
<CpMenuItem label="
|
|
225
|
+
<div :style="menuContainerStyle">
|
|
226
|
+
<CpMenuItem label="Loading" leading-icon="edit" :is-loading="true" />
|
|
125
227
|
</div>
|
|
126
228
|
</div>
|
|
229
|
+
<div :style="docCellStyle">
|
|
230
|
+
<span :style="docLabelStyle">Selected</span>
|
|
231
|
+
<div :style="menuContainerStyle">
|
|
232
|
+
<CpMenuItem label="Selected" leading-icon="check" :is-selected="true" />
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
`,
|
|
237
|
+
}),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* -------------------------------------------------------------------------- */
|
|
241
|
+
/* Tooltip */
|
|
242
|
+
/* -------------------------------------------------------------------------- */
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Hover the label to see the tooltip — useful when the label is truncated or
|
|
246
|
+
* when extra context is needed without taking visual space.
|
|
247
|
+
*/
|
|
248
|
+
export const WithTooltip: Story = {
|
|
249
|
+
args: {
|
|
250
|
+
label: 'Hover me for more details',
|
|
251
|
+
leadingIcon: 'info',
|
|
252
|
+
tooltip: 'Here is some additional context shown on hover.',
|
|
253
|
+
},
|
|
254
|
+
render: defaultRender,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* -------------------------------------------------------------------------- */
|
|
258
|
+
/* Commands */
|
|
259
|
+
/* -------------------------------------------------------------------------- */
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Sync command: the `command` callback is invoked on click, then the `click`
|
|
263
|
+
* event is emitted so the parent (`CpMenu`/`CpMenuList`) can react (e.g.
|
|
264
|
+
* close the menu).
|
|
265
|
+
*/
|
|
266
|
+
export const SyncCommand: Story = {
|
|
267
|
+
parameters: { controls: { disable: true } },
|
|
268
|
+
render: () => ({
|
|
269
|
+
components: { CpMenuItem },
|
|
270
|
+
setup() {
|
|
271
|
+
const lastAction = ref<string>('—')
|
|
272
|
+
const onCommand = () => {
|
|
273
|
+
lastAction.value = `Clicked at ${new Date().toLocaleTimeString()}`
|
|
274
|
+
}
|
|
275
|
+
return { lastAction, onCommand, menuContainerStyle }
|
|
276
|
+
},
|
|
277
|
+
template: `
|
|
278
|
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
|
279
|
+
<div :style="menuContainerStyle">
|
|
280
|
+
<CpMenuItem label="Run command" leading-icon="zap" :command="onCommand" />
|
|
281
|
+
</div>
|
|
282
|
+
<span style="font-size: 12px; color: var(--cp-foreground-secondary);">Last action: {{ lastAction }}</span>
|
|
127
283
|
</div>
|
|
128
284
|
`,
|
|
129
285
|
}),
|
|
130
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Async command: combine `isAsync` with a returning promise to display a
|
|
290
|
+
* loader while the command resolves. The component automatically toggles
|
|
291
|
+
* `isLoading` from outside in this example. `onAsyncCommandComplete` is
|
|
292
|
+
* emitted once the promise settles.
|
|
293
|
+
*/
|
|
294
|
+
export const AsyncCommand: Story = {
|
|
295
|
+
parameters: { controls: { disable: true } },
|
|
296
|
+
render: () => ({
|
|
297
|
+
components: { CpMenuItem },
|
|
298
|
+
setup() {
|
|
299
|
+
const isLoading = ref(false)
|
|
300
|
+
const completedCount = ref(0)
|
|
301
|
+
|
|
302
|
+
const onCommand = async () => {
|
|
303
|
+
isLoading.value = true
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
305
|
+
isLoading.value = false
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const onAsyncCommandComplete = () => {
|
|
309
|
+
completedCount.value += 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { isLoading, completedCount, onCommand, onAsyncCommandComplete, menuContainerStyle }
|
|
313
|
+
},
|
|
314
|
+
template: `
|
|
315
|
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
|
316
|
+
<div :style="menuContainerStyle">
|
|
317
|
+
<CpMenuItem
|
|
318
|
+
label="Download report"
|
|
319
|
+
leading-icon="download"
|
|
320
|
+
:is-async="true"
|
|
321
|
+
:is-loading="isLoading"
|
|
322
|
+
:command="onCommand"
|
|
323
|
+
@on-async-command-complete="onAsyncCommandComplete"
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
<span style="font-size: 12px; color: var(--cp-foreground-secondary);">
|
|
327
|
+
Completed {{ completedCount }} time(s)
|
|
328
|
+
</span>
|
|
329
|
+
</div>
|
|
330
|
+
`,
|
|
331
|
+
}),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* -------------------------------------------------------------------------- */
|
|
335
|
+
/* Composition */
|
|
336
|
+
/* -------------------------------------------------------------------------- */
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Several `CpMenuItem`s stacked together to mimic a typical menu layout.
|
|
340
|
+
* In real apps this is usually wrapped by `CpMenu` / `CpMenuList`, but the
|
|
341
|
+
* primitive composes on its own too.
|
|
342
|
+
*/
|
|
343
|
+
export const InCpMenu: Story = {
|
|
344
|
+
render: () => ({
|
|
345
|
+
components: { CpButton, CpMenu },
|
|
346
|
+
setup() {
|
|
347
|
+
const isLoading = ref(false)
|
|
348
|
+
|
|
349
|
+
const onClick = () => alert('Clicked')
|
|
350
|
+
|
|
351
|
+
const items = computed(() => [
|
|
352
|
+
{
|
|
353
|
+
label: 'Sync',
|
|
354
|
+
leadingIcon: 'edit',
|
|
355
|
+
command: onClick,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
label: 'Async',
|
|
359
|
+
leadingIcon: 'download',
|
|
360
|
+
isLoading: isLoading.value,
|
|
361
|
+
isAsync: true,
|
|
362
|
+
command: async () => {
|
|
363
|
+
isLoading.value = true
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
365
|
+
isLoading.value = false
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{ separator: true },
|
|
369
|
+
{
|
|
370
|
+
label: 'Critical',
|
|
371
|
+
leadingIcon: 'trash-2',
|
|
372
|
+
isCritical: true,
|
|
373
|
+
command: onClick,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
label: 'Disabled',
|
|
377
|
+
leadingIcon: 'edit',
|
|
378
|
+
disabled: true,
|
|
379
|
+
command: onClick,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
label: 'Selected',
|
|
383
|
+
leadingIcon: 'check',
|
|
384
|
+
isSelected: true,
|
|
385
|
+
command: onClick,
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
label: 'Loading',
|
|
389
|
+
isLoading: true,
|
|
390
|
+
command: onClick,
|
|
391
|
+
},
|
|
392
|
+
])
|
|
393
|
+
|
|
394
|
+
return { items }
|
|
395
|
+
},
|
|
396
|
+
template: `
|
|
397
|
+
<CpMenu :items="items" class="defaultMenu">
|
|
398
|
+
<template #trigger>
|
|
399
|
+
<CpButton>Open menu</CpButton>
|
|
400
|
+
</template>
|
|
401
|
+
</CpMenu>
|
|
402
|
+
`,
|
|
403
|
+
}),
|
|
404
|
+
}
|