@ouestfrance/sipa-bms-ui 8.21.0 → 8.22.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.
@@ -1,11 +1,35 @@
1
1
  <template>
2
- <div class="floating-window-wrapper">
3
- <div class="floating-window" v-show="open">
4
- <div class="floating-window__header">
5
- <h2>{{ title }}</h2>
2
+ <div ref="wrapperRef" class="floating-window-wrapper">
3
+ <div
4
+ v-if="isDragging"
5
+ class="floating-window__drag-overlay"
6
+ @mousemove="onDrag"
7
+ @mouseup="onDragEnd"
8
+ />
9
+ <div
10
+ ref="windowRef"
11
+ class="floating-window"
12
+ :class="{
13
+ 'floating-window--expanded': isExpanded,
14
+ 'floating-window--dragging': isDragging,
15
+ }"
16
+ v-show="open"
17
+ :style="windowStyle"
18
+ >
19
+ <div class="floating-window__header" @mousedown="onDragStart">
20
+ <h3 class="floating-window__header__title">{{ title }}</h3>
6
21
  <div class="floating-window__header__buttons">
7
- <BmsIconButton :tooltip-text="'Maximiser la fenêtre'" disabled>
8
- <Maximize2 />
22
+ <BmsIconButton
23
+ v-if="expandable"
24
+ class="floating-window__expand-btn"
25
+ :class="{ 'floating-window__expand-btn--expanded': isExpanded }"
26
+ :tooltip-text="
27
+ isExpanded ? 'Réduire la fenêtre' : 'Maximiser la fenêtre'
28
+ "
29
+ @click.prevent.stop="toggleExpand"
30
+ >
31
+ <Minimize2 v-if="isExpanded" />
32
+ <Maximize2 v-else />
9
33
  </BmsIconButton>
10
34
  <BmsIconButton @click.prevent.stop="open = false">
11
35
  <X />
@@ -20,14 +44,223 @@
20
44
  </template>
21
45
 
22
46
  <script setup lang="ts">
23
- import { Maximize2, X } from 'lucide-vue-next';
47
+ import { Maximize2, Minimize2, X } from 'lucide-vue-next';
24
48
  import BmsIconButton from '../button/BmsIconButton.vue';
49
+ import { ref, computed, onMounted, watch, type CSSProperties } from 'vue';
25
50
 
26
- defineProps<{ title: string }>();
51
+ export type Placement =
52
+ | 'top-left'
53
+ | 'top-right'
54
+ | 'bottom-left'
55
+ | 'bottom-right'
56
+ | 'center';
57
+
58
+ const props = withDefaults(
59
+ defineProps<{
60
+ title: string;
61
+ defaultPlacement?: Placement;
62
+ expandable?: boolean;
63
+ expandedWidth?: string;
64
+ expandedHeight?: string;
65
+ expandTarget?: string;
66
+ width?: string;
67
+ height?: string;
68
+ }>(),
69
+ {
70
+ defaultPlacement: 'center',
71
+ expandable: false,
72
+ expandedWidth: '100%',
73
+ expandedHeight: '100%',
74
+ expandTarget: undefined,
75
+ width: '50%',
76
+ height: '40%',
77
+ },
78
+ );
27
79
 
28
80
  const open = defineModel<boolean>({
29
81
  default: false,
30
82
  });
