@citizenplane/pimp 18.24.0 → 18.25.1
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/CpDynamicMenu.vue.d.ts +51 -0
- package/dist/components/CpDynamicMenu.vue.d.ts.map +1 -0
- package/dist/components/CpLanguageSwitcher.vue.d.ts +35 -0
- package/dist/components/CpLanguageSwitcher.vue.d.ts.map +1 -0
- package/dist/components/CpMenu.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/pimp.es.js +1204 -1005
- package/dist/pimp.umd.js +36 -36
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/CpDynamicMenu.vue +226 -0
- package/src/components/CpLanguageSwitcher.vue +151 -0
- package/src/components/CpMenu.vue +36 -8
- package/src/components/index.ts +7 -0
- package/src/stories/CpDynamicMenu.stories.ts +279 -0
- package/src/stories/CpLanguageSwitcher.stories.ts +196 -0
- package/src/stories/CpMenu.stories.ts +37 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
|
|
3
|
+
import CpButton from '@/components/CpButton.vue'
|
|
4
|
+
import CpDynamicMenu from '@/components/CpDynamicMenu.vue'
|
|
5
|
+
|
|
6
|
+
const STORY_TRIGGER_LAYOUT =
|
|
7
|
+
'display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; width: 100%;'
|
|
8
|
+
|
|
9
|
+
const MENU_ITEMS = [
|
|
10
|
+
{ label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') },
|
|
11
|
+
{ label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') },
|
|
12
|
+
{ label: 'Move', leadingIcon: 'arrow-right-left', command: () => alert('Move clicked') },
|
|
13
|
+
{ separator: true },
|
|
14
|
+
{ label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const meta = {
|
|
18
|
+
title: 'Organisms/CpDynamicMenu',
|
|
19
|
+
component: CpDynamicMenu,
|
|
20
|
+
parameters: {
|
|
21
|
+
layout: 'fullscreen',
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
'Responsive menu wrapper that renders `CpMenu` as a popover anchored to the trigger on desktop, and `CpBottomSheet` on mobile viewports. The same `trigger` slot drives both branches, and `items` are rendered through `CpMenuList` in either surface. The component is imperative like `CpMenu` — there is no `v-model`; use the exposed `show(event)` / `hide()` / `toggle(event)` methods, and listen to `show` / `hide`.',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
argTypes: {
|
|
30
|
+
breakpoint: {
|
|
31
|
+
control: 'number',
|
|
32
|
+
description:
|
|
33
|
+
'Viewport width (px) below which the menu switches to a bottom sheet. Defaults to Breakpoints.MOBILE (552)',
|
|
34
|
+
},
|
|
35
|
+
items: {
|
|
36
|
+
control: 'object',
|
|
37
|
+
description: 'The menu items to display. Each item is a PrimeVue `MenuItem`.',
|
|
38
|
+
table: {
|
|
39
|
+
type: { summary: 'MenuItem[]' },
|
|
40
|
+
defaultValue: { summary: '[]' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
keepOpenOnClick: {
|
|
44
|
+
control: 'boolean',
|
|
45
|
+
description: 'Keep the menu or the bottom sheet open when clicking an item.',
|
|
46
|
+
table: { defaultValue: { summary: 'false' } },
|
|
47
|
+
},
|
|
48
|
+
placement: {
|
|
49
|
+
control: 'select',
|
|
50
|
+
options: ['start', 'end'],
|
|
51
|
+
description: 'Horizontal alignment of the desktop popover relative to the trigger. Ignored on mobile.',
|
|
52
|
+
table: { defaultValue: { summary: 'start' } },
|
|
53
|
+
},
|
|
54
|
+
sheetTitle: {
|
|
55
|
+
control: 'text',
|
|
56
|
+
description:
|
|
57
|
+
'Rendered in the bottom sheet header on mobile. Overridable with the `sheet-title` slot; with neither, the header collapses.',
|
|
58
|
+
},
|
|
59
|
+
preventClose: {
|
|
60
|
+
control: 'boolean',
|
|
61
|
+
description: 'Block backdrop, swipe and Escape dismissal of the mobile bottom sheet.',
|
|
62
|
+
},
|
|
63
|
+
onShow: { action: 'show' },
|
|
64
|
+
onHide: { action: 'hide' },
|
|
65
|
+
},
|
|
66
|
+
} satisfies Meta<typeof CpDynamicMenu>
|
|
67
|
+
|
|
68
|
+
export default meta
|
|
69
|
+
type Story = StoryObj<typeof meta>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resize the viewport to compare both branches: above the breakpoint the items
|
|
73
|
+
* render in a popover anchored to the trigger, below it in a bottom sheet.
|
|
74
|
+
*/
|
|
75
|
+
export const Default: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
items: MENU_ITEMS,
|
|
78
|
+
sheetTitle: 'Actions',
|
|
79
|
+
},
|
|
80
|
+
render: (args) => ({
|
|
81
|
+
components: { CpButton, CpDynamicMenu },
|
|
82
|
+
setup() {
|
|
83
|
+
return { args }
|
|
84
|
+
},
|
|
85
|
+
template: `
|
|
86
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
87
|
+
<CpDynamicMenu v-bind="args">
|
|
88
|
+
<template #trigger>
|
|
89
|
+
<CpButton>Open menu</CpButton>
|
|
90
|
+
</template>
|
|
91
|
+
</CpDynamicMenu>
|
|
92
|
+
<p style="margin-top: 8px; text-align: center;">Resize me</p>
|
|
93
|
+
</div>
|
|
94
|
+
`,
|
|
95
|
+
}),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Mobile viewport — the same API renders a bottom sheet with the `sheetTitle` in
|
|
100
|
+
* its header, closing on item click, backdrop, swipe or Escape.
|
|
101
|
+
*/
|
|
102
|
+
export const Mobile: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
items: MENU_ITEMS,
|
|
105
|
+
sheetTitle: 'Actions',
|
|
106
|
+
},
|
|
107
|
+
globals: {
|
|
108
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
109
|
+
},
|
|
110
|
+
render: (args) => ({
|
|
111
|
+
components: { CpButton, CpDynamicMenu },
|
|
112
|
+
setup() {
|
|
113
|
+
return { args }
|
|
114
|
+
},
|
|
115
|
+
template: `
|
|
116
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
117
|
+
<CpDynamicMenu v-bind="args">
|
|
118
|
+
<template #trigger>
|
|
119
|
+
<CpButton>Open menu</CpButton>
|
|
120
|
+
</template>
|
|
121
|
+
</CpDynamicMenu>
|
|
122
|
+
</div>
|
|
123
|
+
`,
|
|
124
|
+
}),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mobile viewport — the `sheet-title` slot replaces the `sheetTitle` heading
|
|
129
|
+
* when the header needs more than a string.
|
|
130
|
+
*/
|
|
131
|
+
export const SheetTitleSlot: Story = {
|
|
132
|
+
args: {
|
|
133
|
+
items: MENU_ITEMS,
|
|
134
|
+
sheetTitle: 'Ignored while the slot is provided',
|
|
135
|
+
},
|
|
136
|
+
globals: {
|
|
137
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
138
|
+
},
|
|
139
|
+
render: (args) => ({
|
|
140
|
+
components: { CpButton, CpDynamicMenu },
|
|
141
|
+
setup() {
|
|
142
|
+
return { args }
|
|
143
|
+
},
|
|
144
|
+
template: `
|
|
145
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
146
|
+
<CpDynamicMenu v-bind="args">
|
|
147
|
+
<template #trigger>
|
|
148
|
+
<CpButton>Open menu</CpButton>
|
|
149
|
+
</template>
|
|
150
|
+
<template #sheet-title>
|
|
151
|
+
<CpHeading heading-level="h4">Custom header</CpHeading>
|
|
152
|
+
</template>
|
|
153
|
+
</CpDynamicMenu>
|
|
154
|
+
</div>
|
|
155
|
+
`,
|
|
156
|
+
}),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Without `items`, the default slot is rendered instead — inside the popover on
|
|
161
|
+
* desktop, inside the bottom sheet content on mobile. The `footer` slot is only
|
|
162
|
+
* rendered by the mobile bottom sheet. This content carries its own heading, so
|
|
163
|
+
* no `sheetTitle` is passed and the sheet header collapses.
|
|
164
|
+
*/
|
|
165
|
+
export const CustomContent: Story = {
|
|
166
|
+
render: (args) => ({
|
|
167
|
+
components: { CpButton, CpDynamicMenu },
|
|
168
|
+
setup() {
|
|
169
|
+
return { args }
|
|
170
|
+
},
|
|
171
|
+
template: `
|
|
172
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
173
|
+
<CpDynamicMenu v-bind="args">
|
|
174
|
+
<template #trigger>
|
|
175
|
+
<CpButton>Share</CpButton>
|
|
176
|
+
</template>
|
|
177
|
+
<div style="display: flex; flex-direction: column; gap: 12px; padding: 16px; min-width: 260px;">
|
|
178
|
+
<strong>Share this document</strong>
|
|
179
|
+
<input
|
|
180
|
+
value="https://example.com/doc/abc"
|
|
181
|
+
readonly
|
|
182
|
+
style="padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px;"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
<template #footer>
|
|
186
|
+
<CpButton>Copy link</CpButton>
|
|
187
|
+
</template>
|
|
188
|
+
</CpDynamicMenu>
|
|
189
|
+
</div>
|
|
190
|
+
`,
|
|
191
|
+
}),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Mobile viewport — with neither a `sheetTitle` nor a `sheet-title` slot, the bottom
|
|
196
|
+
* sheet header keeps its drag handle but drops its bottom padding, so the items
|
|
197
|
+
* start right under the handle.
|
|
198
|
+
*/
|
|
199
|
+
export const NoTitle: Story = {
|
|
200
|
+
args: {
|
|
201
|
+
items: MENU_ITEMS,
|
|
202
|
+
},
|
|
203
|
+
globals: {
|
|
204
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
205
|
+
},
|
|
206
|
+
render: (args) => ({
|
|
207
|
+
components: { CpButton, CpDynamicMenu },
|
|
208
|
+
setup() {
|
|
209
|
+
return { args }
|
|
210
|
+
},
|
|
211
|
+
template: `
|
|
212
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
213
|
+
<CpDynamicMenu v-bind="args">
|
|
214
|
+
<template #trigger>
|
|
215
|
+
<CpButton>Open menu</CpButton>
|
|
216
|
+
</template>
|
|
217
|
+
</CpDynamicMenu>
|
|
218
|
+
</div>
|
|
219
|
+
`,
|
|
220
|
+
}),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Use `placement` to align the desktop popover's edge to the matching trigger
|
|
225
|
+
* edge. The trigger is centered here so the difference is easy to see, and the
|
|
226
|
+
* prop has no effect once the bottom sheet takes over.
|
|
227
|
+
*/
|
|
228
|
+
export const PlacementEnd: Story = {
|
|
229
|
+
args: {
|
|
230
|
+
items: MENU_ITEMS,
|
|
231
|
+
placement: 'end',
|
|
232
|
+
sheetTitle: 'Actions',
|
|
233
|
+
},
|
|
234
|
+
render: (args) => ({
|
|
235
|
+
components: { CpButton, CpDynamicMenu },
|
|
236
|
+
setup() {
|
|
237
|
+
return { args }
|
|
238
|
+
},
|
|
239
|
+
template: `
|
|
240
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
241
|
+
<CpDynamicMenu v-bind="args">
|
|
242
|
+
<template #trigger>
|
|
243
|
+
<CpButton>Open menu</CpButton>
|
|
244
|
+
</template>
|
|
245
|
+
</CpDynamicMenu>
|
|
246
|
+
</div>
|
|
247
|
+
`,
|
|
248
|
+
}),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Mobile viewport — with `preventClose`, the bottom sheet ignores backdrop,
|
|
253
|
+
* swipe and Escape dismissal. Only an item click closes it.
|
|
254
|
+
*/
|
|
255
|
+
export const PreventClose: Story = {
|
|
256
|
+
args: {
|
|
257
|
+
items: MENU_ITEMS,
|
|
258
|
+
preventClose: true,
|
|
259
|
+
sheetTitle: 'Actions',
|
|
260
|
+
},
|
|
261
|
+
globals: {
|
|
262
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
263
|
+
},
|
|
264
|
+
render: (args) => ({
|
|
265
|
+
components: { CpButton, CpDynamicMenu },
|
|
266
|
+
setup() {
|
|
267
|
+
return { args }
|
|
268
|
+
},
|
|
269
|
+
template: `
|
|
270
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
271
|
+
<CpDynamicMenu v-bind="args">
|
|
272
|
+
<template #trigger>
|
|
273
|
+
<CpButton>Open menu</CpButton>
|
|
274
|
+
</template>
|
|
275
|
+
</CpDynamicMenu>
|
|
276
|
+
</div>
|
|
277
|
+
`,
|
|
278
|
+
}),
|
|
279
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import CpButton from '@/components/CpButton.vue'
|
|
5
|
+
import CpLanguageSwitcher from '@/components/CpLanguageSwitcher.vue'
|
|
6
|
+
|
|
7
|
+
const STORY_TRIGGER_LAYOUT =
|
|
8
|
+
'display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; width: 100%;'
|
|
9
|
+
|
|
10
|
+
const STORY_EDGE_TRIGGER_LAYOUT =
|
|
11
|
+
'display: flex; align-items: center; justify-content: flex-end; min-height: 60vh; width: 100%; padding: 0 16px;'
|
|
12
|
+
|
|
13
|
+
const LANGS_WITH_FLAGS = [
|
|
14
|
+
{ code: 'fr', name: 'Français', flag: 'fr' },
|
|
15
|
+
{ code: 'en', name: 'English', flag: 'gb' },
|
|
16
|
+
{ code: 'es', name: 'Español', flag: 'es' },
|
|
17
|
+
{ code: 'pt', name: 'Português', flag: 'pt' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const LANGS_WITHOUT_FLAGS = [
|
|
21
|
+
{ code: 'fr', name: 'Français' },
|
|
22
|
+
{ code: 'en', name: 'English' },
|
|
23
|
+
{ code: 'es', name: 'Español' },
|
|
24
|
+
{ code: 'pt', name: 'Português' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const findLanguageName = (code: string | undefined) => {
|
|
28
|
+
const lang = LANGS_WITH_FLAGS.find((entry) => entry.code === code)
|
|
29
|
+
if (!lang) return ''
|
|
30
|
+
return lang.name
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const meta = {
|
|
34
|
+
title: 'Organisms/CpLanguageSwitcher',
|
|
35
|
+
component: CpLanguageSwitcher,
|
|
36
|
+
parameters: {
|
|
37
|
+
layout: 'fullscreen',
|
|
38
|
+
docs: {
|
|
39
|
+
description: {
|
|
40
|
+
component:
|
|
41
|
+
'Language picker built on `CpDynamicMenu`: a popover anchored to the trigger on desktop, a titleless bottom sheet on mobile. `v-model` holds the current locale **code**; picking an option sets it and closes the menu. The component never calls an i18n API — the consumer watches the model and runs its own `setLocale` / `switchLocalePath`. Flags are rendered with `flag-icons` classes, which pimp does not ship at runtime: the consumer app loads `flag-icons/css/flag-icons.min.css` itself (Storybook loads it in `preview.ts`).',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
argTypes: {
|
|
46
|
+
langs: {
|
|
47
|
+
control: 'object',
|
|
48
|
+
description: 'Languages to list, rendered in the order given. The component never sorts them.',
|
|
49
|
+
table: { type: { summary: 'LanguageOption[]' } },
|
|
50
|
+
},
|
|
51
|
+
modelValue: {
|
|
52
|
+
control: 'text',
|
|
53
|
+
description: 'The current language code. Selecting an option sets it, then closes the menu.',
|
|
54
|
+
},
|
|
55
|
+
breakpoint: {
|
|
56
|
+
control: 'number',
|
|
57
|
+
description:
|
|
58
|
+
'Viewport width (px) below which the picker switches to a bottom sheet. Defaults to Breakpoints.MOBILE (552)',
|
|
59
|
+
},
|
|
60
|
+
placement: {
|
|
61
|
+
control: 'select',
|
|
62
|
+
options: ['start', 'end'],
|
|
63
|
+
description: 'Horizontal alignment of the desktop popover relative to the trigger. Ignored on mobile.',
|
|
64
|
+
table: { defaultValue: { summary: 'start' } },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
} satisfies Meta<typeof CpLanguageSwitcher>
|
|
68
|
+
|
|
69
|
+
export default meta
|
|
70
|
+
type Story = StoryObj<typeof meta>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The default trigger is the header button from online-checkin: a tertiary accent
|
|
74
|
+
* button with a globe icon and the current code uppercased, so it follows the
|
|
75
|
+
* selection.
|
|
76
|
+
*/
|
|
77
|
+
export const Default: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
langs: LANGS_WITH_FLAGS,
|
|
80
|
+
modelValue: 'fr',
|
|
81
|
+
},
|
|
82
|
+
render: (args) => ({
|
|
83
|
+
components: { CpLanguageSwitcher },
|
|
84
|
+
setup() {
|
|
85
|
+
const currentCode = ref(args.modelValue)
|
|
86
|
+
return { args, currentCode }
|
|
87
|
+
},
|
|
88
|
+
template: `
|
|
89
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
90
|
+
<CpLanguageSwitcher v-bind="args" v-model="currentCode" />
|
|
91
|
+
<p style="margin-top: 8px; text-align: center;">Current code: {{ currentCode }}</p>
|
|
92
|
+
</div>
|
|
93
|
+
`,
|
|
94
|
+
}),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Without a `flag` on an option, only the name is rendered — no placeholder box.
|
|
99
|
+
*/
|
|
100
|
+
export const WithoutFlags: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
langs: LANGS_WITHOUT_FLAGS,
|
|
103
|
+
modelValue: 'en',
|
|
104
|
+
},
|
|
105
|
+
render: (args) => ({
|
|
106
|
+
components: { CpLanguageSwitcher },
|
|
107
|
+
setup() {
|
|
108
|
+
const currentCode = ref(args.modelValue)
|
|
109
|
+
return { args, currentCode }
|
|
110
|
+
},
|
|
111
|
+
template: `
|
|
112
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
113
|
+
<CpLanguageSwitcher v-bind="args" v-model="currentCode" />
|
|
114
|
+
</div>
|
|
115
|
+
`,
|
|
116
|
+
}),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `placement="end"` with the trigger pinned to the right edge of the canvas, the navbar
|
|
121
|
+
* case: PrimeVue's collision handling already right-aligns the popover there, so the
|
|
122
|
+
* popover's right edge must sit flush with the trigger's, not shifted left.
|
|
123
|
+
*/
|
|
124
|
+
export const PlacementEndAtViewportEdge: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
langs: LANGS_WITH_FLAGS,
|
|
127
|
+
modelValue: 'fr',
|
|
128
|
+
placement: 'end',
|
|
129
|
+
},
|
|
130
|
+
render: (args) => ({
|
|
131
|
+
components: { CpLanguageSwitcher },
|
|
132
|
+
setup() {
|
|
133
|
+
const currentCode = ref(args.modelValue)
|
|
134
|
+
return { args, currentCode }
|
|
135
|
+
},
|
|
136
|
+
template: `
|
|
137
|
+
<div style="${STORY_EDGE_TRIGGER_LAYOUT}">
|
|
138
|
+
<CpLanguageSwitcher v-bind="args" v-model="currentCode" />
|
|
139
|
+
</div>
|
|
140
|
+
`,
|
|
141
|
+
}),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mobile viewport — the list moves into a bottom sheet. No `sheetTitle` is passed,
|
|
146
|
+
* so the header collapses to its drag handle and the languages start right under it.
|
|
147
|
+
*/
|
|
148
|
+
export const Mobile: Story = {
|
|
149
|
+
args: {
|
|
150
|
+
langs: LANGS_WITH_FLAGS,
|
|
151
|
+
modelValue: 'es',
|
|
152
|
+
},
|
|
153
|
+
globals: {
|
|
154
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
155
|
+
},
|
|
156
|
+
render: (args) => ({
|
|
157
|
+
components: { CpLanguageSwitcher },
|
|
158
|
+
setup() {
|
|
159
|
+
const currentCode = ref(args.modelValue)
|
|
160
|
+
return { args, currentCode }
|
|
161
|
+
},
|
|
162
|
+
template: `
|
|
163
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
164
|
+
<CpLanguageSwitcher v-bind="args" v-model="currentCode" />
|
|
165
|
+
</div>
|
|
166
|
+
`,
|
|
167
|
+
}),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The `trigger` slot replaces the default button entirely — here it shows the
|
|
172
|
+
* current language name instead of its code.
|
|
173
|
+
*/
|
|
174
|
+
export const CustomTrigger: Story = {
|
|
175
|
+
args: {
|
|
176
|
+
langs: LANGS_WITH_FLAGS,
|
|
177
|
+
modelValue: 'pt',
|
|
178
|
+
},
|
|
179
|
+
render: (args) => ({
|
|
180
|
+
components: { CpButton, CpLanguageSwitcher },
|
|
181
|
+
setup() {
|
|
182
|
+
const currentCode = ref(args.modelValue)
|
|
183
|
+
const currentName = computed(() => findLanguageName(currentCode.value))
|
|
184
|
+
return { args, currentCode, currentName }
|
|
185
|
+
},
|
|
186
|
+
template: `
|
|
187
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
188
|
+
<CpLanguageSwitcher v-bind="args" v-model="currentCode">
|
|
189
|
+
<template #trigger>
|
|
190
|
+
<CpButton appearance="secondary" color="neutral" size="sm">{{ currentName }}</CpButton>
|
|
191
|
+
</template>
|
|
192
|
+
</CpLanguageSwitcher>
|
|
193
|
+
</div>
|
|
194
|
+
`,
|
|
195
|
+
}),
|
|
196
|
+
}
|
|
@@ -5,7 +5,7 @@ import CpButton from '@/components/CpButton.vue'
|
|
|
5
5
|
import CpMenu from '@/components/CpMenu.vue'
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
|
-
title: '
|
|
8
|
+
title: 'Organisms/CpMenu',
|
|
9
9
|
component: CpMenu,
|
|
10
10
|
parameters: {
|
|
11
11
|
docs: {
|
|
@@ -208,6 +208,42 @@ export const Placement: Story = {
|
|
|
208
208
|
}),
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The same `placement="end"` menu with its trigger pinned to the right edge of the
|
|
213
|
+
* canvas, where PrimeVue's collision handling already right-aligns the popover on the
|
|
214
|
+
* trigger. The popover's right edge must sit flush with the trigger's, not shifted left.
|
|
215
|
+
*/
|
|
216
|
+
export const PlacementEndAtViewportEdge: Story = {
|
|
217
|
+
args: {
|
|
218
|
+
forcePopover: true,
|
|
219
|
+
placement: 'end',
|
|
220
|
+
},
|
|
221
|
+
parameters: {
|
|
222
|
+
layout: 'fullscreen',
|
|
223
|
+
},
|
|
224
|
+
render: (args) => ({
|
|
225
|
+
components: { CpButton, CpMenu },
|
|
226
|
+
setup() {
|
|
227
|
+
const items = [
|
|
228
|
+
{ label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') },
|
|
229
|
+
{ label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') },
|
|
230
|
+
{ separator: true },
|
|
231
|
+
{ label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') },
|
|
232
|
+
]
|
|
233
|
+
return { args, items }
|
|
234
|
+
},
|
|
235
|
+
template: `
|
|
236
|
+
<div style="display: flex; align-items: center; justify-content: flex-end; min-height: 320px; width: 100%;">
|
|
237
|
+
<CpMenu v-bind="args" :items="items">
|
|
238
|
+
<template #trigger>
|
|
239
|
+
<CpButton>Open menu</CpButton>
|
|
240
|
+
</template>
|
|
241
|
+
</CpMenu>
|
|
242
|
+
</div>
|
|
243
|
+
`,
|
|
244
|
+
}),
|
|
245
|
+
}
|
|
246
|
+
|
|
211
247
|
/**
|
|
212
248
|
* Use the default slot to render arbitrary content inside the popover —
|
|
213
249
|
* useful for forms, share panels, filters, etc.
|