@teammodlr/ui-kit 3.0.0-beta.9 → 3.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.
Files changed (87) hide show
  1. package/lib/components/checkbox/Checkbox.vue.d.ts +4 -4
  2. package/lib/components/clearbutton/ClearButton.vue.d.ts +7 -0
  3. package/lib/components/clearbutton/style/ClearButton.css.d.ts +2 -0
  4. package/lib/components/clearbutton/style/index.d.ts +1 -0
  5. package/lib/components/colorpicker/ColorPicker.vue.d.ts +0 -1
  6. package/lib/components/colorpicker/style/ColorPicker.css.d.ts +1 -0
  7. package/lib/components/dialog/style/Dialog.css.d.ts +2 -0
  8. package/lib/components/drawer/Drawer.vue.d.ts +142 -0
  9. package/lib/components/drawer/style/Drawer.css.d.ts +33 -0
  10. package/lib/components/drawer/style/index.d.ts +1 -0
  11. package/lib/components/hovercard/HoverCard.vue.d.ts +122 -0
  12. package/lib/components/hovercard/style/HoverCard.css.d.ts +3 -0
  13. package/lib/components/hovercard/style/index.d.ts +1 -0
  14. package/lib/components/index.d.ts +3 -0
  15. package/lib/components/input/Input.vue.d.ts +20 -5
  16. package/lib/components/input/style/Input.css.d.ts +4 -0
  17. package/lib/components/paneldialog/PanelDialog.vue.d.ts +167 -0
  18. package/lib/components/paneldialog/style/PanelDialog.css.d.ts +8 -0
  19. package/lib/components/paneldialog/style/index.d.ts +1 -0
  20. package/lib/components/popover/Popover.vue.d.ts +24 -0
  21. package/lib/components/radio/Radio.vue.d.ts +5 -5
  22. package/lib/components/radiobutton/RadioButton.vue.d.ts +4 -4
  23. package/lib/components/select/Select.vue.d.ts +72 -91
  24. package/lib/components/select/style/Select.css.d.ts +9 -1
  25. package/lib/components/switch/Switch.vue.d.ts +4 -4
  26. package/lib/components/textarea/Textarea.vue.d.ts +16 -5
  27. package/lib/components/textarea/style/Textarea.css.d.ts +2 -0
  28. package/lib/composables/useInput.d.ts +9 -2
  29. package/lib/index.cjs.js +3 -3
  30. package/lib/index.es.js +8491 -6431
  31. package/lib/style.css +1 -1
  32. package/lib/tsconfig.app.tsbuildinfo +1 -0
  33. package/package.json +7 -4
  34. package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
  35. package/src/components/clearbutton/ClearButton.vue +39 -0
  36. package/src/components/clearbutton/style/ClearButton.css.ts +37 -0
  37. package/src/components/clearbutton/style/index.ts +1 -0
  38. package/src/components/colorpicker/ColorPicker.vue +8 -11
  39. package/src/components/colorpicker/examples/basic.vue +8 -17
  40. package/src/components/colorpicker/style/ColorPicker.css.ts +6 -1
  41. package/src/components/dialog/Dialog.vue +2 -5
  42. package/src/components/dialog/style/Dialog.css.ts +30 -0
  43. package/src/components/drawer/Drawer.vue +276 -0
  44. package/src/components/drawer/examples/basic.vue +15 -0
  45. package/src/components/drawer/examples/custom-title.vue +21 -0
  46. package/src/components/drawer/examples/index.stories.ts +24 -0
  47. package/src/components/drawer/examples/index.ts +7 -0
  48. package/src/components/drawer/examples/no-mask.vue +15 -0
  49. package/src/components/drawer/examples/no-outside-close.vue +15 -0
  50. package/src/components/drawer/examples/placements.vue +28 -0
  51. package/src/components/drawer/examples/with-footer.vue +21 -0
  52. package/src/components/drawer/examples/with-trigger.vue +13 -0
  53. package/src/components/drawer/style/Drawer.css.ts +208 -0
  54. package/src/components/drawer/style/index.ts +1 -0
  55. package/src/components/dropdown/Dropdown.vue +1 -1
  56. package/src/components/hovercard/HoverCard.vue +163 -0
  57. package/src/components/hovercard/style/HoverCard.css.ts +175 -0
  58. package/src/components/hovercard/style/index.ts +1 -0
  59. package/src/components/index.ts +3 -0
  60. package/src/components/input/Input.vue +52 -4
  61. package/src/components/input/examples/clearable.vue +18 -0
  62. package/src/components/input/examples/index.stories.ts +3 -1
  63. package/src/components/input/examples/index.ts +1 -0
  64. package/src/components/input/style/Input.css.ts +19 -0
  65. package/src/components/messageprovider/MessageProvider.vue +33 -7
  66. package/src/components/paneldialog/PanelDialog.vue +500 -0
  67. package/src/components/paneldialog/examples/basic.vue +36 -0
  68. package/src/components/paneldialog/examples/index.stories.ts +22 -0
  69. package/src/components/paneldialog/examples/index.ts +2 -0
  70. package/src/components/paneldialog/examples/modal.vue +31 -0
  71. package/src/components/paneldialog/style/PanelDialog.css.ts +90 -0
  72. package/src/components/paneldialog/style/index.ts +1 -0
  73. package/src/components/popover/Popover.vue +6 -1
  74. package/src/components/select/Select.vue +122 -42
  75. package/src/components/select/examples/clearable.vue +18 -0
  76. package/src/components/select/examples/fill.vue +52 -0
  77. package/src/components/select/examples/index.stories.ts +3 -1
  78. package/src/components/select/examples/index.ts +2 -0
  79. package/src/components/select/style/Select.css.ts +140 -24
  80. package/src/components/tab/Tab.vue +1 -1
  81. package/src/components/tab/style/Tab.css.ts +6 -0
  82. package/src/components/textarea/Textarea.vue +40 -4
  83. package/src/components/textarea/examples/clearable.vue +16 -0
  84. package/src/components/textarea/examples/index.stories.ts +2 -1
  85. package/src/components/textarea/examples/index.ts +1 -0
  86. package/src/components/textarea/style/Textarea.css.ts +10 -0
  87. package/src/composables/useInput.ts +10 -2
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div style="display: flex; flex-direction: column; gap: 12px; max-width: 320px;">
3
+ <MInput v-model="value" placeholder="Type something..." clearable />
4
+ <MInput v-model="value" placeholder="With a right icon" clearable>
5
+ <template #icon-right>
6
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" style="width: 20px; height: 20px;">
7
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
8
+ </svg>
9
+ </template>
10
+ </MInput>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import { ref } from 'vue';
16
+
17
+ const value = ref('Clear me');
18
+ </script>
@@ -5,7 +5,8 @@ import {
5
5
  MInputPrefix,
6
6
  MInputLeftIcon,
7
7
  MInputRightIcon,
8
- MInputSizes
8
+ MInputSizes,
9
+ MInputClearable
9
10
  } from './index';
