@teammodlr/ui-kit 3.0.0-beta.2 → 3.0.0-beta.20

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.
Files changed (54) hide show
  1. package/lib/components/colorpicker/ColorPicker.vue.d.ts +0 -1
  2. package/lib/components/colorpicker/style/ColorPicker.css.d.ts +1 -0
  3. package/lib/components/dialog/Dialog.vue.d.ts +6 -2
  4. package/lib/components/dialogprovider/types.d.ts +2 -0
  5. package/lib/components/drawer/Drawer.vue.d.ts +142 -0
  6. package/lib/components/drawer/style/Drawer.css.d.ts +33 -0
  7. package/lib/components/drawer/style/index.d.ts +1 -0
  8. package/lib/components/index.d.ts +2 -0
  9. package/lib/components/messageprovider/MessageProvider.vue.d.ts +2 -0
  10. package/lib/components/messageprovider/style/Toaster.css.d.ts +1 -0
  11. package/lib/components/popover/Popover.vue.d.ts +113 -0
  12. package/lib/components/popover/style/Popover.css.d.ts +4 -0
  13. package/lib/components/popover/style/index.d.ts +1 -0
  14. package/lib/index.cjs.js +3 -3
  15. package/lib/index.es.js +5754 -5230
  16. package/lib/style.css +1 -1
  17. package/package.json +1 -1
  18. package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
  19. package/src/components/colorpicker/ColorPicker.vue +8 -11
  20. package/src/components/colorpicker/examples/basic.vue +8 -17
  21. package/src/components/colorpicker/style/ColorPicker.css.ts +6 -1
  22. package/src/components/dialog/Dialog.vue +32 -13
  23. package/src/components/dialog/style/Dialog.css.ts +15 -1
  24. package/src/components/dialogprovider/DialogProvider.vue +7 -1
  25. package/src/components/dialogprovider/types.ts +2 -0
  26. package/src/components/drawer/Drawer.vue +276 -0
  27. package/src/components/drawer/examples/basic.vue +15 -0
  28. package/src/components/drawer/examples/custom-title.vue +21 -0
  29. package/src/components/drawer/examples/index.stories.ts +24 -0
  30. package/src/components/drawer/examples/index.ts +7 -0
  31. package/src/components/drawer/examples/no-mask.vue +15 -0
  32. package/src/components/drawer/examples/no-outside-close.vue +15 -0
  33. package/src/components/drawer/examples/placements.vue +28 -0
  34. package/src/components/drawer/examples/with-footer.vue +21 -0
  35. package/src/components/drawer/examples/with-trigger.vue +13 -0
  36. package/src/components/drawer/style/Drawer.css.ts +208 -0
  37. package/src/components/drawer/style/index.ts +1 -0
  38. package/src/components/dropdown/Dropdown.vue +1 -1
  39. package/src/components/dropdown/style/Dropdown.css.ts +16 -0
  40. package/src/components/index.ts +2 -0
  41. package/src/components/messageprovider/MessageProvider.vue +36 -7
  42. package/src/components/messageprovider/style/Toaster.css.ts +24 -0
  43. package/src/components/modal/style/Modal.css.ts +26 -2
  44. package/src/components/popover/Popover.vue +155 -0
  45. package/src/components/popover/examples/arrow.vue +47 -0
  46. package/src/components/popover/examples/basic.vue +15 -0
  47. package/src/components/popover/examples/index.stories.ts +19 -0
  48. package/src/components/popover/examples/index.ts +4 -0
  49. package/src/components/popover/examples/placement.vue +59 -0
  50. package/src/components/popover/examples/variant.vue +30 -0
  51. package/src/components/popover/style/Popover.css.ts +203 -0
  52. package/src/components/popover/style/index.ts +1 -0
  53. package/src/components/tab/Tab.vue +1 -1
  54. package/src/components/tab/style/Tab.css.ts +6 -0
@@ -6,6 +6,7 @@
6
6
  <DialogOverlay v-if="isOpen && modal" force-mount :class="backdropClasses" style="z-index: 10" />
7
7
  </Transition>
8
8
  <Transition type="animation" :enter-active-class="modalScale.enterActive" :leave-active-class="modalScale.leaveActive"
9
+ @after-leave="handleAfterLeave"
9
10
  :enter-from-class="modalScale.enterFrom" :leave-to-class="modalScale.leaveTo">
10
11
 
11
12
  <DialogContent
@@ -15,12 +16,12 @@
15
16
  :style="computedStyle"
