@citizenplane/pimp 18.8.3 → 18.9.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/CpBottomSheet.vue.d.ts +43 -0
- package/dist/components/CpBottomSheet.vue.d.ts.map +1 -0
- package/dist/components/CpDate.vue.d.ts +1 -1
- package/dist/components/CpDate.vue.d.ts.map +1 -1
- package/dist/components/CpDynamicDialog.vue.d.ts +40 -0
- package/dist/components/CpDynamicDialog.vue.d.ts.map +1 -0
- package/dist/components/CpInput.vue.d.ts +1 -1
- package/dist/components/CpInput.vue.d.ts.map +1 -1
- package/dist/components/CpMultiselect.vue.d.ts +1 -1
- package/dist/components/CpMultiselect.vue.d.ts.map +1 -1
- package/dist/components/CpSwitch.vue.d.ts +1 -1
- package/dist/components/CpSwitch.vue.d.ts.map +1 -1
- package/dist/components/CpTelInput.vue.d.ts +1 -1
- package/dist/components/CpTelInput.vue.d.ts.map +1 -1
- package/dist/components/CpTextarea.vue.d.ts +2 -2
- package/dist/components/CpTextarea.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/pimp.es.js +9310 -7611
- package/dist/pimp.umd.js +53 -51
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/CpBottomSheet.vue +123 -0
- package/src/components/CpDynamicDialog.vue +121 -0
- package/src/components/index.ts +7 -0
- package/src/stories/CpBottomSheet.stories.ts +135 -0
- package/src/stories/CpDynamicDialog.stories.ts +132 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citizenplane/pimp",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.9.1",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "storybook dev -p 8081",
|
|
6
6
|
"build-storybook": "storybook build --output-dir ./docs",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"unplugin-vue-components": ">=0.26.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@douxcode/vue-spring-bottom-sheet": "^3.2.0",
|
|
50
51
|
"@vueuse/core": "^14.3.0",
|
|
51
52
|
"feather-icons": "^4.29.2",
|
|
52
53
|
"floating-vue": "^5.2.2",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<vue-bottom-sheet
|
|
3
|
+
ref="bottomSheetRef"
|
|
4
|
+
:can-backdrop-close="canBackdropClose"
|
|
5
|
+
:can-swipe-close="canSwipeClose"
|
|
6
|
+
:duration="animationDuration"
|
|
7
|
+
expand-on-content-drag
|
|
8
|
+
:swipe-close-threshold="swipeCloseThreshold"
|
|
9
|
+
teleport-defer
|
|
10
|
+
@closed="onBottomSheetClosed"
|
|
11
|
+
@opened="onBottomSheetOpened"
|
|
12
|
+
>
|
|
13
|
+
<template #header>
|
|
14
|
+
<slot name="header" />
|
|
15
|
+
</template>
|
|
16
|
+
<main class="cpBottomSheet__content" tabindex="0">
|
|
17
|
+
<slot />
|
|
18
|
+
</main>
|
|
19
|
+
<template #footer>
|
|
20
|
+
<slot name="footer" />
|
|
21
|
+
</template>
|
|
22
|
+
</vue-bottom-sheet>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import VueBottomSheet from '@douxcode/vue-spring-bottom-sheet'
|
|
27
|
+
import { useTemplateRef, watch } from 'vue'
|
|
28
|
+
|
|
29
|
+
interface Emits {
|
|
30
|
+
(e: 'close' | 'open'): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
animationDuration?: number
|
|
35
|
+
canBackdropClose?: boolean
|
|
36
|
+
canSwipeClose?: boolean
|
|
37
|
+
preventClose?: boolean
|
|
38
|
+
swipeCloseThreshold?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
42
|
+
animationDuration: 300,
|
|
43
|
+
swipeCloseThreshold: '20%',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<Emits>()
|
|
47
|
+
|
|
48
|
+
const bottomSheetRef = useTemplateRef('bottomSheetRef')
|
|
49
|
+
const isVisibleModel = defineModel<boolean>()
|
|
50
|
+
|
|
51
|
+
const toggleBottomSheet = () => {
|
|
52
|
+
if (isVisibleModel.value) {
|
|
53
|
+
bottomSheetRef.value?.open()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
bottomSheetRef.value?.close()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
watch(isVisibleModel, () => toggleBottomSheet())
|
|
61
|
+
|
|
62
|
+
const onBottomSheetOpened = () => emit('open')
|
|
63
|
+
|
|
64
|
+
const onBottomSheetClosed = () => {
|
|
65
|
+
if (props.preventClose) return
|
|
66
|
+
|
|
67
|
+
emit('close')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
defineExpose({
|
|
71
|
+
open: () => bottomSheetRef.value?.open(),
|
|
72
|
+
close: () => bottomSheetRef.value?.close(),
|
|
73
|
+
})
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<style lang="scss">
|
|
77
|
+
.cpBottomSheet {
|
|
78
|
+
&__content {
|
|
79
|
+
@extend %u-layout-box-padding;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Package style overrides */
|
|
84
|
+
|
|
85
|
+
/* stylelint-disable */
|
|
86
|
+
|
|
87
|
+
:root {
|
|
88
|
+
--vsbs-backdrop-bg: var(--cp-background-overlay);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[data-vsbs-container] {
|
|
92
|
+
z-index: 5 !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
[data-vsbs-content] {
|
|
96
|
+
// override default padding for vue-bottom sheet content div
|
|
97
|
+
padding: 0 !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[data-vsbs-sheet] [data-vsbs-header] {
|
|
101
|
+
padding: var(--cp-spacing-2xl) var(--cp-spacing-xl) var(--cp-spacing-xl) var(--cp-spacing-xl);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[data-vsbs-sheet] {
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
max-height: calc(100% - 65px) !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
[data-vsbs-scroll] {
|
|
110
|
+
-webkit-overflow-scrolling: touch;
|
|
111
|
+
touch-action: pan-y;
|
|
112
|
+
overscroll-behavior: unset !important;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[data-vsbs-footer]:is(.vsbs-enter-active) {
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[data-vsbs-footer]:has(div:empty) {
|
|
120
|
+
display: none;
|
|
121
|
+
}
|
|
122
|
+
/* stylelint-enable */
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="dynamicWrapperComponent">
|
|
3
|
+
<component
|
|
4
|
+
:is="dynamicComponent"
|
|
5
|
+
v-if="dynamicRenderingCondition"
|
|
6
|
+
v-bind="dynamicComponentProps"
|
|
7
|
+
ref="dynamicComponentRef"
|
|
8
|
+
@close="emit('close')"
|
|
9
|
+
>
|
|
10
|
+
<!-- Header slot is only for bottom sheet -->
|
|
11
|
+
<template v-if="title" #header>
|
|
12
|
+
<slot name="title">
|
|
13
|
+
<cp-heading class="cpDynamicDialog__title" heading-level="h3">{{ title }}</cp-heading>
|
|
14
|
+
</slot>
|
|
15
|
+
</template>
|
|
16
|
+
<template v-if="title" #title>
|
|
17
|
+
<slot name="title" />
|
|
18
|
+
</template>
|
|
19
|
+
<template v-if="subtitle" #subtitle>
|
|
20
|
+
<slot name="subtitle" />
|
|
21
|
+
</template>
|
|
22
|
+
<template #default>
|
|
23
|
+
<slot />
|
|
24
|
+
</template>
|
|
25
|
+
<template #footer>
|
|
26
|
+
<slot name="footer" />
|
|
27
|
+
</template>
|
|
28
|
+
</component>
|
|
29
|
+
</component>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { computed, nextTick, resolveComponent, useTemplateRef, watch } from 'vue'
|
|
34
|
+
|
|
35
|
+
import { useIsBreakpoint } from '@/composables/useIsBreakpoint'
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
class?: string
|
|
39
|
+
isClosableOnClickOutside?: boolean
|
|
40
|
+
maxWidth?: number | undefined
|
|
41
|
+
preventClose?: boolean
|
|
42
|
+
subtitle?: string
|
|
43
|
+
title?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = defineProps<Props>()
|
|
47
|
+
|
|
48
|
+
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
49
|
+
|
|
50
|
+
const isMobile = useIsBreakpoint()
|
|
51
|
+
|
|
52
|
+
const dynamicComponentRef = useTemplateRef('dynamicComponentRef')
|
|
53
|
+
|
|
54
|
+
const isVisibleModel = defineModel<boolean>('isVisible', { default: false })
|
|
55
|
+
|
|
56
|
+
const ClientOnly = resolveComponent('client-only')
|
|
57
|
+
const CpTransitionDialog = resolveComponent('cp-transition-dialog')
|
|
58
|
+
const CpBottomSheet = resolveComponent('cp-bottom-sheet')
|
|
59
|
+
const CpDialog = resolveComponent('cp-dialog')
|
|
60
|
+
|
|
61
|
+
const dynamicWrapperComponent = computed(() => {
|
|
62
|
+
if (isMobile.value) return ClientOnly
|
|
63
|
+
return CpTransitionDialog
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const dynamicComponent = computed(() => {
|
|
67
|
+
if (isMobile.value) return CpBottomSheet
|
|
68
|
+
return CpDialog
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const dynamicRenderingCondition = computed(() => {
|
|
72
|
+
if (isMobile.value) return true
|
|
73
|
+
return isVisibleModel.value
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const dynamicComponentProps = computed(() => {
|
|
77
|
+
if (isMobile.value) {
|
|
78
|
+
return {
|
|
79
|
+
modelValue: isVisibleModel.value,
|
|
80
|
+
contentClass: props.class,
|
|
81
|
+
canBackdropClose: !props.preventClose,
|
|
82
|
+
canSwipeClose: !props.preventClose,
|
|
83
|
+
preventClose: props.preventClose,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
maxWidth: props.maxWidth,
|
|
89
|
+
class: props.class,
|
|
90
|
+
preventClose: props.preventClose,
|
|
91
|
+
isClosableOnClickOutside: props.isClosableOnClickOutside,
|
|
92
|
+
...(props.title && { title: props.title }),
|
|
93
|
+
...(props.subtitle && { subtitle: props.subtitle }),
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const openBottomSheet = async () => {
|
|
98
|
+
await nextTick()
|
|
99
|
+
|
|
100
|
+
if (!dynamicComponentRef.value) return
|
|
101
|
+
|
|
102
|
+
// @ts-expect-error - open does not exist on type {}
|
|
103
|
+
dynamicComponentRef.value?.open()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
watch(isMobile, () => {
|
|
107
|
+
if (isMobile.value && isVisibleModel.value) {
|
|
108
|
+
openBottomSheet()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<style lang="scss">
|
|
114
|
+
.cpDynamicDialog {
|
|
115
|
+
&__title {
|
|
116
|
+
font-size: var(--cp-text-size-2xl);
|
|
117
|
+
font-weight: 700;
|
|
118
|
+
line-height: var(--cp-line-height-2xl);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import 'vue-tel-input/vue-tel-input.css'
|
|
2
2
|
import 'floating-vue/dist/style.css'
|
|
3
3
|
import 'modern-normalize/modern-normalize.css'
|
|
4
|
+
import '@douxcode/vue-spring-bottom-sheet/dist/style.css'
|
|
4
5
|
|
|
5
6
|
import '@/assets/main.css'
|
|
6
7
|
|
|
@@ -19,6 +20,7 @@ import CpAccordionGroup from './CpAccordionGroup.vue'
|
|
|
19
20
|
import CpAirlineLogo from './CpAirlineLogo.vue'
|
|
20
21
|
import CpAlert from './CpAlert.vue'
|
|
21
22
|
import CpBadge from './CpBadge.vue'
|
|
23
|
+
import CpBottomSheet from './CpBottomSheet.vue'
|
|
22
24
|
import CpButton from './CpButton.vue'
|
|
23
25
|
import CpButtonGroup from './CpButtonGroup.vue'
|
|
24
26
|
import CpButtonToggle from './CpButtonToggle.vue'
|
|
@@ -28,6 +30,7 @@ import CpContextualMenu from './CpContextualMenu.vue'
|
|
|
28
30
|
import CpDate from './CpDate.vue'
|
|
29
31
|
import CpDatepicker from './CpDatepicker.vue'
|
|
30
32
|
import CpDialog from './CpDialog.vue'
|
|
33
|
+
import CpDynamicDialog from './CpDynamicDialog.vue'
|
|
31
34
|
import CpHeading from './CpHeading.vue'
|
|
32
35
|
import CpIcon from './CpIcon.vue'
|
|
33
36
|
import CpInput from './CpInput.vue'
|
|
@@ -76,11 +79,13 @@ const Components = {
|
|
|
76
79
|
CpToast,
|
|
77
80
|
CpButtonToggle,
|
|
78
81
|
CpBadge,
|
|
82
|
+
CpBottomSheet,
|
|
79
83
|
CpTabs,
|
|
80
84
|
CpHeading,
|
|
81
85
|
CpButton,
|
|
82
86
|
CpButtonGroup,
|
|
83
87
|
CpDialog,
|
|
88
|
+
CpDynamicDialog,
|
|
84
89
|
CpDate,
|
|
85
90
|
CpContextualMenu,
|
|
86
91
|
CpMenu,
|
|
@@ -153,6 +158,7 @@ export {
|
|
|
153
158
|
CpAirlineLogo,
|
|
154
159
|
CpAlert,
|
|
155
160
|
CpBadge,
|
|
161
|
+
CpBottomSheet,
|
|
156
162
|
CpButton,
|
|
157
163
|
CpButtonGroup,
|
|
158
164
|
CpButtonToggle,
|
|
@@ -163,6 +169,7 @@ export {
|
|
|
163
169
|
CpDate,
|
|
164
170
|
CpDatepicker,
|
|
165
171
|
CpDialog,
|
|
172
|
+
CpDynamicDialog,
|
|
166
173
|
CpHeading,
|
|
167
174
|
CpIcon,
|
|
168
175
|
CpInput,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import CpBottomSheet from '@/components/CpBottomSheet.vue'
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Organisms/CpBottomSheet',
|
|
8
|
+
component: CpBottomSheet,
|
|
9
|
+
globals: {
|
|
10
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
11
|
+
},
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen',
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
animationDuration: {
|
|
17
|
+
control: 'number',
|
|
18
|
+
description: 'Animation duration in milliseconds',
|
|
19
|
+
},
|
|
20
|
+
canBackdropClose: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
description: 'Allow closing by tapping the backdrop',
|
|
23
|
+
},
|
|
24
|
+
canSwipeClose: {
|
|
25
|
+
control: 'boolean',
|
|
26
|
+
description: 'Enable swipe-to-close gesture',
|
|
27
|
+
},
|
|
28
|
+
preventClose: {
|
|
29
|
+
control: 'boolean',
|
|
30
|
+
description: 'Prevent the sheet from emitting close when dismissed',
|
|
31
|
+
},
|
|
32
|
+
swipeCloseThreshold: {
|
|
33
|
+
control: 'text',
|
|
34
|
+
description: 'Translation threshold after which the sheet closes (px or %)',
|
|
35
|
+
},
|
|
36
|
+
onClose: { action: 'close' },
|
|
37
|
+
onOpen: { action: 'open' },
|
|
38
|
+
},
|
|
39
|
+
} satisfies Meta<typeof CpBottomSheet>
|
|
40
|
+
|
|
41
|
+
export default meta
|
|
42
|
+
type Story = StoryObj<typeof meta>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default bottom sheet featuring header, body and footer slots.
|
|
46
|
+
* Use the controls to experiment with each prop in isolation.
|
|
47
|
+
*/
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
animationDuration: 300,
|
|
51
|
+
canBackdropClose: true,
|
|
52
|
+
canSwipeClose: true,
|
|
53
|
+
swipeCloseThreshold: '20%',
|
|
54
|
+
},
|
|
55
|
+
render: (args) => ({
|
|
56
|
+
setup() {
|
|
57
|
+
const isVisible = ref(false)
|
|
58
|
+
return { args, isVisible }
|
|
59
|
+
},
|
|
60
|
+
template: `
|
|
61
|
+
<CpButton @click="isVisible = true">Open Bottom Sheet</CpButton>
|
|
62
|
+
<CpBottomSheet
|
|
63
|
+
v-bind="args"
|
|
64
|
+
v-model="isVisible"
|
|
65
|
+
@close="isVisible = false"
|
|
66
|
+
>
|
|
67
|
+
<template #header><strong>Header slot</strong></template>
|
|
68
|
+
<p>This is the default slot content. You can put any content here.</p>
|
|
69
|
+
<template #footer>
|
|
70
|
+
<CpButton @click="isVisible = false">Close</CpButton>
|
|
71
|
+
</template>
|
|
72
|
+
</CpBottomSheet>
|
|
73
|
+
`,
|
|
74
|
+
}),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Disable backdrop tap to close — the sheet can only be dismissed
|
|
79
|
+
* programmatically or via swipe (when enabled).
|
|
80
|
+
*/
|
|
81
|
+
export const NoBackdropClose: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
canBackdropClose: false,
|
|
84
|
+
},
|
|
85
|
+
render: (args) => ({
|
|
86
|
+
setup() {
|
|
87
|
+
const isVisible = ref(false)
|
|
88
|
+
return { args, isVisible }
|
|
89
|
+
},
|
|
90
|
+
template: `
|
|
91
|
+
<CpButton @click="isVisible = true">Open Bottom Sheet</CpButton>
|
|
92
|
+
<CpBottomSheet
|
|
93
|
+
v-bind="args"
|
|
94
|
+
v-model="isVisible"
|
|
95
|
+
@close="isVisible = false"
|
|
96
|
+
>
|
|
97
|
+
<template #header><strong>No backdrop close</strong></template>
|
|
98
|
+
<p>Tap the backdrop — the sheet stays open. Use the button below or swipe down to dismiss.</p>
|
|
99
|
+
<template #footer>
|
|
100
|
+
<CpButton @click="isVisible = false">Close</CpButton>
|
|
101
|
+
</template>
|
|
102
|
+
</CpBottomSheet>
|
|
103
|
+
`,
|
|
104
|
+
}),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Disable swipe-to-close — the sheet can only be dismissed
|
|
109
|
+
* by tapping the backdrop or using a close action.
|
|
110
|
+
*/
|
|
111
|
+
export const NoSwipeClose: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
canSwipeClose: false,
|
|
114
|
+
},
|
|
115
|
+
render: (args) => ({
|
|
116
|
+
setup() {
|
|
117
|
+
const isVisible = ref(false)
|
|
118
|
+
return { args, isVisible }
|
|
119
|
+
},
|
|
120
|
+
template: `
|
|
121
|
+
<CpButton @click="isVisible = true">Open Bottom Sheet</CpButton>
|
|
122
|
+
<CpBottomSheet
|
|
123
|
+
v-bind="args"
|
|
124
|
+
v-model="isVisible"
|
|
125
|
+
@close="isVisible = false"
|
|
126
|
+
>
|
|
127
|
+
<template #header><strong>No swipe close</strong></template>
|
|
128
|
+
<p>Swipe down will not dismiss this sheet. Use the backdrop or the button below.</p>
|
|
129
|
+
<template #footer>
|
|
130
|
+
<CpButton @click="isVisible = false">Close</CpButton>
|
|
131
|
+
</template>
|
|
132
|
+
</CpBottomSheet>
|
|
133
|
+
`,
|
|
134
|
+
}),
|
|
135
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { getCurrentInstance, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import CpDynamicDialog from '@/components/CpDynamicDialog.vue'
|
|
5
|
+
|
|
6
|
+
const ClientOnlyStub = { template: '<slot />' }
|
|
7
|
+
|
|
8
|
+
const registerClientOnly = () => {
|
|
9
|
+
const instance = getCurrentInstance()
|
|
10
|
+
instance?.appContext.app.component('client-only', ClientOnlyStub)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const STORY_TRIGGER_LAYOUT =
|
|
14
|
+
'display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; width: 100%;'
|
|
15
|
+
|
|
16
|
+
const createStoryRender = (content: string) => (args: Record<string, unknown>) => ({
|
|
17
|
+
components: { CpDynamicDialog },
|
|
18
|
+
setup() {
|
|
19
|
+
registerClientOnly()
|
|
20
|
+
const isVisible = ref(false)
|
|
21
|
+
return { args, isVisible }
|
|
22
|
+
},
|
|
23
|
+
template: `
|
|
24
|
+
<div style="${STORY_TRIGGER_LAYOUT}">
|
|
25
|
+
<CpButton @click="isVisible = true">Open Dynamic Dialog</CpButton>
|
|
26
|
+
<p style="margin-top: 8px; text-align: center;">Resize me</p>
|
|
27
|
+
</div>
|
|
28
|
+
<CpDynamicDialog
|
|
29
|
+
v-bind="args"
|
|
30
|
+
v-model:is-visible="isVisible"
|
|
31
|
+
@close="isVisible = false"
|
|
32
|
+
>
|
|
33
|
+
${content}
|
|
34
|
+
<template #footer>
|
|
35
|
+
<CpButton @click="isVisible = false">Close</CpButton>
|
|
36
|
+
</template>
|
|
37
|
+
</CpDynamicDialog>
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const meta = {
|
|
42
|
+
title: 'Organisms/CpDynamicDialog',
|
|
43
|
+
component: CpDynamicDialog,
|
|
44
|
+
parameters: {
|
|
45
|
+
layout: 'fullscreen',
|
|
46
|
+
docs: {
|
|
47
|
+
description: {
|
|
48
|
+
component:
|
|
49
|
+
'Responsive dialog wrapper that renders `CpDialog` inside `CpTransitionDialog` on desktop and `CpBottomSheet` on mobile viewports. Visibility is controlled with `v-model:is-visible`.',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
argTypes: {
|
|
54
|
+
maxWidth: {
|
|
55
|
+
control: 'number',
|
|
56
|
+
description: 'Maximum width of the desktop dialog (px)',
|
|
57
|
+
},
|
|
58
|
+
isClosableOnClickOutside: {
|
|
59
|
+
control: 'boolean',
|
|
60
|
+
description: 'Allow closing the desktop dialog by clicking the backdrop',
|
|
61
|
+
},
|
|
62
|
+
preventClose: {
|
|
63
|
+
control: 'boolean',
|
|
64
|
+
description: 'Prevent dismiss actions from emitting close',
|
|
65
|
+
},
|
|
66
|
+
title: {
|
|
67
|
+
control: 'text',
|
|
68
|
+
description: 'Title passed as a prop (desktop) or rendered in the bottom sheet header (mobile)',
|
|
69
|
+
},
|
|
70
|
+
subtitle: {
|
|
71
|
+
control: 'text',
|
|
72
|
+
description: 'Subtitle passed as a prop on desktop',
|
|
73
|
+
},
|
|
74
|
+
onClose: { action: 'close' },
|
|
75
|
+
},
|
|
76
|
+
} satisfies Meta<typeof CpDynamicDialog>
|
|
77
|
+
|
|
78
|
+
export default meta
|
|
79
|
+
type Story = StoryObj<typeof meta>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Desktop viewport — opens a centered dialog with title, subtitle, body and footer slots.
|
|
83
|
+
*/
|
|
84
|
+
export const Default: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
maxWidth: 600,
|
|
87
|
+
title: 'Dialog title',
|
|
88
|
+
subtitle: 'Dialog subtitle',
|
|
89
|
+
},
|
|
90
|
+
render: createStoryRender('<p>This is the default slot content. You can put any content here.</p>'),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Desktop viewport — the same API renders a dialog instead of a bottom sheet.
|
|
95
|
+
*/
|
|
96
|
+
export const Desktop: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
maxWidth: 600,
|
|
99
|
+
title: 'Dialog title',
|
|
100
|
+
subtitle: 'Dialog subtitle',
|
|
101
|
+
},
|
|
102
|
+
globals: {
|
|
103
|
+
viewport: { value: 'tablet', isRotated: false },
|
|
104
|
+
},
|
|
105
|
+
render: createStoryRender('<p>On desktop viewports, the dynamic dialog renders as a centered dialog.</p>'),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mobile viewport — the same API renders a bottom sheet instead of a dialog.
|
|
110
|
+
*/
|
|
111
|
+
export const Mobile: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
title: 'Bottom sheet title',
|
|
114
|
+
},
|
|
115
|
+
globals: {
|
|
116
|
+
viewport: { value: 'mobile1', isRotated: false },
|
|
117
|
+
},
|
|
118
|
+
render: createStoryRender('<p>On mobile viewports, the dynamic dialog becomes a bottom sheet.</p>'),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Prevent the dialog or bottom sheet from being closed by the user.
|
|
123
|
+
*/
|
|
124
|
+
export const PreventClose: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
maxWidth: 600,
|
|
127
|
+
title: 'Dialog title',
|
|
128
|
+
subtitle: 'Dialog subtitle',
|
|
129
|
+
preventClose: true,
|
|
130
|
+
},
|
|
131
|
+
render: createStoryRender('<p>Dismiss actions are blocked — use the button below to close programmatically.</p>'),
|
|
132
|
+
}
|