@vanduo-oss/framework 1.3.2 → 1.3.3

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.
@@ -18,6 +18,8 @@
18
18
  touchState: null,
19
19
  // Feedback element
20
20
  feedbackElement: null,
21
+ // Shared selector used by init and touch reorder
22
+ containerSelector: '.vd-draggable-container, .vd-draggable-container-vertical',
21
23
 
22
24
  /**
23
25
  * Initialize draggable components
@@ -32,7 +34,7 @@
32
34
  this.initDraggable(element);
33
35
  });
34
36
 
35
- const containers = document.querySelectorAll('.vd-draggable-container, .vd-draggable-container-vertical');
37
+ const containers = document.querySelectorAll(this.containerSelector);
36
38
  containers.forEach(container => {
37
39
  if (!this.instances.has(container)) {
38
40
  this.initContainer(container);
@@ -369,10 +371,16 @@
369
371
  // Don't prevent default here — it blocks scrolling.
370
372
  // We only prevent default in touchmove once drag threshold is reached.
371
373
  const touch = e.touches[0];
374
+ const rect = element.getBoundingClientRect();
372
375
  this.touchState = {
373
376
  element: element,
374
377
  startX: touch.clientX,
375
378
  startY: touch.clientY,
379
+ lastX: touch.clientX,
380
+ lastY: touch.clientY,
381
+ // Keep preview anchored to the original grab point.
382
+ offsetX: touch.clientX - rect.left,
383
+ offsetY: touch.clientY - rect.top,
376
384
  startTime: Date.now(),
377
385
  isDragging: false
378
386
  };
@@ -387,6 +395,8 @@
387
395
  if (!this.touchState) return;
388
396
 
389
397
  const touch = e.touches[0];
398
+ this.touchState.lastX = touch.clientX;
399
+ this.touchState.lastY = touch.clientY;
390
400
  const deltaX = touch.clientX - this.touchState.startX;
391
401
  const deltaY = touch.clientY - this.touchState.startY;
392
402
 
@@ -405,7 +415,10 @@
405
415
  element: element,
406
416
  initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
407
417
  initialBounds: element.getBoundingClientRect(),
408
- data: this.getData(element)
418
+ data: this.getData(element),
419
+ // Preserve where inside the element the drag started for accurate ghost positioning.
420
+ offsetX: this.touchState.offsetX,
421
+ offsetY: this.touchState.offsetY
409
422
  };
410
423
 
411
424
  // Dispatch event
@@ -434,8 +447,10 @@
434
447
  }
435
448
  }));
436
449
 
450
+ this.updateTouchDropZone(touch.clientX, touch.clientY);
451
+
437
452
  // Reorder for touch
438
- const container = element.closest('.vd-draggable-container');
453
+ const container = element.closest(this.containerSelector);
439
454
  if (container && container.contains(element)) {
440
455
  this.handleReorder(container, element, touch.clientX, touch.clientY);
441
456
  }
@@ -451,6 +466,18 @@
451
466
  handleTouchEnd: function (e, element) {
452
467
  if (this.touchState && this.touchState.isDragging) {
453
468
  if (e.cancelable) e.preventDefault();
469
+ const endTouch = e.changedTouches?.[0];
470
+ const endPosition = {
471
+ x: endTouch?.clientX ?? this.touchState.lastX ?? this.touchState.startX,
472
+ y: endTouch?.clientY ?? this.touchState.lastY ?? this.touchState.startY
473
+ };
474
+
475
+ const dropZone = this.resolveDropZoneAtPoint(endPosition.x, endPosition.y) || this.touchState.overZone;
476
+ if (dropZone) {
477
+ this.dispatchDrop(dropZone, endPosition);
478
+ } else if (this.touchState.overZone) {
479
+ this.touchState.overZone.classList.remove('is-drag-over');
480
+ }
454
481
 
455
482
  element.classList.remove('is-dragging');
456
483
  element.classList.add('is-dropped');
@@ -463,7 +490,6 @@
463
490
  }
464
491
 
465
492
  // Dispatch event
466
- const endTouch = e.changedTouches[0];
467
493
  const data = this.currentDrag?.data || this.getData(element);
468
494
  const startX = this.touchState?.startX || 0;
469
495
  const startY = this.touchState?.startY || 0;
@@ -473,10 +499,10 @@
473
499
  detail: {
474
500
  element: element,
475
501
  data: data,
476
- position: { x: endTouch.clientX, y: endTouch.clientY },
502
+ position: endPosition,
477
503
  delta: {
478
- x: endTouch.clientX - startX,
479
- y: endTouch.clientY - startY
504
+ x: endPosition.x - startX,
505
+ y: endPosition.y - startY
480
506
  }
481
507
  }
482
508
  }));
@@ -523,16 +549,79 @@
523
549
  */