10
11
 
11
12
  import { createStory, sizeArgType } from '../../../__storybook__';
@@ -24,3 +25,4 @@ export const Prefix = createStory(MInputPrefix);
24
25
  export const Sizes = createStory(MInputSizes);
25
26
  export const LeftIcon = createStory(MInputLeftIcon);
26
27
  export const RightIcon = createStory(MInputRightIcon);
28
+ export const Clearable = createStory(MInputClearable);
@@ -4,3 +4,4 @@ export { default as MInputPrefix } from './prefix.vue';
4
4
  export { default as MInputSizes } from './sizes.vue';
5
5
  export { default as MInputLeftIcon } from './left-icon.vue';
6
6
  export { default as MInputRightIcon } from './right-icon.vue';
7
+ export { default as MInputClearable } from './clearable.vue';
@@ -251,6 +251,25 @@ export const hasRightIcon = style({
251
251
  paddingRight: '2.25rem'
252
252
  });
253
253
 
254
+ export const hasClear = style({
255
+ paddingRight: '1.75rem'
256
+ });
257
+
258
+ export const hasClearAndRightIcon = style({
259
+ paddingRight: '4rem'
260
+ });
261
+
262
+ export const clear = style({
263
+ position: 'absolute',
264
+ top: '50%',
265
+ right: themeVars.space[1],
266
+ transform: 'translateY(-50%)'
267
+ });
268
+
269
+ export const clearWithRightIcon = style({
270
+ right: '2.25rem'
271
+ });
272
+
254
273
  export const prefix = style({
255
274
  pointerEvents: 'none',
256
275
  marginRight: themeVars.space[3],
@@ -64,7 +64,7 @@ export default defineComponent({
64
64
  },
65
65
  },
66
66
  setup(props) {
67
- const activeMessages = new Map<string, { stop: () => void; closed: boolean; onClose?: () => void }>();
67
+ const activeMessages = new Map<string, { stop: () => void; closed: boolean; currentId: string; onClose?: () => void }>();
68
68
 
69
69
  const sonnerPosition = computed(() => placementMap[props.placement]);
70
70
 
@@ -100,9 +100,9 @@ export default defineComponent({
100
100
  return timeout ?? props.timeout;
101
101
  }
102
102
 
103
- function syncToast(message: PrivateMessageReactive) {
103
+ function createToast(message: PrivateMessageReactive, sonnerId: string) {
104
104
  const options: ExternalToast = {
105
- id: message.id,
105
+ id: sonnerId,
106
106
  duration: normalizeDuration(message.timeout),
107
107
  closeButton: message.closeButton ?? message.closable,
108
108
  richColors: message.richColors,
@@ -133,6 +133,23 @@ export default defineComponent({
133
133
  }
134
134
  }
135
135
 
136
+ function syncToast(message: PrivateMessageReactive) {
137
+ const active = activeMessages.get(message.id);
138
+ if (!active) {
139
+ return;
140
+ }
141
+
142
+ const oldId = active.currentId;
143
+ const newId = Math.random().toString(16).slice(2, 14);
144
+ active.currentId = newId;
145
+
146
+ toast.dismiss(oldId);
147
+
148
+ requestAnimationFrame(() => {
149
+ createToast(message, newId);
150
+ });
151
+ }
152
+
136
153
  function closeMessage(id: string, invokeOnClose: boolean) {
137
154
  const active = activeMessages.get(id);
138
155
  if (!active || active.closed) {
@@ -164,20 +181,29 @@ export default defineComponent({
164
181
  type: options.type,
165
182
  onClose: options.onClose,
166
183
  destroy: () => {
184
+ const active = activeMessages.get(id);
185
+ const sonnerId = active?.currentId ?? id;
167
186
  closeMessage(id, false);
168
- toast.dismiss(id);
187
+ toast.dismiss(sonnerId);
169
188
  },
170
189
  }) as PrivateMessageReactive;
171
190
 
191
+ const sonnerId = Math.random().toString(16).slice(2, 14);
192
+ activeMessages.set(id, { stop: () => {}, closed: false, currentId: sonnerId, onClose: options.onClose });
193
+
172
194
  const stop = watch(
173
195
  () => [message.content, message.type, message.timeout, message.closable, message.closeButton, message.richColors, sonnerPosition.value],
174
- () => {
175
- syncToast(message);
196
+ (_, oldVal) => {
197
+ if (!oldVal) {
198
+ createToast(message, activeMessages.get(id)!.currentId);
199
+ } else {
200
+ syncToast(message);
201
+ }
176
202
  },
177
203
  { immediate: true }
178
204
  );
179
205
 
180
- activeMessages.set(id, { stop, closed: false, onClose: options.onClose });
206
+ activeMessages.get(id)!.stop = stop;
181
207
 
182
208
  return message;
183
209
  }
@@ -0,0 +1,500 @@
1
+ <template>
2
+ <DialogRoot :open="isOpen" :modal="modal" @update:open="handleOpenChange">
3
+ <DialogPortal to="body">
4
+ <Transition
5
+ type="animation"
6
+ :enter-active-class="fade.enterActive"
7
+ :leave-active-class="fade.leaveActive"
8
+ :enter-from-class="fade.enterFrom"
9
+ :leave-to-class="fade.leaveTo"
10
+ >
11
+ <DialogOverlay v-if="isOpen && modal" force-mount :class="style.backdrop" :style="{ zIndex, pointerEvents: 'auto' }" />
12
+ </Transition>
13
+ <Transition
14
+ type="animation"
15
+ :enter-active-class="modalScale.enterActive"
16
+ :leave-active-class="modalScale.leaveActive"
17
+ :enter-from-class="modalScale.enterFrom"
18
+ :leave-to-class="modalScale.leaveTo"
19
+ @after-leave="handleAfterLeave"
20
+ >
21
+ <DialogContent
22
+ v-if="isOpen"
23
+ ref="dialogContent"
24
+ force-mount
25
+ :class="contentClasses"
26
+ :style="contentStyle"
27
+ :disable-outside-pointer-events="modal"
28
+ @open-auto-focus="handleOpenAutoFocus"
29
+ @escape-key-down="handleEscapeKeyDown"
30
+ @pointer-down-outside.prevent
31
+ @focus-outside="handleFocusOutside"
32
+ @interact-outside.prevent
33
+ >
34
+ <FocusScope :trapped="focusTrap" :loop="focusTrap" :class="focusScopeClasses">
35
+ <slot name="backdrop" />
36
+ <div v-if="backdrop && !$slots.backdrop" :class="[style.backdrop, backdropClass]" />
37
+ <div
38
+ ref="panel"
39
+ :class="[style.panel, panelClass]"
40
+ :style="panelComputedStyle"
41
+ @pointerdown="onPanelPointerDown"
42
+ >
43
+ <slot :close="close" />
44
+ </div>
45
+ </FocusScope>
46
+ </DialogContent>
47
+ </Transition>
48
+ </DialogPortal>
49
+ </DialogRoot>
50
+ </template>
51
+ <script lang="ts">
52
+ import { fade, modalScale } from '../../styles/transitions.css';
53
+ import {
54
+ DialogContent,
55
+ DialogOverlay,
56
+ DialogPortal,
57
+ DialogRoot,
58
+ FocusScope
59
+ } from 'reka-ui';
60
+ import {
61
+ computed,
62
+ defineComponent,
63
+ nextTick,
64
+ onBeforeUnmount,
65
+ provide,
66
+ ref,
67
+ shallowRef,
68
+ watch,
69
+ type ComponentPublicInstance,
70
+ type CSSProperties,
71
+ type PropType
72
+ } from 'vue';
73
+ import type { ComputedRef } from 'vue';
74
+ import { useZIndex, ZINDEX } from '../../composables/useZIndex';
75
+ import { useEventListener } from '@vueuse/core';
76
+ import * as style from './style';
77
+
78
+ function toCssSize(value: number | string | undefined): string | undefined {
79
+ if (value === undefined) {
80
+ return undefined;
81
+ }
82
+
83
+ return typeof value === 'number' ? `${value}px` : value;
84
+ }
85
+
86
+ function resolveElement(
87
+ value: HTMLElement | ComponentPublicInstance | null | undefined
88
+ ): HTMLElement | null {
89
+ if (!value) {
90
+ return null;
91
+ }
92
+
93
+ if (value instanceof HTMLElement) {
94
+ return value;
95
+ }
96
+
97
+ if ('$el' in value && value.$el instanceof HTMLElement) {
98
+ return value.$el;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ export default defineComponent({
105
+ name: 'PanelDialog',
106
+ components: {
107
+ DialogContent,
108
+ DialogOverlay,
109
+ DialogPortal,
110
+ DialogRoot,
111
+ FocusScope
112
+ },
113
+ props: {
114
+ open: {
115
+ type: Boolean,
116
+ default: false
117
+ },
118
+ modal: {
119
+ type: Boolean,
120
+ default: false
121
+ },
122
+ width: {
123
+ type: [Number, String] as PropType<number | string>,
124
+ default: '500px'
125
+ },
126
+ height: {
127
+ type: [Number, String] as PropType<number | string>,
128
+ default: undefined
129
+ },
130
+ maxHeight: {
131
+ type: [Number, String] as PropType<number | string>,
132
+ default: '85vh'
133
+ },
134
+ draggable: {
135
+ type: Boolean,
136
+ default: true
137
+ },
138
+ handleSelector: {
139
+ type: String,
140
+ default: '[data-panel-handle]'
141
+ },
142
+ backdrop: {
143
+ type: Boolean,
144
+ default: false
145
+ },
146
+ backdropClass: {
147
+ type: String,
148
+ default: ''
149
+ },
150
+ panelClass: {
151
+ type: [String, Array, Object] as PropType<string | unknown[] | Record<string, unknown>>,
152
+ default: ''
153
+ },
154
+ panelStyle: {
155
+ type: Object as PropType<CSSProperties>,
156
+ default: undefined
157
+ },
158
+ zIndex: {
159
+ type: Number
160
+ },
161
+ focusTrap: {
162
+ type: Boolean,
163
+ default: true
164
+ },
165
+ onClose: {
166
+ type: Function as PropType<() => void>,
167
+ default: () => {}
168
+ }
169
+ },
170
+ emits: ['onOpen', 'onClose', 'update:open', 'open', 'close'],
171
+ setup(props, { emit, expose }) {
172
+ const dialogContent = shallowRef<HTMLElement | ComponentPublicInstance | null>(null);
173
+ const panel = ref<HTMLElement | null>(null);
174
+ const { nextZIndex, current: maxZIndex } = useZIndex();
175
+ const zIndex = ref(props.zIndex ?? nextZIndex());
176
+ provide(ZINDEX, zIndex);
177
+ const hasOpened = ref(false);
178
+
179
+ const offsetX = ref(0);
180
+ const offsetY = ref(0);
181
+ const isDragging = ref(false);
182
+
183
+ const dialogContentElement = computed(() => resolveElement(dialogContent.value));
184
+
185
+ const isOpen = computed(() => props.open);
186
+
187
+ const contentClasses = computed(() => [
188
+ style.dialogContent,
189
+ props.modal ? style.dialogContentModal : style.dialogContentNonModal
190
+ ]);
191
+
192
+ const contentStyle: ComputedRef<CSSProperties> = computed(() => ({
193
+ zIndex: zIndex.value
194
+ }));
195
+
196
+ const focusScopeClasses = computed(() => [
197
+ style.focusScope,
198
+ props.modal ? style.focusScopeModal : style.focusScopeNonModal
199
+ ]);
200
+
201
+ const panelComputedStyle: ComputedRef<CSSProperties> = computed(() => ({
202
+ ...props.panelStyle,
203
+ width: toCssSize(props.width),
204
+ height: toCssSize(props.height),
205
+ maxHeight: toCssSize(props.maxHeight),
206
+ transform: `translate(${offsetX.value}px, ${offsetY.value}px)`
207
+ }));
208
+
209
+ const focusableSelector = [
210
+ '[autofocus]',
211
+ 'input:not([type="hidden"])',
212
+ 'textarea',
213
+ 'select',
214
+ '[contenteditable="true"]',
215
+ 'button',
216
+ 'a[href]',
217
+ '[tabindex]:not([tabindex="-1"])'
218
+ ].join(', ');
219
+
220
+ function close(): void {
221
+ handleOpenChange(false);
222
+ }
223
+
224
+ function handleEscapeKeyDown(event: KeyboardEvent): void {
225
+ event.preventDefault();
226
+ event.stopPropagation();
227
+ close();
228
+ }
229
+
230
+ function handleAfterLeave(): void {
231
+ emit('close');
232
+ }
233
+
234
+ function onCloseEmit(): void {
235
+ emit('onClose');
236
+ if (props.onClose) {
237
+ props.onClose();
238
+ }
239
+ }
240
+
241
+ function onEnter(): void {
242
+ emit('onOpen');
243
+ emit('open');
244
+ zIndex.value = props.zIndex ?? nextZIndex();
245
+ }
246
+
247
+ function handleFocusOutside(event: Event): void {
248
+ event.preventDefault();
249
+ }
250
+
251
+ function handleOpenAutoFocus(event: Event): void {
252
+ const element = dialogContentElement.value ?? panel.value;
253
+
254
+ if (!element) {
255
+ return;
256
+ }
257
+
258
+ const firstFocusableElement = Array.from(
259
+ element.querySelectorAll<HTMLElement>(focusableSelector)
260
+ ).find(candidate => {
261
+ return (
262
+ !candidate.matches(':disabled') &&
263
+ candidate.getAttribute('aria-disabled') !== 'true' &&
264
+ candidate.getAttribute('aria-hidden') !== 'true' &&
265
+ candidate.getClientRects().length > 0
266
+ );
267
+ });
268
+
269
+ if (!firstFocusableElement) {
270
+ return;
271
+ }
272
+
273
+ event.preventDefault();
274
+ firstFocusableElement.focus();
275
+ }
276
+
277
+ function handleOpenChange(value: boolean): void {
278
+ if (!value && props.open) {
279
+ emit('update:open', false);
280
+ onCloseEmit();
281
+ hasOpened.value = false;
282
+
283
+ return;
284
+ }
285
+
286
+ emit('update:open', value);
287
+ }
288
+
289
+ function clampOffset(): void {
290
+ const element = panel.value;
291
+
292
+ if (!element) {
293
+ return;
294
+ }
295
+
296
+ const rect = element.getBoundingClientRect();
297
+ let dx = 0;
298
+ let dy = 0;
299
+
300
+ if (rect.width < window.innerWidth) {
301
+ if (rect.left < 0) {
302
+ dx = -rect.left;
303
+ } else if (rect.right > window.innerWidth) {
304
+ dx = window.innerWidth - rect.right;
305
+ }
306
+ } else if (rect.left !== 0) {
307
+ dx = -rect.left;
308
+ }
309
+
310
+ if (rect.height < window.innerHeight) {
311
+ if (rect.top < 0) {
312
+ dy = -rect.top;
313
+ } else if (rect.bottom > window.innerHeight) {
314
+ dy = window.innerHeight - rect.bottom;
315
+ }
316
+ } else if (rect.top !== 0) {
317
+ dy = -rect.top;
318
+ }
319
+
320
+ if (dx !== 0 || dy !== 0) {
321
+ offsetX.value += dx;
322
+ offsetY.value += dy;
323
+ element.style.transform = `translate(${offsetX.value}px, ${offsetY.value}px)`;
324
+ }
325
+ }
326
+
327
+ function resetPosition(): void {
328
+ offsetX.value = 0;
329
+ offsetY.value = 0;
330
+
331
+ if (panel.value) {
332
+ panel.value.style.transform = 'translate(0px, 0px)';
333
+ }
334
+ }
335
+
336
+ interface DragState {
337
+ startX: number;
338
+ startY: number;
339
+ baseX: number;
340
+ baseY: number;
341
+ }
342
+
343
+ let dragState: DragState | null = null;
344
+
345
+ function onPointerMove(event: PointerEvent): void {
346
+ if (!dragState || !panel.value) {
347
+ return;
348
+ }
349
+
350
+ const element = panel.value;
351
+ const renderedRect = element.getBoundingClientRect();
352
+ const renderedX = offsetX.value;
353
+ const renderedY = offsetY.value;
354
+ let nextX = dragState.baseX + (event.clientX - dragState.startX);
355
+ let nextY = dragState.baseY + (event.clientY - dragState.startY);
356
+ const dx = nextX - renderedX;
357
+ const dy = nextY - renderedY;
358
+ const nextRect = {
359
+ left: renderedRect.left + dx,
360
+ right: renderedRect.right + dx,
361
+ top: renderedRect.top + dy,
362
+ bottom: renderedRect.bottom + dy,
363
+ width: renderedRect.width,
364
+ height: renderedRect.height
365
+ };
366
+
367
+ if (nextRect.width < window.innerWidth) {
368
+ if (nextRect.left < 0) {
369
+ nextX -= nextRect.left;
370
+ } else if (nextRect.right > window.innerWidth) {
371
+ nextX -= nextRect.right - window.innerWidth;
372
+ }
373
+ }
374
+
375
+ if (nextRect.height < window.innerHeight) {
376
+ if (nextRect.top < 0) {
377
+ nextY -= nextRect.top;
378
+ } else if (nextRect.bottom > window.innerHeight) {
379
+ nextY -= nextRect.bottom - window.innerHeight;
380
+ }
381
+ }
382
+
383
+ offsetX.value = nextX;
384
+ offsetY.value = nextY;
385
+ element.style.transform = `translate(${nextX}px, ${nextY}px)`;
386
+ }
387
+
388
+ function onPointerUp(): void {
389
+ dragState = null;
390
+ isDragging.value = false;
391
+ window.removeEventListener('pointermove', onPointerMove);
392
+ window.removeEventListener('pointerup', onPointerUp);
393
+ }
394
+
395
+ function onPanelPointerDown(event: PointerEvent): void {
396
+ if (!props.draggable || !panel.value) {
397
+ return;
398
+ }
399
+
400
+ if (event.button !== 0) {
401
+ return;
402
+ }
403
+
404
+ const target = event.target as HTMLElement | null;
405
+
406
+ if (!target || typeof target.closest !== 'function') {
407
+ return;
408
+ }
409
+
410
+ if (props.handleSelector && !target.closest(props.handleSelector)) {
411
+ return;
412
+ }
413
+
414
+ if (
415
+ target.closest(
416
+ 'button, input, select, textarea, a[href], [contenteditable="true"]'
417
+ )
418
+ ) {
419
+ return;
420
+ }
421
+
422
+ if (zIndex.value < maxZIndex.value) {
423
+ zIndex.value = nextZIndex();
424
+ }
425
+
426
+ dragState = {
427
+ startX: event.clientX,
428
+ startY: event.clientY,
429
+ baseX: offsetX.value,
430
+ baseY: offsetY.value
431
+ };
432
+ isDragging.value = true;
433
+ window.addEventListener('pointermove', onPointerMove);
434
+ window.addEventListener('pointerup', onPointerUp);
435
+ event.preventDefault();
436
+ }
437
+
438
+ function onWindowResize(): void {
439
+ if (props.open) {
440
+ nextTick(() => clampOffset());
441
+ }
442
+ }
443
+
444
+ useEventListener(window, 'resize', onWindowResize);
445
+
446
+ onBeforeUnmount(() => {
447
+ window.removeEventListener('pointermove', onPointerMove);
448
+ window.removeEventListener('pointerup', onPointerUp);
449
+ });
450
+
451
+ watch(
452
+ () => props.open,
453
+ async newVal => {
454
+ if (newVal) {
455
+ await nextTick();
456
+ resetPosition();
457
+ nextTick(() => clampOffset());
458
+ zIndex.value = props.zIndex ?? nextZIndex();
459
+ if (!hasOpened.value) {
460
+ onEnter();
461
+ hasOpened.value = true;
462
+ }
463
+ } else {
464
+ hasOpened.value = false;
465
+ if (dragState) {
466
+ onPointerUp();
467
+ }
468
+ }
469
+ },
470
+ { immediate: true }
471
+ );
472
+
473
+ expose({
474
+ close
475
+ });
476
+
477
+ return {
478
+ dialogContent,
479
+ panel,
480
+ zIndex,
481
+ fade,
482
+ modalScale,
483
+ style,
484
+ isOpen,
485
+ contentClasses,
486
+ contentStyle,
487
+ focusScopeClasses,
488
+ panelComputedStyle,
489
+ isDragging,
490
+ close,
491
+ handleAfterLeave,
492
+ handleEscapeKeyDown,
493
+ handleOpenChange,
494
+ handleOpenAutoFocus,
495
+ handleFocusOutside,
496
+ onPanelPointerDown
497
+ };
498
+ }
499
+ });
500
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <div>
3
+ <button @click="open = true">Open panel</button>
4
+ <MPanelDialog
5
+ v-model:open="open"
6
+ :modal="false"
7
+ width="500px"
8
+ :draggable="true"
9
+ @onClose="closed"
10
+ >
11
+ <div data-panel-handle style="padding: 12px 16px; border-bottom: 1px solid #e4e2ea; font-weight: 600;">
12
+ Draggable panel (grab here)
13
+ </div>
14
+ <div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
15
+ <p style="margin: 0;">Non-modal floating panel. Drag by the header. Background stays interactive.</p>
16
+ <MInput placeholder="Focus me — blue ring must remain" />
17
+ <input type="text" placeholder="Native input" />
18
+ <div style="display: flex; gap: 8px; justify-content: flex-end;">
19
+ <button @click="open = false">Close</button>
20
+ </div>
21
+ </div>
22
+ </MPanelDialog>
23
+ </div>
24
+ </template>
25
+
26
+ <script lang="ts" setup>
27
+ import { ref } from 'vue';
28
+ import MPanelDialog from '../PanelDialog.vue';
29
+ import MInput from '../../input/Input.vue';
30
+
31
+ const open = ref(false);
32
+
33
+ function closed(): void {
34
+ console.log('closed');
35
+ }
36
+ </script>