@teammodlr/ui-kit 3.0.0-beta.9 → 3.0.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.
Files changed (110) hide show
  1. package/lib/components/cardcontextmenu/CardContextMenu.vue.d.ts +4 -2
  2. package/lib/components/checkbox/Checkbox.vue.d.ts +4 -4
  3. package/lib/components/clearbutton/ClearButton.vue.d.ts +9 -0
  4. package/lib/components/clearbutton/style/ClearButton.css.d.ts +2 -0
  5. package/lib/components/clearbutton/style/index.d.ts +1 -0
  6. package/lib/components/colorpicker/ColorPicker.vue.d.ts +6 -4
  7. package/lib/components/colorpicker/style/ColorPicker.css.d.ts +1 -0
  8. package/lib/components/dialog/Dialog.vue.d.ts +14 -6
  9. package/lib/components/dialog/style/Dialog.css.d.ts +2 -0
  10. package/lib/components/drawer/Drawer.vue.d.ts +146 -0
  11. package/lib/components/drawer/style/Drawer.css.d.ts +33 -0
  12. package/lib/components/drawer/style/index.d.ts +1 -0
  13. package/lib/components/dropdown/Dropdown.vue.d.ts +4 -2
  14. package/lib/components/form/Form.vue.d.ts +4 -2
  15. package/lib/components/hovercard/HoverCard.vue.d.ts +124 -0
  16. package/lib/components/hovercard/style/HoverCard.css.d.ts +3 -0
  17. package/lib/components/hovercard/style/index.d.ts +1 -0
  18. package/lib/components/index.d.ts +3 -0
  19. package/lib/components/input/Input.vue.d.ts +20 -5
  20. package/lib/components/input/style/Input.css.d.ts +4 -0
  21. package/lib/components/modal/Modal.vue.d.ts +11 -4
  22. package/lib/components/pagination/Pagination.vue.d.ts +6 -3
  23. package/lib/components/paneldialog/PanelDialog.vue.d.ts +175 -0
  24. package/lib/components/paneldialog/style/PanelDialog.css.d.ts +8 -0
  25. package/lib/components/paneldialog/style/index.d.ts +1 -0
  26. package/lib/components/popover/Popover.vue.d.ts +28 -2
  27. package/lib/components/radio/Radio.vue.d.ts +5 -5
  28. package/lib/components/radiobutton/RadioButton.vue.d.ts +4 -4
  29. package/lib/components/radiobuttongroup/RadioButtonGroup.vue.d.ts +4 -2
  30. package/lib/components/select/Select.vue.d.ts +94 -91
  31. package/lib/components/select/style/Select.css.d.ts +9 -1
  32. package/lib/components/slider/Slider.vue.d.ts +6 -3
  33. package/lib/components/switch/Switch.vue.d.ts +4 -4
  34. package/lib/components/tab/Tab.vue.d.ts +10 -2
  35. package/lib/components/tab/TabGroup.vue.d.ts +6 -3
  36. package/lib/components/table/Table.vue.d.ts +4 -2
  37. package/lib/components/textarea/Textarea.vue.d.ts +16 -5
  38. package/lib/components/textarea/style/Textarea.css.d.ts +2 -0
  39. package/lib/components/tree/Tree.vue.d.ts +6 -3
  40. package/lib/composables/useInput.d.ts +9 -2
  41. package/lib/index.cjs.js +3 -3
  42. package/lib/index.es.js +8526 -6393
  43. package/lib/style.css +1 -1
  44. package/lib/tsconfig.app.tsbuildinfo +1 -0
  45. package/lib/tsconfig.vitest.tsbuildinfo +1 -0
  46. package/package.json +7 -4
  47. package/src/components/cardcontextmenu/CardContextMenu.vue +3 -1
  48. package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
  49. package/src/components/clearbutton/ClearButton.vue +41 -0
  50. package/src/components/clearbutton/style/ClearButton.css.ts +37 -0
  51. package/src/components/clearbutton/style/index.ts +1 -0
  52. package/src/components/colorpicker/ColorPicker.vue +12 -12
  53. package/src/components/colorpicker/examples/basic.vue +8 -17
  54. package/src/components/colorpicker/style/ColorPicker.css.ts +6 -1
  55. package/src/components/dialog/Dialog.vue +12 -6
  56. package/src/components/dialog/style/Dialog.css.ts +30 -0
  57. package/src/components/drawer/Drawer.vue +280 -0
  58. package/src/components/drawer/examples/basic.vue +15 -0
  59. package/src/components/drawer/examples/custom-title.vue +21 -0
  60. package/src/components/drawer/examples/index.stories.ts +24 -0
  61. package/src/components/drawer/examples/index.ts +7 -0
  62. package/src/components/drawer/examples/no-mask.vue +15 -0
  63. package/src/components/drawer/examples/no-outside-close.vue +15 -0
  64. package/src/components/drawer/examples/placements.vue +28 -0
  65. package/src/components/drawer/examples/with-footer.vue +21 -0
  66. package/src/components/drawer/examples/with-trigger.vue +13 -0
  67. package/src/components/drawer/style/Drawer.css.ts +208 -0
  68. package/src/components/drawer/style/index.ts +1 -0
  69. package/src/components/dropdown/Dropdown.vue +4 -2
  70. package/src/components/form/Form.vue +3 -3
  71. package/src/components/hovercard/HoverCard.vue +165 -0
  72. package/src/components/hovercard/style/HoverCard.css.ts +175 -0
  73. package/src/components/hovercard/style/index.ts +1 -0
  74. package/src/components/index.ts +3 -0
  75. package/src/components/input/Input.vue +52 -4
  76. package/src/components/input/examples/clearable.vue +18 -0
  77. package/src/components/input/examples/index.stories.ts +3 -1
  78. package/src/components/input/examples/index.ts +1 -0
  79. package/src/components/input/style/Input.css.ts +19 -0
  80. package/src/components/messageprovider/MessageProvider.vue +33 -7
  81. package/src/components/modal/Modal.vue +28 -11
  82. package/src/components/pagination/Pagination.vue +4 -1
  83. package/src/components/paneldialog/PanelDialog.vue +509 -0
  84. package/src/components/paneldialog/examples/basic.vue +36 -0
  85. package/src/components/paneldialog/examples/index.stories.ts +22 -0
  86. package/src/components/paneldialog/examples/index.ts +2 -0
  87. package/src/components/paneldialog/examples/modal.vue +31 -0
  88. package/src/components/paneldialog/style/PanelDialog.css.ts +90 -0
  89. package/src/components/paneldialog/style/index.ts +1 -0
  90. package/src/components/popover/Popover.vue +9 -2
  91. package/src/components/radiobuttongroup/RadioButtonGroup.vue +3 -1
  92. package/src/components/select/Select.vue +211 -111
  93. package/src/components/select/examples/clearable.vue +18 -0
  94. package/src/components/select/examples/fill.vue +52 -0
  95. package/src/components/select/examples/index.stories.ts +4 -1
  96. package/src/components/select/examples/index.ts +3 -0
  97. package/src/components/select/examples/remote.vue +54 -0
  98. package/src/components/select/style/Select.css.ts +140 -24
  99. package/src/components/slider/Slider.vue +4 -1
  100. package/src/components/tab/Tab.vue +4 -2
  101. package/src/components/tab/TabGroup.vue +4 -4
  102. package/src/components/tab/style/Tab.css.ts +6 -0
  103. package/src/components/table/Table.vue +3 -3
  104. package/src/components/textarea/Textarea.vue +40 -4
  105. package/src/components/textarea/examples/clearable.vue +16 -0
  106. package/src/components/textarea/examples/index.stories.ts +2 -1
  107. package/src/components/textarea/examples/index.ts +1 -0
  108. package/src/components/textarea/style/Textarea.css.ts +10 -0
  109. package/src/components/tree/Tree.vue +4 -1
  110. package/src/composables/useInput.ts +10 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammodlr/ui-kit",