524
550
  handleDrop: function (e, zone) {
525
551
  e.preventDefault();
526
- zone.classList.remove('is-drag-over');
552
+ this.dispatchDrop(zone, { x: e.clientX, y: e.clientY });
553
+ },
527
554
 
528
- // Dispatch event
555
+ /**
556
+ * Resolve a drop zone from viewport coordinates
557
+ * @param {number} x
558
+ * @param {number} y
559
+ * @returns {HTMLElement|null}
560
+ */
561
+ resolveDropZoneAtPoint: function (x, y) {
562
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
563
+
564
+ // Prefer the full stacking list so overlays/top elements don't hide real drop targets.
565
+ if (typeof document.elementsFromPoint === 'function') {
566
+ const stacked = document.elementsFromPoint(x, y);
567
+ for (const element of stacked) {
568
+ const zone = element.closest('.vd-drop-zone');
569
+ if (zone) return zone;
570
+ }
571
+ }
572
+
573
+ const target = document.elementFromPoint(x, y);
574
+ const targetZone = target ? target.closest('.vd-drop-zone') : null;
575
+ if (targetZone) return targetZone;
576
+
577
+ // Last-resort fallback for mobile emulation edge cases.
578
+ const zones = document.querySelectorAll('.vd-drop-zone');
579
+ for (const zone of zones) {
580
+ const rect = zone.getBoundingClientRect();
581
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
582
+ return zone;
583
+ }
584
+ }
585
+
586
+ return null;
587
+ },
588
+
589
+ /**
590
+ * Track and update active drop-zone hover state on touch devices
591
+ * @param {number} x
592
+ * @param {number} y
593
+ */
594
+ updateTouchDropZone: function (x, y) {
595
+ if (!this.touchState) return;
596
+
597
+ const nextZone = this.resolveDropZoneAtPoint(x, y);
598
+ const prevZone = this.touchState.overZone || null;
599
+
600
+ if (prevZone && prevZone !== nextZone) {
601
+ prevZone.classList.remove('is-drag-over');
602
+ }
603
+
604
+ if (nextZone && nextZone !== prevZone) {
605
+ nextZone.classList.add('is-drag-over');
606
+ }
607
+
608
+ this.touchState.overZone = nextZone || null;
609
+ },
610
+
611
+ /**
612
+ * Dispatch a normalized drop event for mouse and touch flows
613
+ * @param {HTMLElement} zone
614
+ * @param {{x:number, y:number}} position
615
+ */
616
+ dispatchDrop: function (zone, position) {
617
+ zone.classList.remove('is-drag-over');
529
618
  zone.dispatchEvent(new CustomEvent('draggable:drop', {
530
619
  bubbles: true,
531
620
  detail: {
532
621
  zone: zone,
533
622
  element: this.currentDrag?.element,
534
623
  data: this.currentDrag?.data,
535
- position: { x: e.clientX, y: e.clientY }
624
+ position: position
536
625
  }
537
626
  }));
538
627
  },
@@ -650,9 +739,11 @@
650
739
  this.feedbackElement.appendChild(clone);
651
740
 
652
741
  // Set styles
