@thinkpixellab-public/px-vue 4.1.13 → 4.1.14

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.
@@ -0,0 +1,606 @@
1
+ <template>
2
+ <teleport to="body" :disabled="!teleportToBody">
3
+ <div
4
+ v-show="ready"
5
+ :class="[bem({ teleport: teleportToBody }), panelClass]"
6
+ ref="panel"
7
+ :style="cssPosition"
8
+ >
9
+ <div
10
+ v-if="shouldShowHeader"
11
+ :class="[bem('header'), headerClass]"
12
+ :data-tool-panel-drag="allowDrag"
13
+ >
14
+ <slot name="header" :title="title">
15
+ <span>{{ title }}</span>
16
+ </slot>
17
+ </div>
18
+ <div :class="bem('content')">
19
+ <slot></slot>
20
+ </div>
21
+
22
+ <div
23
+ v-show="canResizeNorth"
24
+ :class="bem('resize-handle', { n: true })"
25
+ data-resize-handle="n"
26
+ ></div>
27
+ <div
28
+ v-show="canResizeSouth"
29
+ :class="bem('resize-handle', { s: true })"
30
+ data-resize-handle="s"
31
+ ></div>
32
+ <div
33
+ v-show="canResizeEast"
34
+ :class="bem('resize-handle', { e: true })"
35
+ data-resize-handle="e"
36
+ ></div>
37
+ <div
38
+ v-show="canResizeWest"
39
+ :class="bem('resize-handle', { w: true })"
40
+ data-resize-handle="w"
41
+ ></div>
42
+ <!-- Corner handles for diagonal resizing -->
43
+ <div
44
+ v-show="canResizeNorth && canResizeEast"
45
+ :class="bem('resize-handle', { ne: true })"
46
+ data-resize-handle="ne"
47
+ ></div>
48
+ <div
49
+ v-show="canResizeNorth && canResizeWest"
50
+ :class="bem('resize-handle', { nw: true })"
51
+ data-resize-handle="nw"
52
+ ></div>
53
+ <div
54
+ v-show="canResizeSouth && canResizeEast"
55
+ :class="bem('resize-handle', { se: true })"
56
+ data-resize-handle="se"
57
+ ></div>
58
+ <div
59
+ v-show="canResizeSouth && canResizeWest"
60
+ :class="bem('resize-handle', { sw: true })"
61
+ data-resize-handle="sw"
62
+ ></div>
63
+ </div>
64
+ </teleport>
65
+ </template>
66
+
67
+ <script>
68
+ import Drag from '../utils/drag.js';
69
+
70
+ export default {
71
+ name: 'px-tool-panel',
72
+ props: {
73
+ /** Initial bounds object that defines the position and dimensions */
74
+ bounds: { type: Object, default: null },
75
+
76
+ /** Teleport component to body instead of positioning within parent element */
77
+ teleportToBody: { type: Boolean, default: false },
78
+
79
+ /** Show the header section of the panel. If null, header will only show if title or header slot content exists */
80
+ showHeader: { type: Boolean, default: null },
81
+
82
+ /** Title text to display in the panel header */
83
+ title: { type: String, default: null },
84
+
85
+ /** Additional CSS class to apply to the panel (use this instead of class because of the
86
+ * teleport) */
87
+ panelClass: { type: String, default: '' },
88
+
89
+ /** Additional CSS class to apply to the header element */
90
+ headerClass: { type: String, default: '' },
91
+
92
+ /** Whether the panel can be dragged by its header (or other element with
93
+ * data-tool-panel-drag="true") */
94
+ allowDrag: { type: Boolean, default: true },
95
+
96
+ /**
97
+ * Boolean (true means all sides can be resized) or string containing N S E W characters to
98
+ * indicate which sides can be resized
99
+ */
100
+ allowResize: { type: [String, Boolean], default: 'NSEW' },
101
+
102
+ /** Width of resize handles in pixels */
103
+ resizeHandleWidth: { type: Number, default: 8 },
104
+
105
+ /** Minimum width of the panel in pixels */
106
+ minWidth: { type: Number, default: null },
107
+
108
+ /** Minimum height of the panel in pixels */
109
+ minHeight: { type: Number, default: null },
110
+ },
111
+ data() {
112
+ const initialBounds = this.bounds ? { ...this.bounds } : null;
113
+ return {
114
+ ready: false,
115
+ localBounds: initialBounds,
116
+ containerSize: {
117
+ width: 0,
118
+ height: 0,
119
+ },
120
+ resizeObserver: null,
121
+ dragHandler: null,
122
+ };
123
+ },
124
+ computed: {
125
+ cssPosition() {
126
+ if (this.validBounds) {
127
+ const { x, y, width, height } = this.validBounds;
128
+ return {
129
+ left: x != null ? this.cssPx(x) : 'auto',
130
+ top: y != null ? this.cssPx(y) : 'auto',
131
+ width: width != null ? this.cssPx(width) : 'auto',
132
+ height: height != null ? this.cssPx(height) : 'auto',
133
+ minWidth: this.minWidth ? this.cssPx(this.minWidth) : null,
134
+ minHeight: this.minHeight ? this.cssPx(this.minHeight) : null,
135
+ };
136
+ }
137
+
138
+ return null;
139
+ },
140
+
141
+ canResizeNorth() {
142
+ return (
143
+ this.allowResize === true ||
144
+ (typeof this.allowResize === 'string' && this.allowResize.includes('N'))
145
+ );
146
+ },
147
+ canResizeSouth() {
148
+ return (
149
+ this.allowResize === true ||
150
+ (typeof this.allowResize === 'string' && this.allowResize.includes('S'))
151
+ );
152
+ },
153
+ canResizeEast() {
154
+ return (
155
+ this.allowResize === true ||
156
+ (typeof this.allowResize === 'string' && this.allowResize.includes('E'))
157
+ );
158
+ },
159
+ canResizeWest() {
160
+ return (
161
+ this.allowResize === true ||
162
+ (typeof this.allowResize === 'string' && this.allowResize.includes('W'))
163
+ );
164
+ },
165
+
166
+ shouldShowHeader() {
167
+ if (this.showHeader === null) {
168
+ return Boolean(this.title || this.$slots.header);
169
+ }
170
+
171
+ return !!this.showHeader;
172
+ },
173
+ validBounds() {
174
+ // default validation constrains to parent/window bounds
175
+ const validated = this.validateBounds(this.localBounds);
176
+ return validated;
177
+ },
178
+ },
179
+ watch: {
180
+ bounds(nv) {
181
+ if (nv && Object.keys(nv).length > 0) {
182
+ this.localBounds = { ...nv };
183
+ }
184
+ },
185
+
186
+ validBounds(nv, ov) {
187
+ if (JSON.stringify(nv) !== JSON.stringify(ov)) {
188
+ this.$emit('update:bounds', nv);
189
+ }
190
+ },
191
+ },
192
+ mounted() {
193
+ // listen for window resize if teleported to body
194
+ this.addPanelDragHandler();
195
+ this.addPanelResizeHandlers();
196
+ this.addContainerResizeHandlers();
197
+
198
+ this.$nextTick(() => {
199
+ this.updateContainerSize();
200
+ this.localBounds = {
201
+ x: 0,
202
+ y: 0,
203
+ width: null,
204
+ height: null,
205
+ ...(this.bounds || {}),
206
+ };
207
+ this.ready = true;
208
+ });
209
+ },
210
+ beforeDestroy() {
211
+ this.removeContainerResizeHandlers();
212
+ if (this.dragHandler) {
213
+ this.dragHandler.disabled = true;
214
+ this.dragHandler = null;
215
+ }
216
+ },
217
+ methods: {
218
+ updateBounds(newBounds) {
219
+ this.localBounds = {
220
+ ...this.localBounds,
221
+ ...newBounds,
222
+ };
223
+ },
224
+
225
+ validateBounds(bounds) {
226
+ if (!bounds) {
227
+ return null;
228
+ }
229
+
230
+ const validated = { ...bounds };
231
+
232
+ // Always ensure minimum dimensions
233
+ if (validated.width != null) {
234
+ validated.width = Math.max(validated.width, this.minWidth || 0);
235
+ }
236
+ if (validated.height != null) {
237
+ validated.height = Math.max(validated.height, this.minHeight || 0);
238
+ }
239
+
240
+ // Constrain to container/window bounds
241
+ if (validated.x != null) {
242
+ validated.x = Math.max(
243
+ 0,
244
+ Math.min(
245
+ validated.x,
246
+ this.containerSize.width - (validated.width || this.minWidth),
247
+ ),
248
+ );
249
+ }
250
+ if (validated.y != null) {
251
+ validated.y = Math.max(
252
+ 0,
253
+ Math.min(
254
+ validated.y,
255
+ this.containerSize.height - (validated.height || this.minHeight),
256
+ ),
257
+ );
258
+ }
259
+
260
+ return validated;
261
+ },
262
+
263
+ updateContainerSize() {
264
+ // window size
265
+ if (this.teleportToBody) {
266
+ this.containerSize = {
267
+ width: document.documentElement.clientWidth,
268
+ height: document.documentElement.clientHeight,
269
+ };
270
+ }
271
+
272
+ // parent size
273
+ else if (this.$el && this.$el.parentElement) {
274
+ const parentRect = this.$el.parentElement.getBoundingClientRect();
275
+ this.containerSize = {
276
+ width: parentRect.width,
277
+ height: parentRect.height,
278
+ };
279
+ }
280
+
281
+ // no parent or window
282
+ else {
283
+ this.containerSize = {
284
+ width: 0,
285
+ height: 0,
286
+ };
287
+ }
288
+ },
289
+
290
+ addContainerResizeHandlers() {
291
+ if (this.teleportToBody) {
292
+ window.addEventListener('resize', this.updateContainerSize);
293
+ } else {
294
+ if (typeof ResizeObserver === 'undefined' || !this.$el || !this.$el.parentElement) {
295
+ return;
296
+ }
297
+
298
+ this.resizeObserver = new ResizeObserver(entries => {
299
+ this.updateContainerSize();
300
+ });
301
+
302
+ // Start observing the parent element
303
+ this.resizeObserver.observe(this.$el.parentElement);
304
+ }
305
+ },
306
+
307
+ removeContainerResizeHandlers() {
308
+ if (this.teleportToBody) {
309
+ window.removeEventListener('resize', this.handleWindowResize);
310
+ } else if (this.resizeObserver) {
311
+ this.resizeObserver.disconnect();
312
+ this.resizeObserver = null;
313
+ }
314
+ },
315
+
316
+ addPanelDragHandler() {
317
+ const panel = this.$refs.panel;
318
+ if (!panel) return;
319
+
320
+ const dragElement = panel.querySelector('[data-tool-panel-drag="true"]');
321
+ if (!dragElement) return;
322
+
323
+ let startBounds = null;
324
+
325
+ this.dragHandler = new Drag(dragElement, {
326
+ onStart: () => {
327
+ if (!this.allowDrag) return;
328
+ startBounds = { ...this.validBounds };
329
+ this.$emit('dragstart', startBounds);
330
+ },
331
+ onDrag: ev => {
332
+ if (!this.allowDrag) return;
333
+ if (!startBounds) return;
334
+
335
+ // Calculate new position
336
+ const newX = startBounds.x + ev.startDelta.x;
337
+ const newY = startBounds.y + ev.startDelta.y;
338
+
339
+ // get the panel's actual dimensions (explicity or content size)
340
+ const panelHeight = this.validBounds.height || panel.offsetHeight;
341
+ const panelWidth = this.validBounds.width || panel.offsetWidth;
342
+
343
+ // constrain to current window bounds
344
+ const constrainedX = Math.max(
345
+ 0,
346
+ Math.min(newX, this.containerSize.width - panelWidth),
347
+ );
348
+ const constrainedY = Math.max(
349
+ 0,
350
+ Math.min(newY, this.containerSize.height - panelHeight),
351
+ );
352
+
353
+ const newBounds = this.validateBounds({
354
+ ...this.validBounds,
355
+ x: constrainedX,
356
+ y: constrainedY,
357
+ });
358
+
359
+ this.localBounds = newBounds;
360
+ this.$emit('drag', newBounds);
361
+ },
362
+ onEnd: () => {
363
+ if (!this.allowDrag) return;
364
+
365
+ if (startBounds) {
366
+ this.$emit('dragend', this.validBounds);
367
+ }
368
+ startBounds = null;
369
+ },
370
+ });
371
+ },
372
+
373
+ addPanelResizeHandlers() {
374
+ const panel = this.$refs.panel;
375
+ if (!panel) return;
376
+
377
+ const handles = panel.querySelectorAll('[data-resize-handle]');
378
+ if (!handles.length) return;
379
+
380
+ handles.forEach(handle => {
381
+ const direction = handle.getAttribute('data-resize-handle');
382
+ const isEast = direction.includes('e');
383
+ const isWest = direction.includes('w');
384
+ const isNorth = direction.includes('n');
385
+ const isSouth = direction.includes('s');
386
+ let startBounds = null;
387
+
388
+ new Drag(handle, {
389
+ onStart: () => {
390
+ startBounds = { ...this.validBounds };
391
+ if (startBounds.height == null) {
392
+ startBounds.height = panel.offsetHeight;
393
+ }
394
+ if (startBounds.width == null) {
395
+ startBounds.width = panel.offsetWidth;
396
+ }
397
+
398
+ this.$emit('resizestart', startBounds);
399
+ },
400
+ onDrag: ev => {
401
+ if (!startBounds) return;
402
+
403
+ const newBounds = { ...startBounds };
404
+ const minX = 0;
405
+ const minY = 0;
406
+ const maxX = this.containerSize.width;
407
+ const maxY = this.containerSize.height;
408
+
409
+ // Handle horizontal resizing
410
+ if (isEast || isWest) {
411
+ if (isWest) {
412
+ // Resizing from left edge
413
+ const newX = Math.max(minX, startBounds.x + ev.startDelta.x);
414
+ const maxWidth = startBounds.x + startBounds.width - newX;
415
+ newBounds.x = newX;
416
+ newBounds.width = Math.min(
417
+ startBounds.width - (newX - startBounds.x),
418
+ maxWidth,
419
+ );
420
+ } else {
421
+ // Resizing from right edge
422
+ const maxWidth = maxX - startBounds.x;
423
+ newBounds.width = Math.min(
424
+ startBounds.width + ev.startDelta.x,
425
+ maxWidth,
426
+ );
427
+ }
428
+ }
429
+
430
+ // Handle vertical resizing
431
+ if (isNorth || isSouth) {
432
+ if (isNorth) {
433
+ // Resizing from top edge
434
+ const newY = Math.max(minY, startBounds.y + ev.startDelta.y);
435
+ const maxHeight = startBounds.y + startBounds.height - newY;
436
+ newBounds.y = newY;
437
+ newBounds.height = Math.min(
438
+ startBounds.height - (newY - startBounds.y),
439
+ maxHeight,
440
+ );
441
+ } else {
442
+ // Resizing from bottom edge
443
+ const maxHeight = maxY - startBounds.y;
444
+ newBounds.height = Math.min(
445
+ startBounds.height + ev.startDelta.y,
446
+ maxHeight,
447
+ );
448
+ }
449
+ }
450
+
451
+ // Only apply minimum dimensions to non-null values
452
+ if (newBounds.width != null) {
453
+ newBounds.width = Math.max(newBounds.width, this.minWidth || 0);
454
+ }
455
+ if (newBounds.height != null) {
456
+ newBounds.height = Math.max(newBounds.height, this.minHeight || 0);
457
+ }
458
+
459
+ this.updateBounds(newBounds);
460
+ this.$emit('resize', this.validBounds);
461
+ },
462
+ onEnd: () => {
463
+ if (startBounds) {
464
+ this.$emit('resizeend', this.validBounds);
465
+ }
466
+ startBounds = null;
467
+ },
468
+ });
469
+ });
470
+ },
471
+ getPanelElement() {
472
+ return this.$refs.panel;
473
+ },
474
+ },
475
+ };
476
+ </script>
477
+
478
+ <style lang="scss">
479
+ @use '../styles/px.scss' as *;
480
+
481
+ .px-tool-panel {
482
+ // panel
483
+ --px-tool-panel-bg: #{get('controls.popup.background-color')};
484
+ --px-tool-panel-padding: 0.75em;
485
+ --px-tool-panel-radius: #{get('controls.popup.border-radius')};
486
+ --px-tool-panel-shadow: #{get('controls.popup.box-shadow')};
487
+
488
+ // header
489
+ --px-tool-panel-header-bg: transparent;
490
+ --px-tool-panel-header-padding: 0.5em 0.75em;
491
+ --px-tool-panel-header-border-color: transparent;
492
+
493
+ // resize
494
+ --px-tool-panel-resize-width: 8px;
495
+ --px-tool-panel-resize-handle-hover-bg: #{rgba(0, 0, 0, 0)};
496
+
497
+ //--px-tool-panel-bg: var(--theme-page-bg, rgba(255, 255, 255, 0.9));
498
+
499
+ position: absolute;
500
+ border-radius: var(--px-tool-panel-radius);
501
+ box-shadow: var(--px-tool-panel-shadow);
502
+ background-color: var(--px-tool-panel-bg);
503
+ box-sizing: border-box;
504
+ display: flex;
505
+ flex-direction: column;
506
+
507
+ &--teleport {
508
+ position: fixed;
509
+ }
510
+
511
+ &__header {
512
+ padding: var(--px-tool-panel-header-padding);
513
+ background-color: var(--px-tool-panel-header-bg);
514
+ border-bottom: 1px solid var(--px-tool-panel-header-border-color);
515
+ user-select: none;
516
+ display: flex;
517
+ align-items: center;
518
+ }
519
+
520
+ [data-tool-panel-drag='true'] {
521
+ cursor: move;
522
+ }
523
+
524
+ &__content {
525
+ position: relative;
526
+ flex: 1;
527
+ min-height: 0;
528
+ padding: var(--px-tool-panel-padding);
529
+ }
530
+
531
+ &__resize-handle {
532
+ position: absolute;
533
+ background-color: transparent;
534
+ z-index: 1;
535
+
536
+ &--n {
537
+ top: calc(-1 * var(--px-tool-panel-resize-width) / 2);
538
+ left: 0;
539
+ right: 0;
540
+ height: var(--px-tool-panel-resize-width);
541
+ cursor: ns-resize;
542
+ }
543
+
544
+ &--s {
545
+ bottom: calc(-1 * var(--px-tool-panel-resize-width) / 2);
546
+ left: 0;
547
+ right: 0;
548
+ height: var(--px-tool-panel-resize-width);
549
+ cursor: ns-resize;
550
+ }
551
+
552
+ &--e {
553
+ top: 0;
554
+ right: calc(-1 * var(--px-tool-panel-resize-width) / 2);
555
+ bottom: 0;
556
+ width: var(--px-tool-panel-resize-width);
557
+ cursor: ew-resize;
558
+ }
559
+
560
+ &--w {
561
+ top: 0;
562
+ left: calc(-1 * var(--px-tool-panel-resize-width) / 2);
563
+ bottom: 0;
564
+ width: var(--px-tool-panel-resize-width);
565
+ cursor: ew-resize;
566
+ }
567
+
568
+ /* Corner handles - small squares */
569
+ &--ne {
570
+ top: calc(-1 * var(--px-tool-panel-resize-width) / 2);
571
+ right: calc(-1 * var(--px-tool-panel-resize-width) / 2);
572
+ width: var(--px-tool-panel-resize-width);
573
+ height: var(--px-tool-panel-resize-width);
574
+ cursor: nesw-resize;
575
+ }
576
+
577
+ &--nw {
578
+ top: calc(-1 * var(--px-tool-panel-resize-width) / 2);
579
+ left: calc(-1 * var(--px-tool-panel-resize-width) / 2);
580
+ width: var(--px-tool-panel-resize-width);
581
+ height: var(--px-tool-panel-resize-width);
582
+ cursor: nesw-resize;
583
+ }
584
+
585
+ &--se {
586
+ bottom: calc(-1 * var(--px-tool-panel-resize-width) / 2);
587
+ right: calc(-1 * var(--px-tool-panel-resize-width) / 2);
588
+ width: var(--px-tool-panel-resize-width);
589
+ height: var(--px-tool-panel-resize-width);
590
+ cursor: nwse-resize;
591
+ }
592
+
593
+ &--sw {
594
+ bottom: calc(-1 * var(--px-tool-panel-resize-width) / 2);
595
+ left: calc(-1 * var(--px-tool-panel-resize-width) / 2);
596
+ width: var(--px-tool-panel-resize-width);
597
+ height: var(--px-tool-panel-resize-width);
598
+ cursor: nesw-resize;
599
+ }
600
+
601
+ &:hover {
602
+ background-color: var(--px-tool-panel-resize-handle-hover-bg);
603
+ }
604
+ }
605
+ }
606
+ </style>
@@ -15,18 +15,37 @@ import { deepMerge } from '../utils/utils.js';
15
15
  * }