3
- "version": "3.0.0-beta.9",
3
+ "version": "3.0.1",
4
4
  "description": "",
5
5
  "main": "lib/index.cjs.js",
6
6
  "module": "lib/index.es.js",
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "exports": {
14
14
  ".": {
15
+ "types": "./lib/index.d.ts",
15
16
  "import": "./lib/index.es.js",
16
17
  "require": "./lib/index.cjs.js"
17
18
  },
@@ -30,9 +31,10 @@
30
31
  "build:vite": "vite build",
31
32
  "build:storybook": "storybook build",
32
33
  "gen-types": "vue-tsc --declaration --emitDeclarationOnly -p tsconfig.vitest.json --composite false",
34
+ "test:types": "npm run gen-types && vue-tsc --noEmit -p type-tests/tsconfig.json",
33
35
  "release:prepare": "npm run build",
34
36
  "release": "bumpp --execute=\"npm run release:prepare\" --all",
35
- "release:publish": "npm run release && npm publish",
37
+ "release:publish": "npm run release && npm publish --tag latest",
36
38
  "build:watch": "concurrently \"vite build --watch\" \"vue-tsc --declaration --emitDeclarationOnly -p tsconfig.vitest.json --composite false --watch\""
37
39
  },
38
40
  "keywords": [],