16
17
  :disable-outside-pointer-events="modal"
17
18
  @open-auto-focus="handleOpenAutoFocus"
18
- @escape-key-down.prevent
19
- @pointer-down-outside.prevent
20
- @focus-outside="handleFocusOutside"
21
- @interact-outside.prevent
19
+ @escape-key-down="handleEscapeKeyDown"
20
+ @pointer-down-outside="handleOutsideEvent"
21
+ @focus-outside="handleOutsideEvent"
22
+ @interact-outside="handleOutsideEvent"
22
23
  >
23
- <FocusScope :trapped="focusTrap" :loop="focusTrap">
24
+ <FocusScope :trapped="activeFocusTrap" :loop="activeFocusTrap">
24
25
  <slot v-if="$slots.container" name="container" :close="close"></slot>
25
26
  <template v-else>
26
27
  <div :class="style.content">
@@ -125,7 +126,7 @@ export default defineComponent({
125
126
  },
126
127
  },
127
128
  // Eventually remove 'open', i lazy cant be assed for now
128
- emits: ['onOpen', 'onClose', 'update:open', 'open'],
129
+ emits: ['onOpen', 'onClose', 'update:open', 'open', 'close'],
129
130
  setup(props, { emit, expose }) {
130
131
  const wrapper = ref<HTMLElement | null>(null);
131
132
  const dialog = shallowRef<HTMLElement | ComponentPublicInstance | null>(null);
@@ -135,6 +136,8 @@ export default defineComponent({
135
136
  const zIndex = ref(props.zIndex ?? nextZIndex());
136
137
  provide(ZINDEX, zIndex);
137
138
  const hasOpened = ref(false);
139
+ const focusTrapOverride = ref<boolean | null>(null);
140
+ const activeFocusTrap = computed(() => focusTrapOverride.value ?? props.focusTrap);
138
141
 
139
142
  const isDragging = ref<boolean>(false);
140
143
 
@@ -221,6 +224,16 @@ export default defineComponent({
221
224
  handleOpenChange(false);
222
225
  }
223
226
 
227
+ function handleEscapeKeyDown(event: KeyboardEvent) {
228
+ event.preventDefault();
229
+ event.stopPropagation();
230
+ close();
231
+ }
232
+
233
+ function handleAfterLeave() {
234
+ emit('close');
235
+ }
236
+
224
237
  function onCloseEmit() {
225
238
  emit('onClose');
226
239
  if (props.onClose) {
@@ -234,12 +247,10 @@ export default defineComponent({
234
247
  zIndex.value = nextZIndex();
235
248
  }
236
249
 
237
- function handleFocusOutside(event: Event) {
238
- if (!props.focusTrap) {
239
- return;
250
+ function handleOutsideEvent(event: Event) {
251
+ if (activeFocusTrap.value) {
252
+ event.preventDefault();
240
253
  }
241
-
242
- event.preventDefault();
243
254
  }
244
255
 
245
256
  function handleOpenAutoFocus(event: Event) {
@@ -340,8 +351,13 @@ export default defineComponent({
340
351
  { immediate: true }
341
352
  );
342
353
 
354
+ function setFocusTrap(value: boolean) {
355
+ focusTrapOverride.value = value;
356
+ }
357
+
343
358
  expose({
344
359
  close,
360
+ setFocusTrap,
345
361
  });
346
362
 
347
363
  return {
@@ -360,11 +376,14 @@ export default defineComponent({
360
376
  wrapperClasses,
361
377
  wrapperStyle,
362
378
  close,
379
+ handleAfterLeave,
380
+ handleEscapeKeyDown,
363
381
  onEnter,
364
382
  handleOpenChange,
365
383
  handleOpenAutoFocus,
366
- handleFocusOutside,
367
- isOpen: computed(() => props.open)
384
+ handleOutsideEvent,
385
+ isOpen: computed(() => props.open),
386
+ activeFocusTrap,
368
387
  };
369
388
  },
370
389
  });
@@ -60,7 +60,16 @@ export const content = style({
60
60
  border: `1px solid ${themeVars.color.gray[300]}`,
61
61
  borderRadius: themeVars.rounded.medium,
62
62
  pointerEvents: 'auto',
63
- boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`
63
+ boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`,
64
+ selectors: {
65
+ '&:focus': {
66
+ outline: 'none'
67
+ },
68
+ '&:focus-visible': {
69
+ outline: 'none',
70
+ boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`
71
+ }
72
+ }
64
73
  });
65
74
 
66
75
  export const header = style({
@@ -119,6 +128,11 @@ export const close = style([
119
128
  ':focus': {
120
129
  backgroundColor: themeVars.color.gray[150]
121
130
  },
131
+ ':focus-visible': {
132
+ outline: 'none',
133
+ boxShadow: 'none',
134
+ backgroundColor: themeVars.color.gray[150]
135
+ },
122
136
  '::before': {
123
137
  content: '""',
124
138
  position: 'absolute',
@@ -39,6 +39,10 @@ export default defineComponent({
39
39
  };
40
40
  }
41
41
 
42
+ function setFocusTrap(key: string, enabled: boolean) {
43
+ dialogRefs.value[key]?.setFocusTrap(enabled);
44
+ }
45
+
42
46
  function create(key: string, options: DialogOptions = {}): DialogReactive {
43
47
  if (dialogMap[key]) {
44
48
  // Merge new options into existing reactive options
@@ -72,6 +76,7 @@ export default defineComponent({
72
76
  handleClose(key);
73
77
  }
74
78
  },
79
+ setFocusTrap: (enabled: boolean) => setFocusTrap(key, enabled),
75
80
  };
76
81
 
77
82
  dialogMap[key] = dialog;
@@ -122,7 +127,8 @@ export default defineComponent({
122
127
  remove,
123
128
  hasOpenDialog: () => hasOpenDialog.value,
124
129
  openDialogCount: () => openDialogCount.value,
125
- openDialogKeys: () => openDialogKeys.value
130
+ openDialogKeys: () => openDialogKeys.value,
131
+ setFocusTrap,
126
132
  };
127
133
 
128
134
  provide('m-dialog', api);
@@ -19,6 +19,7 @@ export interface DialogReactive {
19
19
  options: DialogOptions;
20
20
  open: () => void;
21
21
  close: () => void;
22
+ setFocusTrap: (enabled: boolean) => void;
22
23
  }
23
24
 
24
25
  export interface DialogApiInjection {
@@ -27,4 +28,5 @@ export interface DialogApiInjection {
27
28
  hasOpenDialog: () => boolean;
28
29
  openDialogCount: () => number;
29
30
  openDialogKeys: () => string[];
31
+ setFocusTrap: (key: string, enabled: boolean) => void;
30
32
  }
@@ -0,0 +1,276 @@
1
+ <template>
2
+ <DialogRoot :open="isOpen" modal @update:open="handleOpenChange">
3
+ <DialogTrigger v-if="hasTrigger" as-child>
4
+ <slot name="trigger" />
5
+ </DialogTrigger>
6
+ <DialogPortal to="body">
7
+ <Transition
8
+ appear
9
+ type="animation"
10
+ :enter-active-class="fade.enterActive"
11
+ :leave-active-class="fade.leaveActive"
12
+ :enter-from-class="fade.enterFrom"
13
+ :leave-to-class="fade.leaveTo"
14
+ >
15
+ <DialogOverlay
16
+ v-if="isOpen && showMask"
17
+ force-mount
18
+ :class="style.mask"
19
+ :style="{ zIndex }"
20
+ />
21
+ </Transition>
22
+ <Transition
23
+ appear
24
+ type="animation"
25
+ :enter-active-class="currentTransition.enterActive"
26
+ :leave-active-class="currentTransition.leaveActive"
27
+ :enter-from-class="currentTransition.enterFrom"
28
+ :leave-to-class="currentTransition.leaveTo"
29
+ >
30
+ <DialogContent
31
+ v-if="isOpen"
32
+ force-mount
33
+ :class="[style.placement[placement]]"
34
+ :style="contentStyle"
35
+ :disable-outside-pointer-events="true"
36
+ @open-auto-focus="handleOpenAutoFocus"
37
+ @escape-key-down="onEscapeKeyDown"
38
+ @pointer-down-outside="onPointerDownOutside"
39
+ @interact-outside="onInteractOutside"
40
+ >
41
+ <FocusScope :trapped="focusTrap" :loop="focusTrap">
42
+ <div v-if="hasTitle || showClose" :class="style.header">
43
+ <DialogTitle as-child>
44
+ <div :class="style.title">
45
+ <slot name="title">{{ title }}</slot>
46
+ </div>
47
+ </DialogTitle>
48
+ <DialogClose v-if="showClose" as-child>
49
+ <button :class="style.close" aria-label="Close" />
50
+ </DialogClose>
51
+ </div>
52
+ <div :class="style.body" ref="bodyRef">
53
+ <slot />
54
+ </div>
55
+ <div v-if="hasFooter" :class="style.footer">
56
+ <slot name="footer" />
57
+ </div>
58
+ </FocusScope>
59
+ </DialogContent>
60
+ </Transition>
61
+ </DialogPortal>
62
+ </DialogRoot>
63
+ </template>
64
+
65
+ <script lang="ts">
66
+ import type { CSSProperties, PropType } from 'vue';
67
+ import {
68
+ DialogClose,
69
+ DialogContent,
70
+ DialogOverlay,
71
+ DialogPortal,
72
+ DialogRoot,
73
+ DialogTitle,
74
+ DialogTrigger,
75
+ FocusScope
76
+ } from 'reka-ui';
77
+ import { computed, defineComponent, provide, ref, watch } from 'vue';
78
+ import * as style from './style';
79
+ import { fade } from '../../styles/transitions.css';
80
+ import { useZIndex, ZINDEX } from '../../composables/useZIndex';
81
+
82
+ type Placement = 'left' | 'right' | 'top' | 'bottom';
83
+
84
+ export default defineComponent({
85
+ name: 'MDrawer',
86
+ components: {
87
+ DialogClose,
88
+ DialogContent,
89
+ DialogOverlay,
90
+ DialogPortal,
91
+ DialogRoot,
92
+ DialogTitle,
93
+ DialogTrigger,
94
+ FocusScope
95
+ },
96
+ props: {
97
+ open: {
98
+ type: Boolean,
99
+ default: undefined
100
+ },
101
+ placement: {
102
+ type: String as PropType<Placement>,
103
+ default: 'right'
104
+ },
105
+ width: {
106
+ type: [String, Number],
107
+ default: '378px'
108
+ },
109
+ height: {
110
+ type: [String, Number],
111
+ default: '378px'
112
+ },
113
+ title: {
114
+ type: String,
115
+ default: ''
116
+ },
117
+ showClose: {
118
+ type: Boolean,
119
+ default: true
120
+ },
121
+ showMask: {
122
+ type: Boolean,
123
+ default: true
124
+ },
125
+ outsideClose: {
126
+ type: Boolean,
127
+ default: true
128
+ },
129
+ focusTrap: {
130
+ type: Boolean,
131
+ default: true
132
+ },
133
+ zIndex: {
134
+ type: Number
135
+ }
136
+ },
137
+ emits: ['open', 'close', 'update:open'],
138
+ setup(props, { emit, slots }) {
139
+ const { nextZIndex } = useZIndex();
140
+ const zIndex = ref(props.zIndex ?? nextZIndex());
141
+ provide(ZINDEX, zIndex);
142
+
143
+ const bodyRef = ref<HTMLElement | null>(null);
144
+ const hasFooter = computed(() => !!slots.footer);
145
+ const hasTitle = computed(() => !!slots.title || !!props.title);
146
+ const hasTrigger = computed(() => !!slots.trigger);
147
+
148
+ const internalOpen = ref(false);
149
+
150
+ const isOpen = computed({
151
+ get: () => {
152
+ if (props.open !== undefined) return props.open;
153
+ return internalOpen.value;
154
+ },
155
+ set: (value: boolean) => {
156
+ if (props.open !== undefined) {
157
+ emit('update:open', value);
158
+ } else {
159
+ internalOpen.value = value;
160
+ }
161
+ }
162
+ });
163
+
164
+ const isHorizontal = computed(() =>
165
+ props.placement === 'left' || props.placement === 'right'
166
+ );
167
+
168
+ const contentStyle = computed<CSSProperties>(() => {
169
+ const s: CSSProperties = { zIndex: zIndex.value };
170
+ if (isHorizontal.value) {
171
+ const w = props.width;
172
+ s.width = typeof w === 'number' ? `${w}px` : w;
173
+ } else {
174
+ const h = props.height;
175
+ s.height = typeof h === 'number' ? `${h}px` : h;
176
+ }
177
+ return s;
178
+ });
179
+
180
+ const currentTransition = computed(() => style.slideTransition[props.placement]);
181
+
182
+ const focusableSelector = [
183
+ '[autofocus]',
184
+ 'input:not([type="hidden"])',
185
+ 'textarea',
186
+ 'select',
187
+ '[contenteditable="true"]',
188
+ 'button',
189
+ 'a[href]',
190
+ '[tabindex]:not([tabindex="-1"])'
191
+ ].join(', ');
192
+
193
+ function getFirstFocusable() {
194
+ if (!bodyRef.value) return null;
195
+ return Array.from(
196
+ bodyRef.value.querySelectorAll<HTMLElement>(focusableSelector)
197
+ ).find(el =>
198
+ !el.matches(':disabled') &&
199
+ el.getAttribute('aria-disabled') !== 'true' &&
200
+ el.getAttribute('aria-hidden') !== 'true' &&
201
+ el.getClientRects().length > 0
202
+ ) ?? null;
203
+ }
204
+
205
+ const handleOpenAutoFocus = (e: Event) => {
206
+ const first = getFirstFocusable();
207
+ if (first) {
208
+ e.preventDefault();
209
+ first.focus();
210
+ }
211
+ };
212
+
213
+ const close = (e: MouseEvent | KeyboardEvent | undefined) => {
214
+ emit('close', e);
215
+ isOpen.value = false;
216
+ };
217
+
218
+ const onEscapeKeyDown = (e: KeyboardEvent) => {
219
+ e.stopPropagation();
220
+ close(e);
221
+ };
222
+
223
+ const onPointerDownOutside = (e: Event) => {
224
+ if (!props.outsideClose) e.preventDefault();
225
+ };
226
+
227
+ const onInteractOutside = (e: Event) => {
228
+ if (!props.outsideClose) {
229
+ e.preventDefault();
230
+ return;
231
+ }
232
+ close(e as MouseEvent | KeyboardEvent);
233
+ };
234
+
235
+ const handleOpenChange = (value: boolean) => {
236
+ if (!value && isOpen.value) {
237
+ close(undefined);
238
+ return;
239
+ }
240
+ isOpen.value = value;
241
+ };
242
+
243
+ watch(
244
+ () => isOpen.value,
245
+ (next, prev) => {
246
+ if (next && !prev) {
247
+ zIndex.value = props.zIndex ?? nextZIndex();
248
+ emit('open');
249
+ }
250
+ }
251
+ );
252
+
253
+ const open = () => { isOpen.value = true; };
254
+
255
+ return {
256
+ style,
257
+ fade,
258
+ isOpen,
259
+ zIndex,
260
+ hasFooter,
261
+ hasTitle,
262
+ hasTrigger,
263
+ bodyRef,
264
+ contentStyle,
265
+ currentTransition,
266
+ open,
267
+ close,
268
+ handleOpenAutoFocus,
269
+ handleOpenChange,
270
+ onEscapeKeyDown,
271
+ onPointerDownOutside,
272
+ onInteractOutside
273
+ };
274
+ }
275
+ });
276
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <MDrawer ref="drawer" title="Basic Drawer">
3
+ <p style="margin: 0">This is the drawer body. It slides in from the right by default.</p>
4
+ </MDrawer>
5
+
6
+ <MButton @click="drawer?.open()">Open Drawer</MButton>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import { ref } from 'vue';
11
+ import MDrawer from '../Drawer.vue';
12
+ import MButton from '../../button/Button.vue';
13
+
14
+ const drawer = ref<InstanceType<typeof MDrawer> | null>(null);
15
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <MDrawer ref="drawer">
3
+ <template #title>
4
+ <span style="display: flex; align-items: center; gap: 6px">
5
+ <span style="font-size: 1.1em">⚙️</span>
6
+ Settings
7
+ </span>
8
+ </template>
9
+ <p style="margin: 0">Custom title slot with an icon.</p>
10
+ </MDrawer>
11
+
12
+ <MButton @click="drawer?.open()">Open Drawer</MButton>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { ref } from 'vue';
17
+ import MDrawer from '../Drawer.vue';
18
+ import MButton from '../../button/Button.vue';
19
+
20
+ const drawer = ref<InstanceType<typeof MDrawer> | null>(null);
21
+ </script>
@@ -0,0 +1,24 @@
1
+ import MDrawer from '../Drawer.vue';
2
+ import {
3
+ MDrawerBasic,
4
+ MDrawerPlacements,
5
+ MDrawerNoMask,
6
+ MDrawerWithFooter,
7
+ MDrawerCustomTitle,
8
+ MDrawerWithTrigger,
9
+ MDrawerNoOutsideClose
10
+ } from './index';
11
+ import { createStory } from '../../../__storybook__';
12
+
13
+ export default {
14
+ component: MDrawer,
15
+ title: 'Components/Drawer'
16
+ };
17
+
18
+ export const Basic = createStory(MDrawerBasic);
19
+ export const WithTrigger = createStory(MDrawerWithTrigger);
20
+ export const Placements = createStory(MDrawerPlacements);
21
+ export const NoMask = createStory(MDrawerNoMask);
22
+ export const WithFooter = createStory(MDrawerWithFooter);
23
+ export const CustomTitle = createStory(MDrawerCustomTitle);
24
+ export const NoOutsideClose = createStory(MDrawerNoOutsideClose);
@@ -0,0 +1,7 @@
1
+ export { default as MDrawerBasic } from './basic.vue';
2
+ export { default as MDrawerPlacements } from './placements.vue';
3
+ export { default as MDrawerNoMask } from './no-mask.vue';
4
+ export { default as MDrawerWithFooter } from './with-footer.vue';
5
+ export { default as MDrawerCustomTitle } from './custom-title.vue';
6
+ export { default as MDrawerWithTrigger } from './with-trigger.vue';
7
+ export { default as MDrawerNoOutsideClose } from './no-outside-close.vue';
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <MDrawer v-model:open="isOpen" title="No Backdrop" :show-mask="false">
3
+ <p style="margin: 0">This drawer has no backdrop overlay.</p>
4
+ </MDrawer>
5
+
6
+ <MButton @click="isOpen = true">Open (no backdrop)</MButton>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import { ref } from 'vue';
11
+ import MDrawer from '../Drawer.vue';
12
+ import MButton from '../../button/Button.vue';
13
+
14
+ const isOpen = ref(false);
15
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <MDrawer ref="drawer" title="Persistent Drawer" :outside-close="false">
3
+ <p style="margin: 0">Clicking outside won't close this drawer. Use the × button or Escape.</p>
4
+ </MDrawer>
5
+
6
+ <MButton @click="drawer?.open()">Open Drawer</MButton>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import { ref } from 'vue';
11
+ import MDrawer from '../Drawer.vue';
12
+ import MButton from '../../button/Button.vue';
13
+
14
+ const drawer = ref<InstanceType<typeof MDrawer> | null>(null);
15
+ </script>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <MDrawer v-model:open="isOpen" :placement="activePlacement" :title="`Drawer — ${activePlacement}`">
3
+ <p style="margin: 0">Sliding in from the <strong>{{ activePlacement }}</strong>.</p>
4
+ </MDrawer>
5
+
6
+ <div style="display: flex; gap: 8px; flex-wrap: wrap">
7
+ <MButton @click="open('left')">Left</MButton>
8
+ <MButton @click="open('right')">Right</MButton>
9
+ <MButton @click="open('top')">Top</MButton>
10
+ <MButton @click="open('bottom')">Bottom</MButton>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import { ref } from 'vue';
16
+ import MDrawer from '../Drawer.vue';
17
+ import MButton from '../../button/Button.vue';
18
+
19
+ type Placement = 'left' | 'right' | 'top' | 'bottom';
20
+
21
+ const isOpen = ref(false);
22
+ const activePlacement = ref<Placement>('right');
23
+
24
+ function open(placement: Placement) {
25
+ activePlacement.value = placement;
26
+ isOpen.value = true;
27
+ }
28
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <MDrawer v-model:open="isOpen" title="Drawer with Footer">
3
+ <p style="margin: 0">Drawer body content goes here.</p>
4
+ <template #footer>
5
+ <div style="display: flex; gap: 8px; justify-content: flex-end; width: 100%">
6
+ <MButton @click="isOpen = false">Cancel</MButton>
7
+ <MButton variant="primary" @click="isOpen = false">Save</MButton>
8
+ </div>
9
+ </template>
10
+ </MDrawer>
11
+
12
+ <MButton @click="isOpen = true">Open Drawer</MButton>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { ref } from 'vue';
17
+ import MDrawer from '../Drawer.vue';
18
+ import MButton from '../../button/Button.vue';
19
+
20
+ const isOpen = ref(false);
21
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <MDrawer title="Profile">
3
+ <template #trigger>
4
+ <MButton>Edit profile</MButton>
5
+ </template>
6
+ <p style="margin: 0">Trigger slot — no ref or v-model needed.</p>
7
+ </MDrawer>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ import MDrawer from '../Drawer.vue';
12
+ import MButton from '../../button/Button.vue';
13
+ </script>