16
16
  */
17
17
 
18
- export default function useSettings(key, defaultSettings = {}, onInit = null) {
18
+ export default function useSettings(
19
+ key,
20
+ defaultSettings = {},
21
+ beforeInit = null,
22
+ afterInit = null,
23
+ ) {
19
24
  const settings = ref(defaultSettings);
20
25
 
21
26
  const loadSettings = () => {
22
27
  if (process.client) {
23
- const stored = localStorage.getItem(key);
24
- const parsed = stored ? JSON.parse(stored) : {};
28
+ let parsed = {};
29
+ try {
30
+ const stored = localStorage.getItem(key);
31
+ parsed = stored ? JSON.parse(stored) : {};
32
+
33
+ if (typeof parsed !== 'object' || parsed === null) {
34
+ parsed = {};
35
+ }
36
+ } catch (e) {
37
+ console.error('Error loading settings:', e);
38
+ parsed = {};
39
+ }
40
+
41
+ if (beforeInit && typeof beforeInit === 'function') {
42
+ beforeInit(settings, parsed);
43
+ }
25
44
 
26
45
  settings.value = deepMerge(settings.value, parsed, true);
27
46
 
28
- if (onInit && typeof onInit === 'function') {
29
- onInit(settings.value);
47
+ if (afterInit && typeof afterInit === 'function') {
48
+ afterInit(settings);
30
49
  }
31
50
  }
32
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "4.1.13",
3
+ "version": "4.1.14",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": {
6
6
  "name": "Pixel Lab"