83
+
84
+ const wrapperRef = ref<HTMLElement | null>(null);
85
+ const windowRef = ref<HTMLElement | null>(null);
86
+ const position = ref<{ x: number; y: number } | null>(null);
87
+ const computedSize = ref<{ width: number; height: number } | null>(null);
88
+ const isDragging = ref(false);
89
+ const dragOffset = ref({ x: 0, y: 0 });
90
+ const isExpanded = ref(false);
91
+ const positionBeforeExpand = ref<{ x: number; y: number } | null>(null);
92
+ const expandedStyle = ref<CSSProperties | null>(null);
93
+
94
+ function computeInitialPosition(): { x: number; y: number } | null {
95
+ if (!wrapperRef.value || !windowRef.value) return null;
96
+
97
+ const wrapperRect = wrapperRef.value.getBoundingClientRect();
98
+ const windowRect = windowRef.value.getBoundingClientRect();
99
+ const padding = 16;
100
+
101
+ switch (props.defaultPlacement) {
102
+ case 'top-left':
103
+ return { x: wrapperRect.left + padding, y: wrapperRect.top + padding };
104
+ case 'top-right':
105
+ return {
106
+ x: wrapperRect.right - windowRect.width - padding,
107
+ y: wrapperRect.top + padding,
108
+ };
109
+ case 'bottom-left':
110
+ return {
111
+ x: wrapperRect.left + padding,
112
+ y: wrapperRect.bottom - windowRect.height - padding,
113
+ };
114
+ case 'bottom-right':
115
+ return {
116
+ x: wrapperRect.right - windowRect.width - padding,
117
+ y: wrapperRect.bottom - windowRect.height - padding,
118
+ };
119
+ case 'center':
120
+ default:
121
+ return null;
122
+ }
123
+ }
124
+
125
+ function initPosition() {
126
+ if (props.defaultPlacement !== 'center' && !position.value) {
127
+ position.value = computeInitialPosition();
128
+ }
129
+ }
130
+
131
+ onMounted(() => {
132
+ if (open.value) {
133
+ initPosition();
134
+ }
135
+ });
136
+
137
+ watch(open, (newVal) => {
138
+ if (newVal) {
139
+ requestAnimationFrame(initPosition);
140
+ }
141
+ });
142
+
143
+ const windowStyle = computed<CSSProperties>(() => {
144
+ if (isExpanded.value && expandedStyle.value) {
145
+ return expandedStyle.value;
146
+ }
147
+
148
+ const sizeStyle: CSSProperties = {};
149
+ if (computedSize.value) {
150
+ sizeStyle.width = `${computedSize.value.width}px`;
151
+ sizeStyle.height = `${computedSize.value.height}px`;
152
+ } else {
153
+ if (props.width) {
154
+ sizeStyle.width = props.width;
155
+ }
156
+ if (props.height) {
157
+ sizeStyle.height = props.height;
158
+ }
159
+ }
160
+
161
+ if (!position.value) {
162
+ return sizeStyle;
163
+ }
164
+
165
+ return {
166
+ position: 'absolute',
167
+ left: `${position.value.x}px`,
168
+ top: `${position.value.y}px`,
169
+ ...sizeStyle,
170
+ };
171
+ });
172
+
173
+ function computeExpandedStyle(): CSSProperties {
174
+ if (props.expandTarget) {
175
+ const targetEl = document.querySelector(props.expandTarget);
176
+ if (targetEl) {
177
+ const targetRect = targetEl.getBoundingClientRect();
178
+ return {
179
+ position: 'fixed',
180
+ left: `${targetRect.left}px`,
181
+ top: `${targetRect.top}px`,
182
+ width:
183
+ props.expandedWidth === '100%'
184
+ ? `${targetRect.width}px`
185
+ : props.expandedWidth,
186
+ height:
187
+ props.expandedHeight === '100%'
188
+ ? `${targetRect.height}px`
189
+ : props.expandedHeight,
190
+ maxWidth: 'none',
191
+ maxHeight: 'none',
192
+ };
193
+ }
194
+ }
195
+
196
+ return {
197
+ position: 'absolute',
198
+ width: props.expandedWidth,
199
+ height: props.expandedHeight,
200
+ maxWidth: 'none',
201
+ maxHeight: 'none',
202
+ };
203
+ }
204
+
205
+ function toggleExpand() {
206
+ if (isExpanded.value) {
207
+ expandedStyle.value = null;
208
+ position.value = positionBeforeExpand.value;
209
+ isExpanded.value = false;
210
+ } else {
211
+ positionBeforeExpand.value = position.value;
212
+ expandedStyle.value = computeExpandedStyle();
213
+ isExpanded.value = true;
214
+ }
215
+ }
216
+
217
+ function onDragStart(event: MouseEvent) {
218
+ if (
219
+ (event.target as HTMLElement).closest('.floating-window__header__buttons')
220
+ ) {
221
+ return;
222
+ }
223
+
224
+ if (isExpanded.value) {
225
+ return;
226
+ }
227
+
228
+ isDragging.value = true;
229
+
230
+ if (windowRef.value) {
231
+ const rect = windowRef.value.getBoundingClientRect();
232
+ dragOffset.value = {
233
+ x: event.clientX - rect.left,
234
+ y: event.clientY - rect.top,
235
+ };
236
+
237
+ if (!computedSize.value) {
238
+ computedSize.value = { width: rect.width, height: rect.height };
239
+ }
240
+
241
+ if (!position.value) {
242
+ position.value = { x: rect.left, y: rect.top };
243
+ }
244
+ }
245
+
246
+ document.addEventListener('mousemove', onDrag);
247
+ document.addEventListener('mouseup', onDragEnd);
248
+ }
249
+
250
+ function onDrag(event: MouseEvent) {
251
+ if (!isDragging.value) return;
252
+
253
+ position.value = {
254
+ x: event.clientX - dragOffset.value.x,
255
+ y: event.clientY - dragOffset.value.y,
256
+ };
257
+ }
258
+
259
+ function onDragEnd() {
260
+ isDragging.value = false;
261
+ document.removeEventListener('mousemove', onDrag);
262
+ document.removeEventListener('mouseup', onDragEnd);
263
+ }
31
264
  </script>
