vanduo-framework 1.1.8 → 1.2.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,4 +1,4 @@
1
- /*! Vanduo v1.1.6 | Built: 2026-02-17T18:38:55.867Z | git:c40d96d | development */
1
+ /*! Vanduo v1.2.0 | Built: 2026-02-22T21:30:31.940Z | git:64c88fd | development */
2
2
 
3
3
  // js/utils/lifecycle.js
4
4
  (function() {
@@ -108,7 +108,7 @@
108
108
  (function() {
109
109
  "use strict";
110
110
  const Vanduo2 = {
111
- version: "1.1.6",
111
+ version: "1.2.0",
112
112
  components: {},
113
113
  /**
114
114
  * Initialize framework
@@ -143,7 +143,7 @@
143
143
  }
144
144
  }
145
145
  });
146
- console.log("Vanduo Framework v1.1.6 initialized");
146
+ console.log("Vanduo Framework v1.2.0 initialized");
147
147
  },
148
148
  /**
149
149
  * Register a component
@@ -158,7 +158,7 @@
158
158
  * @param {string} name - Component name
159
159
  */
160
160
  reinit: function(name) {
161
- var component = this.components[name];
161
+ const component = this.components[name];
162
162
  if (component && component.init && typeof component.init === "function") {
163
163
  try {
164
164
  component.init();
@@ -172,9 +172,9 @@
172
172
  * Uses lifecycle manager for memory leak prevention
173
173
  */
174
174
  destroyAll: function() {
175
- var names = Object.keys(this.components);
176
- for (var i = 0; i < names.length; i++) {
177
- var component = this.components[names[i]];
175
+ const names = Object.keys(this.components);
176
+ for (let i = 0; i < names.length; i++) {
177
+ const component = this.components[names[i]];
178
178
  if (component && component.destroyAll && typeof component.destroyAll === "function") {
179
179
  try {
180
180
  component.destroyAll();
@@ -491,7 +491,9 @@
491
491
  html = this.formatHtml(html);
492
492
  html = this.escapeHtml(html);
493
493
  html = this.highlightHtml(html);
494
- pane.innerHTML = "<code>" + html + "</code>";
494
+ const codeEl = document.createElement("code");
495
+ codeEl.innerHTML = html;
496
+ pane.replaceChildren(codeEl);
495
497
  pane.dataset.extracted = "true";
496
498
  },
497
499
  /**
@@ -596,7 +598,7 @@
596
598
  }
597
599
  const codeWrapper = document.createElement("div");
598
600
  codeWrapper.className = "vd-code-snippet-code";
599
- codeWrapper.innerHTML = code.outerHTML;
601
+ codeWrapper.appendChild(code.cloneNode(true));
600
602
  code.parentNode.removeChild(code);
601
603
  pane.appendChild(lineNumbers);
602
604
  pane.appendChild(codeWrapper);
@@ -1362,20 +1364,20 @@
1362
1364
  // js/components/grid.js
1363
1365
  (function() {
1364
1366
  "use strict";
1365
- var supportsHas = (function() {
1367
+ const supportsHas = (function() {
1366
1368
  try {
1367
1369
  return CSS.supports("selector(:has(*))");
1368
1370
  } catch (_e) {
1369
1371
  return false;
1370
1372
  }
1371
1373
  })();
1372
- var GridLayout = {
1374
+ const GridLayout = {
1373
1375
  instances: /* @__PURE__ */ new Map(),
1374
1376
  /**
1375
1377
  * Initialize all grid layout containers
1376
1378
  */
1377
1379
  init: function() {
1378
- var containers = document.querySelectorAll("[data-layout-mode]");
1380
+ const containers = document.querySelectorAll("[data-layout-mode]");
1379
1381
  containers.forEach(function(container) {
1380
1382
  if (this.instances.has(container)) {
1381
1383
  return;
@@ -1389,8 +1391,8 @@
1389
1391
  * @param {HTMLElement} container - Element with data-layout-mode
1390
1392
  */
1391
1393
  initContainer: function(container) {
1392
- var mode = container.getAttribute("data-layout-mode") || "standard";
1393
- var cleanupFunctions = [];
1394
+ const mode = container.getAttribute("data-layout-mode") || "standard";
1395
+ const cleanupFunctions = [];
1394
1396
  this.applyMode(container, mode);
1395
1397
  container.setAttribute("role", "region");
1396
1398
  container.setAttribute("aria-label", "Grid layout: " + mode + " mode");
@@ -1403,15 +1405,15 @@
1403
1405
  * Initialize toggle buttons that target grid containers
1404
1406
  */
1405
1407
  initToggleButtons: function() {
1406
- var toggleButtons = document.querySelectorAll("[data-grid-toggle]");
1408
+ const toggleButtons = document.querySelectorAll("[data-grid-toggle]");
1407
1409
  toggleButtons.forEach(function(button) {
1408
1410
  if (button.getAttribute("data-grid-initialized") === "true") {
1409
1411
  return;
1410
1412
  }
1411
- var clickHandler = function(e) {
1413
+ const clickHandler = function(e) {
1412
1414
  e.preventDefault();
1413
- var targetSelector = button.getAttribute("data-grid-toggle");
1414
- var target;
1415
+ const targetSelector = button.getAttribute("data-grid-toggle");
1416
+ let target;
1415
1417
  if (targetSelector) {
1416
1418
  target = document.querySelector(targetSelector);
1417
1419
  } else {
@@ -1437,10 +1439,10 @@
1437
1439
  */
1438
1440
  applyFibFallback: function(container) {
1439
1441
  if (supportsHas) return;
1440
- var rows = container.querySelectorAll(".vd-row, .row");
1442
+ const rows = container.querySelectorAll(".vd-row, .row");
1441
1443
  rows.forEach(function(row) {
1442
- var cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
1443
- var count = cols.length;
1444
+ const cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
1445
+ const count = cols.length;
1444
1446
  if (count === 1) {
1445
1447
  row.style.gridTemplateColumns = "1fr";
1446
1448
  } else if (count === 2) {
@@ -1459,7 +1461,7 @@
1459
1461
  * @param {HTMLElement} container - Grid container
1460
1462
  */
1461
1463
  removeFibFallback: function(container) {
1462
- var rows = container.querySelectorAll(".vd-row, .row");
1464
+ const rows = container.querySelectorAll(".vd-row, .row");
1463
1465
  rows.forEach(function(row) {
1464
1466
  row.style.gridTemplateColumns = "";
1465
1467
  });
@@ -1480,11 +1482,11 @@
1480
1482
  }
1481
1483
  container.setAttribute("data-layout-mode", mode);
1482
1484
  container.setAttribute("aria-label", "Grid layout: " + mode + " mode");
1483
- var toggleButtons = document.querySelectorAll("[data-grid-toggle]");
1485
+ const toggleButtons = document.querySelectorAll("[data-grid-toggle]");
1484
1486
  toggleButtons.forEach(function(btn) {
1485
- var targetSelector = btn.getAttribute("data-grid-toggle");
1487
+ const targetSelector = btn.getAttribute("data-grid-toggle");
1486
1488
  if (targetSelector && container.matches(targetSelector)) {
1487
- var isActive = mode === "fibonacci";
1489
+ const isActive = mode === "fibonacci";
1488
1490
  if (isActive) {
1489
1491
  btn.classList.add("is-active");
1490
1492
  } else {
@@ -1493,11 +1495,11 @@
1493
1495
  btn.setAttribute("aria-pressed", isActive ? "true" : "false");
1494
1496
  }
1495
1497
  });
1496
- var instance = this.instances.get(container);
1498
+ const instance = this.instances.get(container);
1497
1499
  if (instance) {
1498
1500
  instance.mode = mode;
1499
1501
  }
1500
- var event;
1502
+ let event;
1501
1503
  try {
1502
1504
  event = new CustomEvent("grid:modechange", {
1503
1505
  bubbles: true,
@@ -1524,8 +1526,8 @@
1524
1526
  container = document.querySelector(container);
1525
1527
  }
1526
1528
  if (!container) return;
1527
- var currentMode = container.getAttribute("data-layout-mode") || "standard";
1528
- var newMode = currentMode === "fibonacci" ? "standard" : "fibonacci";
1529
+ const currentMode = container.getAttribute("data-layout-mode") || "standard";
1530
+ const newMode = currentMode === "fibonacci" ? "standard" : "fibonacci";
1529
1531
  this.applyMode(container, newMode);
1530
1532
  },
1531
1533
  /**
@@ -1558,7 +1560,7 @@
1558
1560
  * @param {HTMLElement} container - Grid container
1559
1561
  */
1560
1562
  destroy: function(container) {
1561
- var instance = this.instances.get(container);
1563
+ const instance = this.instances.get(container);
1562
1564
  if (!instance) return;
1563
1565
  instance.cleanup.forEach(function(fn) {
1564
1566
  fn();
@@ -1575,7 +1577,7 @@
1575
1577
  this.instances.forEach(function(instance, container) {
1576
1578
  this.destroy(container);
1577
1579
  }.bind(this));
1578
- var toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
1580
+ const toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
1579
1581
  toggleButtons.forEach(function(button) {
1580
1582
  if (button._gridCleanup) {
1581
1583
  button._gridCleanup();
@@ -3672,9 +3674,9 @@
3672
3674
  },
3673
3675
  // Default values
3674
3676
  DEFAULTS: {
3675
- PRIMARY_LIGHT: "amber",
3677
+ PRIMARY_LIGHT: "black",
3676
3678
  PRIMARY_DARK: "amber",
3677
- NEUTRAL: "slate",
3679
+ NEUTRAL: "neutral",
3678
3680
  RADIUS: "0.5",
3679
3681
  FONT: "ubuntu",
3680
3682
  THEME: "system"
@@ -4037,21 +4039,33 @@
4037
4039
  * Generate panel HTML
4038
4040
  */
4039
4041
  getPanelHTML: function() {
4042
+ const esc = typeof escapeHtml === "function" ? escapeHtml : function(text) {
4043
+ const div = document.createElement("div");
4044
+ div.textContent = String(text ?? "");
4045
+ return div.innerHTML;
4046
+ };
4047
+ const safeColor = function(value) {
4048
+ const normalized = String(value ?? "").trim();
4049
+ if (/^(#[0-9a-fA-F]{3,8}|rgb[a]?\([^)]{1,60}\)|hsl[a]?\([^)]{1,60}\)|var\(--[a-zA-Z0-9_-]{1,40}\))$/.test(normalized)) {
4050
+ return normalized;
4051
+ }
4052
+ return "#000000";
4053
+ };
4040
4054
  let primarySwatches = "";
4041
4055
  for (const [key, value] of Object.entries(this.PRIMARY_COLORS)) {
4042
- primarySwatches += `<button class="tc-color-swatch${key === this.state.primary ? " is-active" : ""}" data-color="${key}" style="--swatch-color: ${value.color}" title="${value.name}"></button>`;
4056
+ primarySwatches += `<button class="tc-color-swatch${key === this.state.primary ? " is-active" : ""}" data-color="${esc(key)}" style="--swatch-color: ${safeColor(value.color)}" title="${esc(value.name)}"></button>`;
4043
4057
  }
4044
4058
  let neutralSwatches = "";
4045
4059
  for (const [key, value] of Object.entries(this.NEUTRAL_COLORS)) {
4046
- neutralSwatches += `<button class="tc-neutral-swatch${key === this.state.neutral ? " is-active" : ""}" data-neutral="${key}" style="--swatch-color: ${value.color}" title="${value.name}"><span>${value.name}</span></button>`;
4060
+ neutralSwatches += `<button class="tc-neutral-swatch${key === this.state.neutral ? " is-active" : ""}" data-neutral="${esc(key)}" style="--swatch-color: ${safeColor(value.color)}" title="${esc(value.name)}"><span>${esc(value.name)}</span></button>`;
4047
4061
  }
4048
4062
  let radiusButtons = "";
4049
4063
  this.RADIUS_OPTIONS.forEach((r) => {
4050
- radiusButtons += `<button class="tc-radius-btn${r === this.state.radius ? " is-active" : ""}" data-radius="${r}">${r}</button>`;
4064
+ radiusButtons += `<button class="tc-radius-btn${r === this.state.radius ? " is-active" : ""}" data-radius="${esc(r)}">${esc(r)}</button>`;
4051
4065
  });
4052
4066
  let fontOptions = "";
4053
4067
  for (const [key, value] of Object.entries(this.FONT_OPTIONS)) {
4054
- fontOptions += `<option value="${key}"${key === this.state.font ? " selected" : ""}>${value.name}</option>`;
4068
+ fontOptions += `<option value="${esc(key)}"${key === this.state.font ? " selected" : ""}>${esc(value.name)}</option>`;
4055
4069
  }
4056
4070
  const modeIcons = {
4057
4071
  "system": "ph-desktop",
@@ -4109,6 +4123,13 @@
4109
4123
  /**
4110
4124
  * Bind event listeners
4111
4125
  */
4126
+ /**
4127
+ * Check whether the current primary color is one of the auto-defaults
4128
+ * (i.e. the user hasn't explicitly picked a non-default color).
4129
+ */
4130
+ isUsingDefaultPrimary: function() {
4131
+ return this.state.primary === this.DEFAULTS.PRIMARY_LIGHT || this.state.primary === this.DEFAULTS.PRIMARY_DARK;
4132
+ },
4112
4133
  bindEvents: function() {
4113
4134
  if (this.elements.trigger) {
4114
4135
  this.addListener(this.elements.trigger, "click", (e) => {
@@ -4118,6 +4139,20 @@
4118
4139
  });
4119
4140
  }
4120
4141
  this.bindPanelEvents();
4142
+ if (window.matchMedia) {
4143
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
4144
+ const handler = () => {
4145
+ if (this.state.theme === "system" && this.isUsingDefaultPrimary()) {
4146
+ const newDefault = this.getDefaultPrimary("system");
4147
+ if (newDefault !== this.state.primary) {
4148
+ this.applyPrimary(newDefault);
4149
+ this.updateUI();
4150
+ }
4151
+ }
4152
+ };
4153
+ mq.addEventListener("change", handler);
4154
+ this._cleanup.push(() => mq.removeEventListener("change", handler));
4155
+ }
4121
4156
  this.addListener(document, "click", (e) => {
4122
4157
  if (this.state.isOpen && this.elements.customizer && !this.elements.customizer.contains(e.target)) {
4123
4158
  this.close();
@@ -4679,7 +4714,7 @@
4679
4714
  if (typeof sanitizeHtml === "function") {
4680
4715
  return sanitizeHtml(input);
4681
4716
  }
4682
- var div = document.createElement("div");
4717
+ const div = document.createElement("div");
4683
4718
  div.textContent = input || "";
4684
4719
  return div.innerHTML;
4685
4720
  },
@@ -4918,7 +4953,7 @@
4918
4953
  // js/components/doc-search.js
4919
4954
  (function() {
4920
4955
  "use strict";
4921
- var DEFAULTS = {
4956
+ const DEFAULTS = {
4922
4957
  // Behavior
4923
4958
  minQueryLength: 2,
4924
4959
  maxResults: 10,
@@ -4965,8 +5000,8 @@
4965
5000
  placeholder: "Search..."
4966
5001
  };
4967
5002
  function createSearch(options) {
4968
- var config = Object.assign({}, DEFAULTS, options || {});
4969
- var state = {
5003
+ const config = Object.assign({}, DEFAULTS, options || {});
5004
+ const state = {
4970
5005
  initialized: false,
4971
5006
  index: [],
4972
5007
  results: [],
@@ -4979,6 +5014,21 @@
4979
5014
  debounceTimer: null,
4980
5015
  boundHandlers: {}
4981
5016
  };
5017
+ function safeInvokeCallback(name, fn, ...args) {
5018
+ try {
5019
+ fn(...args);
5020
+ } catch (error) {
5021
+ console.warn('[Vanduo Search] Callback error in "' + name + '":', error);
5022
+ }
5023
+ }
5024
+ function setResultsHtml(html) {
5025
+ if (!state.resultsContainer) return;
5026
+ try {
5027
+ state.resultsContainer.innerHTML = html;
5028
+ } catch (error) {
5029
+ console.warn("[Vanduo Search] Failed to render results:", error);
5030
+ }
5031
+ }
4982
5032
  function init() {
4983
5033
  if (state.initialized) {
4984
5034
  return instance;
@@ -5020,20 +5070,20 @@
5020
5070
  });
5021
5071
  return;
5022
5072
  }
5023
- var sections = document.querySelectorAll(config.contentSelector);
5024
- var categoryMap = buildCategoryMap();
5073
+ const sections = document.querySelectorAll(config.contentSelector);
5074
+ const categoryMap = buildCategoryMap();
5025
5075
  sections.forEach(function(section) {
5026
- var id = section.id;
5076
+ const id = section.id;
5027
5077
  if (!id) return;
5028
- var titleEl = section.querySelector(config.titleSelector);
5029
- var title = titleEl ? titleEl.textContent.replace(/v[\d.]+/g, "").trim() : id;
5030
- var category = categoryMap[id] || "Documentation";
5031
- var content = extractContent(section);
5032
- var keywords = extractKeywords(section, title);
5033
- var iconEl = titleEl ? titleEl.querySelector("i.ph") : null;
5034
- var icon = "";
5078
+ const titleEl = section.querySelector(config.titleSelector);
5079
+ const title = titleEl ? titleEl.textContent.replace(/v[\d.]+/g, "").trim() : id;
5080
+ const category = categoryMap[id] || "Documentation";
5081
+ const content = extractContent(section);
5082
+ const keywords = extractKeywords(section, title);
5083
+ const iconEl = titleEl ? titleEl.querySelector("i.ph") : null;
5084
+ let icon = "";
5035
5085
  if (iconEl && iconEl.classList) {
5036
- for (var ci = 0; ci < iconEl.classList.length; ci++) {
5086
+ for (let ci = 0; ci < iconEl.classList.length; ci++) {
5037
5087
  if (iconEl.classList[ci].indexOf("ph-") === 0) {
5038
5088
  icon = iconEl.classList[ci];
5039
5089
  break;
@@ -5053,16 +5103,16 @@
5053
5103
  });
5054
5104
  }
5055
5105
  function buildCategoryMap() {
5056
- var map = {};
5057
- var currentCategory = "Documentation";
5058
- var navItems = document.querySelectorAll(config.navSelector + ", " + config.sectionSelector);
5106
+ const map = {};
5107
+ let currentCategory = "Documentation";
5108
+ const navItems = document.querySelectorAll(config.navSelector + ", " + config.sectionSelector);
5059
5109
  navItems.forEach(function(item) {
5060
5110
  if (item.classList.contains("doc-nav-section")) {
5061
5111
  currentCategory = item.textContent.trim();
5062
5112
  } else {
5063
- var href = item.getAttribute("href");
5113
+ const href = item.getAttribute("href");
5064
5114
  if (href && href.startsWith("#")) {
5065
- var id = href.substring(1);
5115
+ const id = href.substring(1);
5066
5116
  map[id] = currentCategory;
5067
5117
  }
5068
5118
  }
@@ -5070,35 +5120,35 @@
5070
5120
  return map;
5071
5121
  }
5072
5122
  function extractContent(section) {
5073
- var clone = section.cloneNode(true);
5074
- var toRemove = clone.querySelectorAll(config.excludeFromContent);
5123
+ const clone = section.cloneNode(true);
5124
+ const toRemove = clone.querySelectorAll(config.excludeFromContent);
5075
5125
  toRemove.forEach(function(el) {
5076
5126
  el.remove();
5077
5127
  });
5078
- var text = clone.textContent || "";
5128
+ let text = clone.textContent || "";
5079
5129
  text = text.replace(/\s+/g, " ").trim();
5080
5130
  return text.substring(0, config.maxContentLength);
5081
5131
  }
5082
5132
  function extractKeywords(section, title) {
5083
- var keywords = [];
5133
+ const keywords = [];
5084
5134
  title.toLowerCase().split(/\s+/).forEach(function(word) {
5085
5135
  if (word.length > 2) {
5086
5136
  keywords.push(word);
5087
5137
  }
5088
5138
  });
5089
- var codeBlocks = section.querySelectorAll("code");
5139
+ const codeBlocks = section.querySelectorAll("code");
5090
5140
  codeBlocks.forEach(function(code) {
5091
- var text = code.textContent || "";
5092
- var classMatches = text.match(/\.([\w-]+)/g);
5141
+ const text = code.textContent || "";
5142
+ const classMatches = text.match(/\.([\w-]+)/g);
5093
5143
  if (classMatches) {
5094
5144
  classMatches.forEach(function(match) {
5095
5145
  keywords.push(match.substring(1).toLowerCase());
5096
5146
  });
5097
5147
  }
5098
5148
  });
5099
- var dataAttrs = section.querySelectorAll("[data-tooltip], [data-modal]");
5149
+ const dataAttrs = section.querySelectorAll("[data-tooltip], [data-modal]");
5100
5150
  dataAttrs.forEach(function(el) {
5101
- var attrs = el.getAttributeNames().filter(function(name) {
5151
+ const attrs = el.getAttributeNames().filter(function(name) {
5102
5152
  return name.startsWith("data-");
5103
5153
  });
5104
5154
  attrs.forEach(function(attr) {
@@ -5108,7 +5158,7 @@
5108
5158
  return Array.from(new Set(keywords));
5109
5159
  }
5110
5160
  function extractKeywordsFromText(text) {
5111
- var words = text.toLowerCase().split(/\s+/);
5161
+ const words = text.toLowerCase().split(/\s+/);
5112
5162
  return words.filter(function(word) {
5113
5163
  return word.length > 2;
5114
5164
  });
@@ -5141,9 +5191,9 @@
5141
5191
  }
5142
5192
  };
5143
5193
  state.boundHandlers.handleResultClick = function(e) {
5144
- var result = e.target.closest(".vd-doc-search-result");
5194
+ const result = e.target.closest(".vd-doc-search-result");
5145
5195
  if (result) {
5146
- var index = parseInt(result.dataset.index, 10);
5196
+ const index = parseInt(result.dataset.index, 10);
5147
5197
  select(index);
5148
5198
  }
5149
5199
  };
@@ -5167,7 +5217,7 @@
5167
5217
  }
5168
5218
  }
5169
5219
  function setupAria() {
5170
- var resultsId = state.resultsContainer.id || "search-results-" + Math.random().toString(36).substr(2, 9);
5220
+ const resultsId = state.resultsContainer.id || "search-results-" + Math.random().toString(36).substr(2, 9);
5171
5221
  state.resultsContainer.id = resultsId;
5172
5222
  state.input.setAttribute("role", "combobox");
5173
5223
  state.input.setAttribute("aria-autocomplete", "list");
@@ -5177,7 +5227,7 @@
5177
5227
  state.resultsContainer.setAttribute("aria-label", "Search results");
5178
5228
  }
5179
5229
  function handleInput(e) {
5180
- var query = e.target.value.trim();
5230
+ const query = e.target.value.trim();
5181
5231
  if (state.debounceTimer) {
5182
5232
  clearTimeout(state.debounceTimer);
5183
5233
  }
@@ -5192,7 +5242,7 @@
5192
5242
  render();
5193
5243
  open();
5194
5244
  if (typeof config.onSearch === "function") {
5195
- config.onSearch(query, state.results);
5245
+ safeInvokeCallback("onSearch", config.onSearch, query, state.results);
5196
5246
  }
5197
5247
  }, config.debounceMs);
5198
5248
  }
@@ -5233,15 +5283,15 @@
5233
5283
  }
5234
5284
  }
5235
5285
  function search(query) {
5236
- var terms = query.toLowerCase().split(/\s+/).filter(function(t) {
5286
+ const terms = query.toLowerCase().split(/\s+/).filter(function(t) {
5237
5287
  return t.length > 0;
5238
5288
  });
5239
- var scored = [];
5289
+ const scored = [];
5240
5290
  state.index.forEach(function(entry) {
5241
- var score = 0;
5242
- var titleLower = entry.title.toLowerCase();
5243
- var categoryLower = entry.category.toLowerCase();
5244
- var contentLower = entry.content.toLowerCase();
5291
+ let score = 0;
5292
+ const titleLower = entry.title.toLowerCase();
5293
+ const categoryLower = entry.category.toLowerCase();
5294
+ const contentLower = entry.content.toLowerCase();
5245
5295
  terms.forEach(function(term) {
5246
5296
  if (titleLower.includes(term)) {
5247
5297
  score += 100;
@@ -5254,7 +5304,7 @@
5254
5304
  if (categoryLower.includes(term)) {
5255
5305
  score += 50;
5256
5306
  }
5257
- var keywordMatch = entry.keywords.some(function(k) {
5307
+ const keywordMatch = entry.keywords.some(function(k) {
5258
5308
  return k.includes(term);
5259
5309
  });
5260
5310
  if (keywordMatch) {
@@ -5284,19 +5334,19 @@
5284
5334
  }
5285
5335
  function render() {
5286
5336
  if (state.results.length === 0) {
5287
- state.resultsContainer.innerHTML = renderEmpty();
5337
+ setResultsHtml(renderEmpty());
5288
5338
  return;
5289
5339
  }
5290
- var html = '<ul class="vd-doc-search-results-list" role="listbox">';
5340
+ let html = '<ul class="vd-doc-search-results-list" role="listbox">';
5291
5341
  state.results.forEach(function(result, index) {
5292
- var isActive = index === state.activeIndex;
5293
- var icon = result.icon || getCategoryIcon(result.categorySlug);
5294
- var excerpt = getExcerpt(result.content, state.query);
5342
+ const isActive = index === state.activeIndex;
5343
+ const icon = result.icon || getCategoryIcon(result.categorySlug);
5344
+ const excerpt = getExcerpt(result.content, state.query);
5295
5345
  html += '<li class="vd-doc-search-result' + (isActive ? " is-active" : "") + '" role="option" id="vd-doc-search-result-' + index + '" data-index="' + index + '" data-category="' + escapeHtml2(result.categorySlug) + '" aria-selected="' + isActive + '"><div class="vd-doc-search-result-icon"><i class="ph ' + escapeHtml2(icon) + '"></i></div><div class="vd-doc-search-result-content"><div class="vd-doc-search-result-title">' + highlight(result.title, state.query) + '</div><div class="vd-doc-search-result-category">' + escapeHtml2(result.category) + '</div><div class="vd-doc-search-result-excerpt">' + highlight(excerpt, state.query) + "</div></div></li>";
5296
5346
  });
5297
5347
  html += "</ul>";
5298
5348
  html += renderFooter();
5299
- state.resultsContainer.innerHTML = html;
5349
+ setResultsHtml(html);
5300
5350
  }
5301
5351
  function renderEmpty() {
5302
5352
  return '<div class="vd-doc-search-empty"><div class="vd-doc-search-empty-icon"><i class="ph ph-magnifying-glass"></i></div><div class="vd-doc-search-empty-title">' + escapeHtml2(config.emptyTitle) + '</div><div class="vd-doc-search-empty-text">' + escapeHtml2(config.emptyText) + "</div></div>";
@@ -5308,12 +5358,12 @@
5308
5358
  return config.categoryIcons[categorySlug] || config.categoryIcons["default"] || "ph-file-text";
5309
5359
  }
5310
5360
  function getExcerpt(content, query) {
5311
- var terms = query.toLowerCase().split(/\s+/);
5312
- var contentLower = content.toLowerCase();
5313
- var excerptLength = 100;
5314
- var matchPos = -1;
5315
- for (var i = 0; i < terms.length; i++) {
5316
- var pos = contentLower.indexOf(terms[i]);
5361
+ const terms = query.toLowerCase().split(/\s+/);
5362
+ const contentLower = content.toLowerCase();
5363
+ const excerptLength = 100;
5364
+ let matchPos = -1;
5365
+ for (let i = 0; i < terms.length; i++) {
5366
+ const pos = contentLower.indexOf(terms[i]);
5317
5367
  if (pos !== -1 && (matchPos === -1 || pos < matchPos)) {
5318
5368
  matchPos = pos;
5319
5369
  }
@@ -5321,9 +5371,9 @@
5321
5371
  if (matchPos === -1) {
5322
5372
  return content.substring(0, excerptLength) + "...";
5323
5373
  }
5324
- var start = Math.max(0, matchPos - 30);
5325
- var end = Math.min(content.length, matchPos + excerptLength);
5326
- var excerpt = content.substring(start, end);
5374
+ const start = Math.max(0, matchPos - 30);
5375
+ const end = Math.min(content.length, matchPos + excerptLength);
5376
+ let excerpt = content.substring(start, end);
5327
5377
  if (start > 0) {
5328
5378
  excerpt = "..." + excerpt;
5329
5379
  }
@@ -5334,24 +5384,24 @@
5334
5384
  }
5335
5385
  function highlight(text, query) {
5336
5386
  if (!query) return escapeHtml2(text);
5337
- var terms = query.toLowerCase().split(/\s+/).filter(function(t) {
5387
+ const terms = query.toLowerCase().split(/\s+/).filter(function(t) {
5338
5388
  return t.length > 0;
5339
5389
  });
5340
- var escaped = escapeHtml2(text);
5390
+ let escaped = escapeHtml2(text);
5341
5391
  terms.forEach(function(term) {
5342
5392
  if (term.length > 50) return;
5343
- var regex = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
5393
+ const regex = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
5344
5394
  escaped = escaped.replace(regex, "<" + config.highlightTag + ">$1</" + config.highlightTag + ">");
5345
5395
  });
5346
5396
  return escaped;
5347
5397
  }
5348
5398
  function escapeHtml2(text) {
5349
- var div = document.createElement("div");
5399
+ const div = document.createElement("div");
5350
5400
  div.textContent = text;
5351
5401
  return div.innerHTML;
5352
5402
  }
5353
5403
  function navigate(direction) {
5354
- var newIndex = state.activeIndex + direction;
5404
+ let newIndex = state.activeIndex + direction;
5355
5405
  if (newIndex < 0) {
5356
5406
  newIndex = state.results.length - 1;
5357
5407
  } else if (newIndex >= state.results.length) {
@@ -5360,13 +5410,13 @@
5360
5410
  setActiveIndex(newIndex);
5361
5411
  }
5362
5412
  function setActiveIndex(index) {
5363
- var prevActive = state.resultsContainer.querySelector(".vd-doc-search-result.is-active");
5413
+ const prevActive = state.resultsContainer.querySelector(".vd-doc-search-result.is-active");
5364
5414
  if (prevActive) {
5365
5415
  prevActive.classList.remove("is-active");
5366
5416
  prevActive.setAttribute("aria-selected", "false");
5367
5417
  }
5368
5418
  state.activeIndex = index;
5369
- var newActive = state.resultsContainer.querySelector('[data-index="' + index + '"]');
5419
+ const newActive = state.resultsContainer.querySelector('[data-index="' + index + '"]');
5370
5420
  if (newActive) {
5371
5421
  newActive.classList.add("is-active");
5372
5422
  newActive.setAttribute("aria-selected", "true");
@@ -5375,16 +5425,16 @@
5375
5425
  }
5376
5426
  }
5377
5427
  function select(index) {
5378
- var result = state.results[index];
5428
+ const result = state.results[index];
5379
5429
  if (!result) return;
5380
5430
  close();
5381
5431
  state.input.value = "";
5382
5432
  state.query = "";
5383
5433
  if (typeof config.onSelect === "function") {
5384
- config.onSelect(result);
5434
+ safeInvokeCallback("onSelect", config.onSelect, result);
5385
5435
  return;
5386
5436
  }
5387
- var section = document.querySelector(result.url);
5437
+ const section = document.querySelector(result.url);
5388
5438
  if (section) {
5389
5439
  section.scrollIntoView({ behavior: "smooth", block: "start" });
5390
5440
  window.history.pushState(null, "", result.url);
@@ -5392,7 +5442,7 @@
5392
5442
  }
5393
5443
  }
5394
5444
  function updateSidebarActive(sectionId) {
5395
- var navLinks = document.querySelectorAll(config.navSelector);
5445
+ const navLinks = document.querySelectorAll(config.navSelector);
5396
5446
  navLinks.forEach(function(link) {
5397
5447
  link.classList.remove("active");
5398
5448
  if (link.getAttribute("href") === "#" + sectionId) {
@@ -5406,7 +5456,7 @@
5406
5456
  state.resultsContainer.classList.add("is-open");
5407
5457
  state.input.setAttribute("aria-expanded", "true");
5408
5458
  if (typeof config.onOpen === "function") {
5409
- config.onOpen();
5459
+ safeInvokeCallback("onOpen", config.onOpen);
5410
5460
  }
5411
5461
  }
5412
5462
  function close() {
@@ -5417,7 +5467,7 @@
5417
5467
  state.input.setAttribute("aria-expanded", "false");
5418
5468
  state.input.removeAttribute("aria-activedescendant");
5419
5469
  if (typeof config.onClose === "function") {
5420
- config.onClose();
5470
+ safeInvokeCallback("onClose", config.onClose);
5421
5471
  }
5422
5472
  }
5423
5473
  function destroy() {
@@ -5431,7 +5481,7 @@
5431
5481
  clearTimeout(state.debounceTimer);
5432
5482
  }
5433
5483
  if (state.resultsContainer) {
5434
- state.resultsContainer.innerHTML = "";
5484
+ setResultsHtml("");
5435
5485
  }
5436
5486
  }
5437
5487
  function rebuild() {
@@ -5446,7 +5496,7 @@
5446
5496
  function getIndex() {
5447
5497
  return state.index.slice();
5448
5498
  }
5449
- var instance = {
5499
+ const instance = {
5450
5500
  init,
5451
5501
  destroy,
5452
5502
  rebuild,
@@ -5459,12 +5509,12 @@
5459
5509
  };
5460
5510
  return instance;
5461
5511
  }
5462
- var Search = {
5512
+ const Search = {
5463
5513
  // Factory method — creates and auto-initializes a new independent instance.
5464
5514
  // Always returns the instance so callers retain a reference even if the
5465
5515
  // DOM container is not yet available (they can retry init() later).
5466
5516
  create: function(options) {
5467
- var instance = createSearch(options);
5517
+ const instance = createSearch(options);
5468
5518
  if (instance) {
5469
5519
  instance.init();
5470
5520
  }
@@ -5538,6 +5588,565 @@
5538
5588
  window.VanduoDocSearch = Search;
5539
5589
  })();
5540
5590
 
5591
+ // js/components/draggable.js
5592
+ (function() {
5593
+ "use strict";
5594
+ const Draggable = {
5595
+ // Store initialized draggables and their cleanup functions
5596
+ instances: /* @__PURE__ */ new Map(),
5597
+ // Store current drag state
5598
+ currentDrag: null,
5599
+ // Store touch state
5600
+ touchState: null,
5601
+ // Feedback element
5602
+ feedbackElement: null,
5603
+ /**
5604
+ * Initialize draggable components
5605
+ */
5606
+ init: function() {
5607
+ const draggables = document.querySelectorAll(".vd-draggable, [data-draggable]");
5608
+ draggables.forEach((element) => {
5609
+ if (this.instances.has(element)) {
5610
+ return;
5611
+ }
5612
+ this.initDraggable(element);
5613
+ });
5614
+ const containers = document.querySelectorAll(".vd-draggable-container, .vd-draggable-container-vertical");
5615
+ containers.forEach((container) => {
5616
+ if (!this.instances.has(container)) {
5617
+ this.initContainer(container);
5618
+ }
5619
+ });
5620
+ const dropZones = document.querySelectorAll(".vd-drop-zone");
5621
+ dropZones.forEach((zone) => {
5622
+ if (!this.instances.has(zone)) {
5623
+ this.initDropZone(zone);
5624
+ }
5625
+ });
5626
+ this.createFeedbackElement();
5627
+ },
5628
+ /**
5629
+ * Initialize a single draggable element
5630
+ * @param {HTMLElement} element - Draggable element
5631
+ */
5632
+ initDraggable: function(element) {
5633
+ const cleanupFunctions = [];
5634
+ if (!element.hasAttribute("draggable")) {
5635
+ element.setAttribute("draggable", "true");
5636
+ }
5637
+ if (!element.hasAttribute("tabindex")) {
5638
+ element.setAttribute("tabindex", "0");
5639
+ }
5640
+ element.setAttribute("role", "option");
5641
+ element.setAttribute("aria-roledescription", "draggable item");
5642
+ element.setAttribute("aria-grabbed", "false");
5643
+ const dragStartHandler = (e) => {
5644
+ this.handleDragStart(e, element);
5645
+ };
5646
+ element.addEventListener("dragstart", dragStartHandler);
5647
+ cleanupFunctions.push(() => element.removeEventListener("dragstart", dragStartHandler));
5648
+ const dragHandler = (e) => {
5649
+ this.handleDrag(e, element);
5650
+ };
5651
+ element.addEventListener("drag", dragHandler);
5652
+ cleanupFunctions.push(() => element.removeEventListener("drag", dragHandler));
5653
+ const dragEndHandler = (e) => {
5654
+ this.handleDragEnd(e, element);
5655
+ };
5656
+ element.addEventListener("dragend", dragEndHandler);
5657
+ cleanupFunctions.push(() => element.removeEventListener("dragend", dragEndHandler));
5658
+ const touchStartHandler = (e) => {
5659
+ this.handleTouchStart(e, element);
5660
+ };
5661
+ element.addEventListener("touchstart", touchStartHandler);
5662
+ cleanupFunctions.push(() => element.removeEventListener("touchstart", touchStartHandler));
5663
+ const touchMoveHandler = (e) => {
5664
+ this.handleTouchMove(e, element);
5665
+ };
5666
+ element.addEventListener("touchmove", touchMoveHandler);
5667
+ cleanupFunctions.push(() => element.removeEventListener("touchmove", touchMoveHandler));
5668
+ const touchEndHandler = (e) => {
5669
+ this.handleTouchEnd(e, element);
5670
+ };
5671
+ element.addEventListener("touchend", touchEndHandler);
5672
+ cleanupFunctions.push(() => element.removeEventListener("touchend", touchEndHandler));
5673
+ const touchCancelHandler = (e) => {
5674
+ this.handleTouchEnd(e, element);
5675
+ };
5676
+ element.addEventListener("touchcancel", touchCancelHandler);
5677
+ cleanupFunctions.push(() => element.removeEventListener("touchcancel", touchCancelHandler));
5678
+ const keydownHandler = (e) => {
5679
+ this.handleKeydown(e, element);
5680
+ };
5681
+ element.addEventListener("keydown", keydownHandler);
5682
+ cleanupFunctions.push(() => element.removeEventListener("keydown", keydownHandler));
5683
+ this.instances.set(element, { cleanup: cleanupFunctions });
5684
+ },
5685
+ /**
5686
+ * Initialize a draggable container
5687
+ * @param {HTMLElement} container - Draggable container
5688
+ */
5689
+ initContainer: function(container) {
5690
+ container.setAttribute("role", "listbox");
5691
+ container.setAttribute("aria-label", container.getAttribute("aria-label") || "Draggable items");
5692
+ const items = container.querySelectorAll(".vd-draggable-item");
5693
+ items.forEach((item) => {
5694
+ if (!this.instances.has(item)) {
5695
+ this.initDraggable(item);
5696
+ }
5697
+ });
5698
+ const cleanupFunctions = [];
5699
+ const dragEnterHandler = (e) => {
5700
+ e.preventDefault();
5701
+ e.dataTransfer.dropEffect = "move";
5702
+ };
5703
+ const dragOverHandler = (e) => {
5704
+ e.preventDefault();
5705
+ e.dataTransfer.dropEffect = "move";
5706
+ if (!this.currentDrag) return;
5707
+ const draggingEl = this.currentDrag.element;
5708
+ if (!container.contains(draggingEl)) return;
5709
+ if (e.clientX === 0 && e.clientY === 0) return;
5710
+ this.handleReorder(container, draggingEl, e.clientX, e.clientY);
5711
+ };
5712
+ const dropHandler = (e) => {
5713
+ e.preventDefault();
5714
+ };
5715
+ container.addEventListener("dragenter", dragEnterHandler);
5716
+ container.addEventListener("dragover", dragOverHandler);
5717
+ container.addEventListener("drop", dropHandler);
5718
+ cleanupFunctions.push(() => {
5719
+ container.removeEventListener("dragenter", dragEnterHandler);
5720
+ container.removeEventListener("dragover", dragOverHandler);
5721
+ container.removeEventListener("drop", dropHandler);
5722
+ });
5723
+ this.instances.set(container, { cleanup: cleanupFunctions });
5724
+ },
5725
+ /**
5726
+ * Initialize a drop zone
5727
+ * @param {HTMLElement} zone - Drop zone element
5728
+ */
5729
+ initDropZone: function(zone) {
5730
+ const cleanupFunctions = [];
5731
+ zone.setAttribute("role", "region");
5732
+ zone.setAttribute("aria-dropeffect", "move");
5733
+ if (!zone.hasAttribute("aria-label")) {
5734
+ zone.setAttribute("aria-label", "Drop zone");
5735
+ }
5736
+ const dragOverHandler = (e) => {
5737
+ e.preventDefault();
5738
+ this.handleDragOver(e, zone);
5739
+ };
5740
+ zone.addEventListener("dragover", dragOverHandler);
5741
+ cleanupFunctions.push(() => zone.removeEventListener("dragover", dragOverHandler));
5742
+ const dragEnterHandler = (e) => {
5743
+ e.preventDefault();
5744
+ this.handleDragEnter(e, zone);
5745
+ };
5746
+ zone.addEventListener("dragenter", dragEnterHandler);
5747
+ cleanupFunctions.push(() => zone.removeEventListener("dragenter", dragEnterHandler));
5748
+ const dragLeaveHandler = (e) => {
5749
+ this.handleDragLeave(e, zone);
5750
+ };
5751
+ zone.addEventListener("dragleave", dragLeaveHandler);
5752
+ cleanupFunctions.push(() => zone.removeEventListener("dragleave", dragLeaveHandler));
5753
+ const dropHandler = (e) => {
5754
+ e.preventDefault();
5755
+ this.handleDrop(e, zone);
5756
+ };
5757
+ zone.addEventListener("drop", dropHandler);
5758
+ cleanupFunctions.push(() => zone.removeEventListener("drop", dropHandler));
5759
+ this.instances.set(zone, { cleanup: cleanupFunctions });
5760
+ },
5761
+ /**
5762
+ * Create feedback element for drag operations
5763
+ */
5764
+ createFeedbackElement: function() {
5765
+ if (!this.feedbackElement) {
5766
+ const existing = document.querySelector(".vd-drag-feedback");
5767
+ if (existing) {
5768
+ this.feedbackElement = existing;
5769
+ return;
5770
+ }
5771
+ this.feedbackElement = document.createElement("div");
5772
+ this.feedbackElement.className = "vd-drag-feedback hidden";
5773
+ this.feedbackElement.setAttribute("role", "presentation");
5774
+ document.body.appendChild(this.feedbackElement);
5775
+ }
5776
+ },
5777
+ /**
5778
+ * Handle drag start event
5779
+ * @param {DragEvent} e - Drag event
5780
+ * @param {HTMLElement} element - Draggable element
5781
+ */
5782
+ handleDragStart: function(e, element) {
5783
+ element.classList.add("is-dragging");
5784
+ element.setAttribute("aria-grabbed", "true");
5785
+ this.currentDrag = {
5786
+ element,
5787
+ initialPosition: { x: e.clientX, y: e.clientY },
5788
+ initialBounds: element.getBoundingClientRect(),
5789
+ data: this.getData(element)
5790
+ };
5791
+ e.dataTransfer.effectAllowed = "move";
5792
+ e.dataTransfer.setData("text/plain", this.currentDrag.data);
5793
+ element.dispatchEvent(new CustomEvent("draggable:start", {
5794
+ bubbles: true,
5795
+ detail: {
5796
+ element,
5797
+ data: this.currentDrag.data,
5798
+ position: { x: e.clientX, y: e.clientY }
5799
+ }
5800
+ }));
5801
+ },
5802
+ /**
5803
+ * Handle drag event
5804
+ * @param {DragEvent} e - Drag event
5805
+ * @param {HTMLElement} element - Draggable element
5806
+ */
5807
+ handleDrag: function(e, element) {
5808
+ if (!this.currentDrag) return;
5809
+ element.dispatchEvent(new CustomEvent("draggable:drag", {
5810
+ bubbles: true,
5811
+ detail: {
5812
+ element,
5813
+ data: this.currentDrag.data,
5814
+ position: { x: e.clientX, y: e.clientY },
5815
+ delta: {
5816
+ x: e.clientX - this.currentDrag.initialPosition.x,
5817
+ y: e.clientY - this.currentDrag.initialPosition.y
5818
+ }
5819
+ }
5820
+ }));
5821
+ },
5822
+ /**
5823
+ * Handle drag end event
5824
+ * @param {DragEvent} e - Drag event
5825
+ * @param {HTMLElement} element - Draggable element
5826
+ */
5827
+ handleDragEnd: function(e, element) {
5828
+ element.classList.remove("is-dragging");
5829
+ element.classList.add("is-dropped");
5830
+ setTimeout(() => element.classList.remove("is-dropped"), 300);
5831
+ element.setAttribute("aria-grabbed", "false");
5832
+ if (this.feedbackElement) {
5833
+ this.feedbackElement.classList.add("hidden");
5834
+ }
5835
+ const data = this.currentDrag?.data || this.getData(element);
5836
+ const initialPos = this.currentDrag?.initialPosition || { x: 0, y: 0 };
5837
+ element.dispatchEvent(new CustomEvent("draggable:end", {
5838
+ bubbles: true,
5839
+ detail: {
5840
+ element,
5841
+ data,
5842
+ position: { x: e.clientX, y: e.clientY },
5843
+ delta: {
5844
+ x: e.clientX - initialPos.x,
5845
+ y: e.clientY - initialPos.y
5846
+ }
5847
+ }
5848
+ }));
5849
+ this.currentDrag = null;
5850
+ },
5851
+ /**
5852
+ * Handle touch start event (for mobile)
5853
+ * @param {TouchEvent} e - Touch event
5854
+ * @param {HTMLElement} element - Draggable element
5855
+ */
5856
+ handleTouchStart: function(e, element) {
5857
+ const touch = e.touches[0];
5858
+ this.touchState = {
5859
+ element,
5860
+ startX: touch.clientX,
5861
+ startY: touch.clientY,
5862
+ startTime: Date.now(),
5863
+ isDragging: false
5864
+ };
5865
+ },
5866
+ /**
5867
+ * Handle touch move event (for mobile)
5868
+ * @param {TouchEvent} e - Touch event
5869
+ * @param {HTMLElement} element - Draggable element
5870
+ */
5871
+ handleTouchMove: function(e, element) {
5872
+ if (!this.touchState) return;
5873
+ const touch = e.touches[0];
5874
+ const deltaX = touch.clientX - this.touchState.startX;
5875
+ const deltaY = touch.clientY - this.touchState.startY;
5876
+ if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
5877
+ e.preventDefault();
5878
+ if (!this.touchState.isDragging) {
5879
+ this.touchState.isDragging = true;
5880
+ element.classList.add("is-dragging");
5881
+ element.setAttribute("aria-grabbed", "true");
5882
+ this.currentDrag = {
5883
+ element,
5884
+ initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
5885
+ initialBounds: element.getBoundingClientRect(),
5886
+ data: this.getData(element)
5887
+ };
5888
+ element.dispatchEvent(new CustomEvent("draggable:start", {
5889
+ bubbles: true,
5890
+ detail: {
5891
+ element,
5892
+ data: this.currentDrag.data,
5893
+ position: { x: touch.clientX, y: touch.clientY }
5894
+ }
5895
+ }));
5896
+ }
5897
+ this.updateFeedback(touch.clientX, touch.clientY);
5898
+ if (this.currentDrag) {
5899
+ element.dispatchEvent(new CustomEvent("draggable:drag", {
5900
+ bubbles: true,
5901
+ detail: {
5902
+ element,
5903
+ data: this.currentDrag.data,
5904
+ position: { x: touch.clientX, y: touch.clientY },
5905
+ delta: { x: deltaX, y: deltaY }
5906
+ }
5907
+ }));
5908
+ const container = element.closest(".vd-draggable-container");
5909
+ if (container && container.contains(element)) {
5910
+ this.handleReorder(container, element, touch.clientX, touch.clientY);
5911
+ }
5912
+ }
5913
+ }
5914
+ },
5915
+ /**
5916
+ * Handle touch end event (for mobile)
5917
+ * @param {TouchEvent} e - Touch event
5918
+ * @param {HTMLElement} element - Draggable element
5919
+ */
5920
+ handleTouchEnd: function(e, element) {
5921
+ if (this.touchState && this.touchState.isDragging) {
5922
+ e.preventDefault();
5923
+ element.classList.remove("is-dragging");
5924
+ element.classList.add("is-dropped");
5925
+ element.setAttribute("aria-grabbed", "false");
5926
+ setTimeout(() => element.classList.remove("is-dropped"), 300);
5927
+ if (this.feedbackElement) {
5928
+ this.feedbackElement.classList.add("hidden");
5929
+ }
5930
+ const endTouch = e.changedTouches[0];
5931
+ const data = this.currentDrag?.data || this.getData(element);
5932
+ const startX = this.touchState?.startX || 0;
5933
+ const startY = this.touchState?.startY || 0;
5934
+ element.dispatchEvent(new CustomEvent("draggable:end", {
5935
+ bubbles: true,
5936
+ detail: {
5937
+ element,
5938
+ data,
5939
+ position: { x: endTouch.clientX, y: endTouch.clientY },
5940
+ delta: {
5941
+ x: endTouch.clientX - startX,
5942
+ y: endTouch.clientY - startY
5943
+ }
5944
+ }
5945
+ }));
5946
+ }
5947
+ this.touchState = null;
5948
+ this.currentDrag = null;
5949
+ },
5950
+ /**
5951
+ * Handle drag over event
5952
+ * @param {DragEvent} e - Drag event
5953
+ * @param {HTMLElement} _zone - Drop zone element
5954
+ */
5955
+ handleDragOver: function(e, _zone) {
5956
+ e.preventDefault();
5957
+ e.dataTransfer.dropEffect = "move";
5958
+ },
5959
+ /**
5960
+ * Handle drag enter event
5961
+ * @param {DragEvent} e - Drag event
5962
+ * @param {HTMLElement} zone - Drop zone element
5963
+ */
5964
+ handleDragEnter: function(e, zone) {
5965
+ e.preventDefault();
5966
+ zone.classList.add("is-drag-over");
5967
+ },
5968
+ /**
5969
+ * Handle drag leave event
5970
+ * @param {DragEvent} e - Drag event
5971
+ * @param {HTMLElement} zone - Drop zone element
5972
+ */
5973
+ handleDragLeave: function(e, zone) {
5974
+ zone.classList.remove("is-drag-over");
5975
+ },
5976
+ /**
5977
+ * Handle drop event
5978
+ * @param {DragEvent} e - Drag event
5979
+ * @param {HTMLElement} zone - Drop zone element
5980
+ */
5981
+ handleDrop: function(e, zone) {
5982
+ e.preventDefault();
5983
+ zone.classList.remove("is-drag-over");
5984
+ zone.dispatchEvent(new CustomEvent("draggable:drop", {
5985
+ bubbles: true,
5986
+ detail: {
5987
+ zone,
5988
+ element: this.currentDrag?.element,
5989
+ data: this.currentDrag?.data,
5990
+ position: { x: e.clientX, y: e.clientY }
5991
+ }
5992
+ }));
5993
+ },
5994
+ /**
5995
+ * Reorder elements in container based on cursor position
5996
+ * @param {HTMLElement} container
5997
+ * @param {HTMLElement} element
5998
+ * @param {number} clientX
5999
+ * @param {number} clientY
6000
+ */
6001
+ handleReorder: function(container, element, clientX, clientY) {
6002
+ const isVertical = container.classList.contains("vd-draggable-container-vertical");
6003
+ const siblings = [...container.querySelectorAll(".vd-draggable-item:not(.is-dragging), .vd-draggable:not(.is-dragging)")];
6004
+ const nextSibling = siblings.reduce((closest, child) => {
6005
+ const box = child.getBoundingClientRect();
6006
+ const offset = isVertical ? clientY - box.top - box.height / 2 : clientX - box.left - box.width / 2;
6007
+ if (offset < 0 && offset > closest.offset) {
6008
+ return { offset, element: child };
6009
+ } else {
6010
+ return closest;
6011
+ }
6012
+ }, { offset: Number.NEGATIVE_INFINITY }).element;
6013
+ if (nextSibling == null) {
6014
+ container.appendChild(element);
6015
+ } else {
6016
+ container.insertBefore(element, nextSibling);
6017
+ }
6018
+ },
6019
+ /**
6020
+ * Handle keyboard events
6021
+ * @param {KeyboardEvent} e - Keyboard event
6022
+ * @param {HTMLElement} element - Draggable element
6023
+ */
6024
+ handleKeydown: function(e, element) {
6025
+ switch (e.key) {
6026
+ case "Enter":
6027
+ case " ":
6028
+ e.preventDefault();
6029
+ element.click();
6030
+ break;
6031
+ case "Escape":
6032
+ if (element.classList.contains("is-dragging")) {
6033
+ element.classList.remove("is-dragging");
6034
+ element.setAttribute("aria-grabbed", "false");
6035
+ if (this.feedbackElement) {
6036
+ this.feedbackElement.classList.add("hidden");
6037
+ }
6038
+ this.currentDrag = null;
6039
+ }
6040
+ break;
6041
+ case "ArrowUp":
6042
+ case "ArrowLeft": {
6043
+ e.preventDefault();
6044
+ const prev = element.previousElementSibling;
6045
+ if (prev && (prev.classList.contains("vd-draggable") || prev.classList.contains("vd-draggable-item"))) {
6046
+ element.parentNode.insertBefore(element, prev);
6047
+ element.focus();
6048
+ element.dispatchEvent(new CustomEvent("draggable:reorder", {
6049
+ bubbles: true,
6050
+ detail: { element, direction: "up" }
6051
+ }));
6052
+ }
6053
+ break;
6054
+ }
6055
+ case "ArrowDown":
6056
+ case "ArrowRight": {
6057
+ e.preventDefault();
6058
+ const next = element.nextElementSibling;
6059
+ if (next && (next.classList.contains("vd-draggable") || next.classList.contains("vd-draggable-item"))) {
6060
+ element.parentNode.insertBefore(next, element);
6061
+ element.focus();
6062
+ element.dispatchEvent(new CustomEvent("draggable:reorder", {
6063
+ bubbles: true,
6064
+ detail: { element, direction: "down" }
6065
+ }));
6066
+ }
6067
+ break;
6068
+ }
6069
+ }
6070
+ },
6071
+ /**
6072
+ * Get data from draggable element
6073
+ * @param {HTMLElement} element - Draggable element
6074
+ * @returns {string} Data associated with the element
6075
+ */
6076
+ getData: function(element) {
6077
+ return element.dataset.draggable || element.textContent.trim();
6078
+ },
6079
+ /**
6080
+ * Update drag feedback element
6081
+ * @param {number} x - Current X coordinate
6082
+ * @param {number} y - Current Y coordinate
6083
+ */
6084
+ updateFeedback: function(x, y) {
6085
+ if (!this.currentDrag) return;
6086
+ this.feedbackElement.classList.remove("hidden");
6087
+ const rect = this.currentDrag.initialBounds;
6088
+ this.feedbackElement.innerHTML = "";
6089
+ const clone = this.currentDrag.element.cloneNode(true);
6090
+ this.feedbackElement.appendChild(clone);
6091
+ Object.assign(this.feedbackElement.style, {
6092
+ left: x - 20 + "px",
6093
+ top: y - 20 + "px",
6094
+ width: rect.width + "px",
6095
+ height: rect.height + "px"
6096
+ });
6097
+ },
6098
+ /**
6099
+ * Make an element draggable programmatically
6100
+ * @param {HTMLElement|string} element - Element or selector
6101
+ * @param {Object} options - Configuration options
6102
+ */
6103
+ makeDraggable: function(element, options = {}) {
6104
+ const el = typeof element === "string" ? document.querySelector(element) : element;
6105
+ if (el && !this.instances.has(el)) {
6106
+ el.classList.add("vd-draggable");
6107
+ el.setAttribute("draggable", "true");
6108
+ if (options.data) {
6109
+ el.dataset.draggable = options.data;
6110
+ }
6111
+ this.initDraggable(el);
6112
+ }
6113
+ },
6114
+ /**
6115
+ * Remove draggable functionality from an element
6116
+ * @param {HTMLElement|string} element - Element or selector
6117
+ */
6118
+ removeDraggable: function(element) {
6119
+ const el = typeof element === "string" ? document.querySelector(element) : element;
6120
+ if (el && this.instances.has(el)) {
6121
+ const instance = this.instances.get(el);
6122
+ instance.cleanup.forEach((fn) => fn());
6123
+ this.instances.delete(el);
6124
+ el.classList.remove("vd-draggable");
6125
+ el.removeAttribute("draggable");
6126
+ el.removeAttribute("data-draggable");
6127
+ }
6128
+ },
6129
+ /**
6130
+ * Destroy a draggable instance and clean up event listeners
6131
+ * @param {HTMLElement} element - Draggable element
6132
+ */
6133
+ destroy: function(element) {
6134
+ this.removeDraggable(element);
6135
+ },
6136
+ /**
6137
+ * Destroy all draggable instances
6138
+ */
6139
+ destroyAll: function() {
6140
+ const instances = Array.from(this.instances.keys());
6141
+ instances.forEach((element) => this.destroy(element));
6142
+ }
6143
+ };
6144
+ if (typeof window.Vanduo !== "undefined") {
6145
+ window.Vanduo.register("draggable", Draggable);
6146
+ }
6147
+ window.VanduoDraggable = Draggable;
6148
+ })();
6149
+
5541
6150
  // js/index.js
5542
6151
  var Vanduo = window.Vanduo;
5543
6152
  var index_default = Vanduo;