@@ -63,9 +65,9 @@
63
65
  "ts-node": "^10.9.2",
64
66
  "typescript": "^5.5.3",
65
67
  "vite": "^5.3.3",
68
+ "vue": "^3.4.31",
66
69
  "vue-tsc": "^2.0.26",
67
- "vueuc": "^0.4.58",
68
- "vue": "^3.4.31"
70
+ "vueuc": "^0.4.58"
69
71
  },
70
72
  "dependencies": {
71
73
  "@floating-ui/vue": "^1.1.1",
@@ -77,6 +79,7 @@
77
79
  "maska": "^2.1.11",
78
80
  "reka-ui": "^2.9.3",
79
81
  "treemate": "^0.3.11",
82
+ "virtua": "^0.49.1",
80
83
  "vooks": "^0.2.12",
81
84
  "vue-sonner": "^2.0.9"
82
85
  },
@@ -124,7 +124,9 @@ export default defineComponent({
124
124
  type: Number
125
125
  }
126
126
  },
127
- emits: ['update:open'],
127
+ emits: {
128
+ 'update:open': (_value: boolean) => true
129
+ },
128
130
  setup(props, { emit }) {
129
131
  const parentState = inject(CARDCONTEXTMENU_STATE, null);
130
132
  const { nextZIndex } = useZIndex();
@@ -189,6 +189,7 @@ export const menuItem = style({
189
189
  cursor: 'pointer',
190
190
  transition: 'background-color 0.14s ease',
191
191
  textDecoration: 'none',
192
+ outline: 'none',
192
193
  ':hover': {
193
194
  vars: {
194
195
  [vars.backgroundColor]: themeVars.color.gray[100],
@@ -196,6 +197,12 @@ export const menuItem = style({
196
197
  },
197
198
  backgroundColor: vars.backgroundColor,
198
199
  color: vars.color
200
+ },
201
+ ':focus': {
202
+ outline: 'none'
203
+ },
204
+ ':focus-visible': {
205
+ outline: 'none'
199
206
  }
200
207
  });
201
208
 
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ tabindex="-1"
5
+ aria-label="Clear"
6
+ :class="style.clearButton"
7
+ @mousedown.prevent
8
+ @click.stop.prevent="$emit('clear', $event)"
9
+ @keydown.stop
10
+ >
11
+ <svg
12
+ :class="style.icon"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width="2.25"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ aria-hidden="true"
21
+ >
22
+ <path d="M18 6 6 18" />
23
+ <path d="m6 6 12 12" />
24
+ </svg>
25
+ </button>
26
+ </template>
27
+
28
+ <script lang="ts">
29
+ import { defineComponent } from 'vue';
30
+ import * as style from './style';
31
+
32
+ export default defineComponent({
33
+ name: 'MClearButton',
34
+ emits: {
35
+ clear: (_e: MouseEvent) => true
36
+ },
37
+ setup() {
38
+ return { style };
39
+ }
40
+ });
41
+ </script>
@@ -0,0 +1,37 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { themeVars } from '../../../styles/theme.css';
3
+
4
+ export const clearButton = style({
5
+ display: 'inline-flex',
6
+ alignItems: 'center',
7
+ justifyContent: 'center',
8
+ flexShrink: 0,
9
+ width: themeVars.space[6],
10
+ height: themeVars.space[6],
11
+ padding: 0,
12
+ border: 'none',
13
+ borderRadius: '9999px',
14
+ background: 'transparent',
15
+ color: themeVars.color.gray[400],
16
+ cursor: 'pointer',
17
+ outline: 'none',
18
+ transition: `color .15s ${themeVars.transition.default},
19
+ background-color .15s ${themeVars.transition.default}`,
20
+ ':hover': {
21
+ color: themeVars.color.gray[700],
22
+ backgroundColor: themeVars.color.gray[100]
23
+ },
24
+ ':focus-visible': {
25
+ color: themeVars.color.gray[700],
26
+ boxShadow: `0 0 0 2px ${themeVars.color.blue[200]}`
27
+ },
28
+ ':active': {
29
+ backgroundColor: themeVars.color.gray[200]
30
+ }
31
+ });
32
+
33
+ export const icon = style({
34
+ width: themeVars.space[4],
35
+ height: themeVars.space[4],
36
+ pointerEvents: 'none'
37
+ });
@@ -0,0 +1 @@
1
+ export * from './ClearButton.css';
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div ref="wrapper" class="m-color-picker">
3
- <PopoverRoot v-model:open="isOpen" @update:open="handleOpenChange">
3
+ <PopoverRoot v-model:open="isOpen">
4
4
  <PopoverTrigger as-child>
5
- <div ref="referenceRef">
5
+ <div ref="referenceRef" tabindex="0" @mousedown.prevent @focus="isOpen = true" :class="style.trigger">
6
6
  <slot :r="finalColor.r" :g="finalColor.g" :b="finalColor.b" :a="finalColor.a" :hex="hexInput"
7
7
  :transparent="style.transparent">
8
8
  <div :class="style.reference">
@@ -147,7 +147,7 @@ export default defineComponent({
147
147
  },
148
148
  transfer: {
149
149
  type: Boolean,
150
- default: false
150
+ default: true
151
151
  },
152
152
  placement: {
153
153
  type: String as PropType<Placement>,
@@ -162,7 +162,10 @@ export default defineComponent({
162
162
  default: "#FFFFFF"
163
163
  }
164
164
  },
165
- emits: ['update:modelValue', 'onColorChange'],
165
+ emits: {
166
+ 'update:modelValue': (_value: string) => true,
167
+ onColorChange: (_value: string) => true
168
+ },
166
169
  components: {
167
170
  MSlider,
168
171
  MInput,
@@ -441,15 +444,13 @@ export default defineComponent({
441
444
  updateFromHex();
442
445
  };
443
446
 
444
- const handleOpenChange = (value: boolean) => {
445
- const wasOpen = isOpen.value;
446
-
447
- isOpen.value = value;
448
-
449
- if (wasOpen && !value) {
447
+ watch(isOpen, (open, wasOpen) => {
448
+ console.log('[ColorPicker] isOpen watch:', { wasOpen, open, hexInput: hexInput.value });
449
+ if (wasOpen && !open) {
450
+ console.log('[ColorPicker] Emitting onColorChange:', hexInput.value);
450
451
  emit('onColorChange', hexInput.value);
451
452
  }
452
- };
453
+ });
453
454
 
454
455
 
455
456
  return {
@@ -485,7 +486,6 @@ export default defineComponent({
485
486
  transfer,
486
487
  canPickColor,
487
488
  openEyeDropper,
488
- handleOpenChange,
489
489
  onSliderChangeComplete,
490
490
  hexInputUser,
491
491
  toggleInherit,
@@ -12,22 +12,13 @@
12
12
  </div>
13
13
  </template>
14
14
 
15
- <script>
16
- export default {
17
- data() {
18
- return {
19
- colorValue: 'rgba(0,0,0,50%)' // Initial color value
20
- };
21
- },
22
- setup() {
23
- const updateColor = (color) => {
24
- console.log(color);
25
- debugger;
26
- }
15
+ <script setup>
16
+ import { ref } from 'vue';
27
17
 
28
- return {
29
- updateColor
30
- }
31
- }
32
- };
18
+ const colorValue = ref('rgba(0,0,0,50%)');
19
+
20
+ function updateColor(color) {
21
+ console.log(color);
22
+ colorValue.value = color;
23
+ }
33
24
  </script>
@@ -72,6 +72,10 @@ export const colours = style({
72
72
  cursor: 'crosshair'
73
73
  });
74
74
 
75
+ export const trigger = style({
76
+ outline: 'none'
77
+ });
78
+
75
79
  export const reference = style({
76
80
  backgroundImage:
77
81
  'linear-gradient(45deg, #bebebe 25%, transparent 25%), linear-gradient(-45deg, #bebebe 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #bebebe 75%), linear-gradient(-45deg, transparent 75%, #bebebe 75%)',
@@ -85,7 +89,8 @@ export const reference = style({
85
89
  borderRadius: themeVars.rounded.default,
86
90
  position: 'relative',
87
91
  cursor: 'pointer',
88
- userSelect: 'none'
92
+ userSelect: 'none',
93
+ outline: 'none'
89
94
  });
90
95
 
91
96
  export const transparent = style({
@@ -13,6 +13,7 @@
13
13
  v-if="isOpen"
14
14
  ref="dialog"
15
15
  force-mount
16
+ :class="style.dialogContent"
16
17
  :style="computedStyle"
17
18
  :disable-outside-pointer-events="modal"
18
19
  @open-auto-focus="handleOpenAutoFocus"
@@ -21,7 +22,7 @@
21
22
  @focus-outside="handleFocusOutside"
22
23
  @interact-outside.prevent
23
24
  >
24
- <FocusScope :trapped="focusTrap" :loop="focusTrap">
25
+ <FocusScope :trapped="focusTrap" :loop="focusTrap" :class="style.focusScope">
25
26
  <slot v-if="$slots.container" name="container" :close="close"></slot>
26
27
  <template v-else>
27
28
  <div :class="style.content">
@@ -126,7 +127,15 @@ export default defineComponent({
126
127
  },
127
128
  },
128
129
  // Eventually remove 'open', i lazy cant be assed for now
129
- emits: ['onOpen', 'onClose', 'update:open', 'open', 'close'],
130
+ emits: {
131
+ onOpen: () => true,
132
+ onClose: () => true,
133
+ 'update:open': (_value: boolean) => true,
134
+ open: () => true,
135
+ close: () => true,
136
+ // Fired once the leave animation has finished and the dialog is out of the DOM
137
+ 'after-leave': () => true
138
+ },
130
139
  setup(props, { emit, expose }) {
131
140
  const wrapper = ref<HTMLElement | null>(null);
132
141
  const dialog = shallowRef<HTMLElement | ComponentPublicInstance | null>(null);
@@ -230,6 +239,7 @@ export default defineComponent({
230
239
 
231
240
  function handleAfterLeave() {
232
241
  emit('close');
242
+ emit('after-leave');
233
243
  }
234
244
 
235
245
  function onCloseEmit() {
@@ -246,10 +256,6 @@ export default defineComponent({
246
256
  }
247
257
 
248
258
  function handleFocusOutside(event: Event) {
249
- if (!props.focusTrap) {
250
- return;
251
- }
252
-
253
259
  event.preventDefault();
254
260
  }
255
261
 
@@ -72,6 +72,36 @@ export const content = style({
72
72
  }
73
73
  });
74
74
 
75
+ // DialogContent chrome ([role="dialog"]). Covers both the default content
76
+ // above and custom `#container` panels, which bypass `content`.
77
+ // Scoped to the chrome element itself so interactive descendants
78
+ // (inputs, buttons) keep their own focus rings.
79
+ export const dialogContent = style({
80
+ outline: 'none',
81
+ selectors: {
82
+ '&:focus': {
83
+ outline: 'none'
84
+ },
85
+ '&:focus-visible': {
86
+ outline: 'none'
87
+ }
88
+ }
89
+ });
90
+
91
+ // Inner FocusScope wrapper ([tabindex="-1"]) rendered inside DialogContent.
92
+ // Suppresses the detached black outline seen on drag mousedown / Tab wrap.
93
+ export const focusScope = style({
94
+ outline: 'none',
95
+ selectors: {
96
+ '&:focus': {
97
+ outline: 'none'
98
+ },
99
+ '&:focus-visible': {
100
+ outline: 'none'
101
+ }
102
+ }
103
+ });
104
+
75
105
  export const header = style({
76
106
  color: themeVars.color.font.default,
77
107
  backgroundColor: '#FFF',
@@ -0,0 +1,280 @@
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: {
138
+ open: () => true,
139
+ close: (_e?: MouseEvent | KeyboardEvent) => true,
140
+ 'update:open': (_value: boolean) => true
141
+ },
142
+ setup(props, { emit, slots }) {
143
+ const { nextZIndex } = useZIndex();
144
+ const zIndex = ref(props.zIndex ?? nextZIndex());
145
+ provide(ZINDEX, zIndex);
146
+
147
+ const bodyRef = ref<HTMLElement | null>(null);
148
+ const hasFooter = computed(() => !!slots.footer);
149
+ const hasTitle = computed(() => !!slots.title || !!props.title);
150
+ const hasTrigger = computed(() => !!slots.trigger);
151
+
152
+ const internalOpen = ref(false);
153
+
154
+ const isOpen = computed({
155
+ get: () => {
156
+ if (props.open !== undefined) return props.open;
157
+ return internalOpen.value;
158
+ },
159
+ set: (value: boolean) => {
160
+ if (props.open !== undefined) {
161
+ emit('update:open', value);
162
+ } else {
163
+ internalOpen.value = value;
164
+ }
165
+ }
166
+ });
167
+
168
+ const isHorizontal = computed(() =>
169
+ props.placement === 'left' || props.placement === 'right'
170
+ );
171
+
172
+ const contentStyle = computed<CSSProperties>(() => {
173
+ const s: CSSProperties = { zIndex: zIndex.value };
174
+ if (isHorizontal.value) {
175
+ const w = props.width;
176
+ s.width = typeof w === 'number' ? `${w}px` : w;
177
+ } else {
178
+ const h = props.height;
179
+ s.height = typeof h === 'number' ? `${h}px` : h;
180
+ }
181
+ return s;
182
+ });
183
+
184
+ const currentTransition = computed(() => style.slideTransition[props.placement]);
185
+
186
+ const focusableSelector = [
187
+ '[autofocus]',
188
+ 'input:not([type="hidden"])',
189
+ 'textarea',
190
+ 'select',
191
+ '[contenteditable="true"]',
192
+ 'button',
193
+ 'a[href]',
194
+ '[tabindex]:not([tabindex="-1"])'
195
+ ].join(', ');
196
+
197
+ function getFirstFocusable() {
198
+ if (!bodyRef.value) return null;
199
+ return Array.from(
200
+ bodyRef.value.querySelectorAll<HTMLElement>(focusableSelector)
201
+ ).find(el =>
202
+ !el.matches(':disabled') &&
203
+ el.getAttribute('aria-disabled') !== 'true' &&
204
+ el.getAttribute('aria-hidden') !== 'true' &&
205
+ el.getClientRects().length > 0
206
+ ) ?? null;
207
+ }
208
+
209
+ const handleOpenAutoFocus = (e: Event) => {
210
+ const first = getFirstFocusable();
211
+ if (first) {
212
+ e.preventDefault();
213
+ first.focus();
214
+ }
215
+ };
216
+
217
+ const close = (e: MouseEvent | KeyboardEvent | undefined) => {
218
+ emit('close', e);
219
+ isOpen.value = false;
220
+ };
221
+
222
+ const onEscapeKeyDown = (e: KeyboardEvent) => {
223
+ e.stopPropagation();
224
+ close(e);
225
+ };
226
+
227
+ const onPointerDownOutside = (e: Event) => {
228
+ if (!props.outsideClose) e.preventDefault();
229
+ };
230
+
231
+ const onInteractOutside = (e: Event) => {
232
+ if (!props.outsideClose) {
233
+ e.preventDefault();
234
+ return;
235
+ }
236
+ close(e as MouseEvent | KeyboardEvent);
237
+ };
238
+
239
+ const handleOpenChange = (value: boolean) => {
240
+ if (!value && isOpen.value) {
241
+ close(undefined);
242
+ return;
243
+ }
244
+ isOpen.value = value;
245
+ };
246
+
247
+ watch(
248
+ () => isOpen.value,
249
+ (next, prev) => {
250
+ if (next && !prev) {
251
+ zIndex.value = props.zIndex ?? nextZIndex();
252
+ emit('open');
253
+ }
254
+ }
255
+ );
256
+
257
+ const open = () => { isOpen.value = true; };
258
+
259
+ return {
260
+ style,
261
+ fade,
262
+ isOpen,
263
+ zIndex,
264
+ hasFooter,
265
+ hasTitle,
266
+ hasTrigger,
267
+ bodyRef,
268
+ contentStyle,
269
+ currentTransition,
270
+ open,
271
+ close,
272
+ handleOpenAutoFocus,
273
+ handleOpenChange,
274
+ onEscapeKeyDown,
275
+ onPointerDownOutside,
276
+ onInteractOutside
277
+ };
278
+ }
279
+ });
280
+ </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>