742
+ const offsetX = this.currentDrag.offsetX ?? 20;
743
+ const offsetY = this.currentDrag.offsetY ?? 20;
653
744
  Object.assign(this.feedbackElement.style, {
654
- left: (x - 20) + 'px',
655
- top: (y - 20) + 'px',
745
+ left: (x - offsetX) + 'px',
746
+ top: (y - offsetY) + 'px',
656
747
  width: rect.width + 'px',
657
748
  height: rect.height + 'px'
658
749
  });
@@ -147,6 +147,7 @@
147
147
  loadPreferences: function () {
148
148
  this.state.theme = this.getStorageValue(this.STORAGE_KEYS.THEME, this.DEFAULTS.THEME);
149
149
  this.state.primary = this.getStorageValue(this.STORAGE_KEYS.PRIMARY, this.getDefaultPrimary(this.state.theme));
150
+ this._normalizeDefaultPrimaryIfStaleWithStoredTheme();
150
151
  this.state.neutral = this.getStorageValue(this.STORAGE_KEYS.NEUTRAL, this.DEFAULTS.NEUTRAL);
151
152
  this.state.radius = this.getStorageValue(this.STORAGE_KEYS.RADIUS, this.DEFAULTS.RADIUS);
152
153
  this.state.font = this.getStorageValue(this.STORAGE_KEYS.FONT, this.DEFAULTS.FONT);
@@ -254,15 +255,12 @@
254
255
  // Prevent circular updates
255
256
  this._isApplying = true;
256
257
 
257
- // Check if we should switch primary color (if using default)
258
- // Use the incoming mode for both old and new default checks since
259
- // ThemeSwitcher calls this with the new mode before updating its own state
260
- const currentMode = this.state.theme;
261
- const oldDefault = this.getDefaultPrimary(currentMode);
262
- if (this.state.primary === oldDefault) {
263
- const newDefault = this.getDefaultPrimary(mode);
264
- if (newDefault !== this.state.primary) {
265
- this.applyPrimary(newDefault);
258
+ // Re-align black/amber when they don't match the effective primary for the target mode.
259
+ // Covers theme toggles and stale localStorage (e.g. amber saved while theme is light).
260
+ if (this.isUsingDefaultPrimary()) {
261
+ const expected = this.getDefaultPrimary(mode);
262
+ if (this.state.primary !== expected) {
263
+ this.applyPrimary(expected);
266
264
  }
267
265
  }
268
266
 
@@ -581,6 +579,21 @@
581
579
  this.state.primary === this.DEFAULTS.PRIMARY_DARK;
582
580
  },
583
581
 
582
+ /**
583
+ * When primary is still one of the auto-default palette keys (black/amber) but
584
+ * localStorage was written under a different theme (or OS changed in system mode),
585
+ * align in-memory state before applyAllPreferences runs — avoids amber+light / black+dark drift.
586
+ */
587
+ _normalizeDefaultPrimaryIfStaleWithStoredTheme: function () {
588
+ if (!this.isUsingDefaultPrimary()) {
589
+ return;
590
+ }
591
+ const expected = this.getDefaultPrimary(this.state.theme);
592
+ if (this.state.primary !== expected) {
593
+ this.state.primary = expected;
594
+ }
595
+ },
596
+
584
597
  bindEvents: function () {
585
598
  // Trigger click - bind to any trigger button
586
599
  if (this.elements.trigger) {
@@ -103,6 +103,10 @@
103
103
  if (this.state.preference === 'system') {
104
104
  // Re-apply (effectively just to ensure consistency, though removing attribute usually suffices)
105
105
  this.applyTheme();
106
+ // Keep default primary (black/amber) aligned when OS scheme changes while in system mode
107
+ if (window.ThemeCustomizer && typeof window.ThemeCustomizer.applyTheme === 'function' && !window.ThemeCustomizer._isApplying) {
108
+ window.ThemeCustomizer.applyTheme('system');
109
+ }
106
110
  }
107
111
  };
108
112
  this._mediaQuery.addEventListener('change', this._onMediaChange);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanduo-oss/framework",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Zero-dependency CSS/JS framework built on Fibonacci/Golden Ratio design system with Open Color integration",
5
5
  "keywords": [
6
6
  "css",