32
265
 
33
266
  <style scoped lang="scss">
@@ -45,25 +278,65 @@ const open = defineModel<boolean>({
45
278
  align-items: center;
46
279
  pointer-events: none;
47
280
 
281
+ .floating-window__drag-overlay {
282
+ position: fixed;
283
+ top: 0;
284
+ left: 0;
285
+ width: 100vw;
286
+ height: 100vh;
287
+ z-index: var(--bms-z-index-modal);
288
+ pointer-events: all;
289
+ cursor: grabbing;
290
+ }
291
+
48
292
  .floating-window {
49
293
  background-color: var(--bms-white);
50
- height: 100%;
51
- width: 100%;
52
294
  border-radius: var(--bms-border-radius-large);
53
295
  border: 1px solid var(--bms-grey-10);
54
296
  pointer-events: all;
55
297
  display: grid;
56
298
  grid-template-rows: auto 1fr;
57
- box-shadow: 0px 8px 8px 0px rgba(0, 0, 0, 0.25);
299
+ box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.25);
58
300
  z-index: var(--bms-z-index-modal);
301
+ transition:
302
+ left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
303
+ top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
304
+ width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
305
+ height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
306
+ border-radius 0.3s cubic-bezier(0.4, 0, 0.2, 1),
307
+ box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
308
+
309
+ &--expanded {
310
+ border-radius: 0;
311
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
312
+
313
+ .floating-window__header {
314
+ cursor: default;
315
+
316
+ &:active {
317
+ cursor: default;
318
+ }
319
+ }
320
+ }
321
+
322
+ &--dragging {
323
+ transition: none;
324
+ }
59
325
 
60
326
  &__header {
61
327
  display: flex;
62
328
  justify-content: space-between;
63
329
  align-items: center;
64
330
  border-bottom: 1px solid var(--bms-grey-10);
65
- padding: 1em;
66
- h2 {
331
+ padding: 0.5em;
332
+ cursor: grab;
333
+ user-select: none;
334
+
335
+ &:active {
336
+ cursor: grabbing;
337
+ }
338
+
339
+ &__title {
67
340
  margin: 0;
68
341
  }
69
342
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import BmsAlert from './components/feedback/BmsAlert.vue';
7
7
  import BmsBadge from './components/feedback/BmsBadge.vue';
8
8
  import BmsCaption from './components/feedback/BmsCaption.vue';
9
9
  import BmsCircularProgress from './components/feedback/BmsCircularProgress.vue';
10
+ import BmsGhost from '@/components/feedback/BmsGhost.vue';
10
11
  import BmsLoader from './components/feedback/BmsLoader.vue';
11
12
  import BmsTooltip from './components/feedback/BmsTooltip.vue';
12
13
 
@@ -79,6 +80,7 @@ export const createBmsUi = () => ({
79
80
  app.component('BmsBadge', BmsBadge);
80
81
  app.component('BmsCaption', BmsCaption);
81
82
  app.component('BmsCircularProgress', BmsCircularProgress);
83
+ app.component('BmsGhost', BmsGhost);
82
84
  app.component('BmsLoader', BmsLoader);
83
85
  app.component('BmsTooltip', BmsTooltip);
84
86
 
@@ -161,6 +163,7 @@ export {
161
163
  BmsBadge,
162
164
  BmsCaption,
163
165
  BmsCircularProgress,
166
+ BmsGhost,
164
167
  BmsLoader,
165
168
  BmsTooltip,
166
169
  BmsAutocomplete,