@vanduo-oss/framework 1.3.0 → 1.3.1

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Vanduo Framework v1.3.0
1
+ # Vanduo Framework v1.3.1
2
2
 
3
3
  <p align="center">
4
4
  <img src="vanduo-banner.svg" alt="Vanduo Framework Banner" width="100%">
@@ -38,14 +38,16 @@ A lightweight, pure HTML/CSS/JS framework with **45+ components** for designing
38
38
 
39
39
  ---
40
40
 
41
- ## What's New in v1.3.0
41
+ ## What's New in v1.3.1
42
42
 
43
- v1.3.0 is a maintenance release focused on documentation improvements and framework consistency:
43
+ v1.3.1 is a security and correctness release (12 issues fixed, 0 breaking changes):
44
44
 
45
- - **Added missing spacing utilities and text-italic class.** The framework now includes `.vd-mt-6` and `.vd-mt-8` spacing utilities, plus `.vd-text-italic` for text styling.
46
- - **Fixed phantom class references in documentation.** Updated docs to use correct class names that match the actual framework implementation.
47
- - **Improved documentation structure.** Added inline sidebar filter for Components & Guides pages, unified component documentation structure, and refreshed visual styling.
48
- - **Release artifacts and docs are aligned for v1.3.0.** Package metadata, generated bundles, `llms.txt`, and release-facing README examples now point at the current version.
45
+ - **XSS fix in Suggest.** `renderItems()` now escapes user/server data before `innerHTML` highlight injection.
46
+ - **Select component repairs.** Fixed 3 broken `querySelector` selectors (keyboard nav + programmatic updates); `generateId()` now assigns `element.id` so ARIA `aria-labelledby` resolves correctly.
47
+ - **Typeahead isolation.** `_typeaheadBuffer` / `_typeaheadTimer` moved to per-instance state in Dropdown and Select typing in one instance no longer corrupts another.
48
+ - **Navbar scroll-lock fix.** CSS class `body-navbar-open` replaces inline `overflow:hidden`, preventing conflicts with modal scroll locks.
49
+ - **Validate hardening.** 100-char limit on user regex patterns (ReDoS prevention); `CSS.escape()` applied to `match` rule param (selector injection fix).
50
+ - **Release artifacts and docs are aligned for v1.3.1.** Package metadata, generated bundles, `llms.txt`, and release-facing README examples now point at the current version.
49
51
 
50
52
  The framework still ships **45+ components**, including the v1.2.7 additions below.
51
53
 
@@ -88,8 +90,8 @@ The quickest way to get started — no install, no build step. Add two lines to
88
90
 
89
91
  **Pin to a specific version** for production:
90
92
  ```html
91
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.0/dist/vanduo.min.css">
92
- <script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.0/dist/vanduo.min.js"></script>
93
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.1/dist/vanduo.min.css">
94
+ <script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.1/dist/vanduo.min.js"></script>
93
95
  <script>Vanduo.init();</script>
94
96
  ```
95
97
 
@@ -152,7 +154,7 @@ This project includes an [`llms.txt`](llms.txt) file — a structured markdown s
152
154
  Use the hardened upload script to attach only approved bundle artifacts from `dist/`:
153
155
 
154
156
  ```bash
155
- pnpm run release:assets -- v1.3.0
157
+ pnpm run release:assets -- v1.3.1
156
158
  ```
157
159
 
158
160
  Notes:
@@ -528,3 +528,8 @@
528
528
  display: none;
529
529
  }
530
530
 
531
+ /* Body Scroll Lock - class-based to avoid conflicts with modals */
532
+ .body-navbar-open {
533
+ overflow: hidden;
534
+ }
535
+
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.3.0",
3
- "builtAt": "2026-03-17T16:46:03.216Z",
4
- "commit": "ea44b23",
2
+ "version": "1.3.1",
3
+ "builtAt": "2026-03-20T21:48:40.922Z",
4
+ "commit": "7e73bb8",
5
5
  "mode": "development+production"
6
6
  }
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.3.0 | Built: 2026-03-17T16:46:03.216Z | git:ea44b23 | development */
1
+ /*! Vanduo v1.3.1 | Built: 2026-03-20T21:48:40.922Z | git:7e73bb8 | development */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -132,7 +132,7 @@ module.exports = __toCommonJS(index_exports);
132
132
  // js/vanduo.js
133
133
  (function() {
134
134
  "use strict";
135
- const VANDUO_VERSION = true ? "1.3.0" : "0.0.0-dev";
135
+ const VANDUO_VERSION = true ? "1.3.1" : "0.0.0-dev";
136
136
  const Vanduo2 = {
137
137
  version: VANDUO_VERSION,
138
138
  components: {},
@@ -443,16 +443,17 @@ module.exports = __toCommonJS(index_exports);
443
443
  }
444
444
  const codeElement = activePane.querySelector("code") || activePane;
445
445
  const code = codeElement.textContent;
446
+ let copySuccess;
446
447
  try {
447
448
  await navigator.clipboard.writeText(code);
448
- this.showCopyFeedback(copyBtn, true);
449
+ copySuccess = true;
449
450
  } catch (_err) {
450
- const success = this.fallbackCopy(code);
451
- this.showCopyFeedback(copyBtn, success);
451
+ copySuccess = this.fallbackCopy(code);
452
452
  }
453
+ this.showCopyFeedback(copyBtn, copySuccess);
453
454
  const event = new CustomEvent("codesnippet:copy", {
454
455
  bubbles: true,
455
- detail: { snippet, code, success: true }
456
+ detail: { snippet, code, success: copySuccess }
456
457
  });
457
458
  snippet.dispatchEvent(event);
458
459
  },
@@ -889,9 +890,6 @@ module.exports = __toCommonJS(index_exports);
889
890
  const Dropdown = {
890
891
  // Store initialized dropdowns and their cleanup functions
891
892
  instances: /* @__PURE__ */ new Map(),
892
- // Typeahead state
893
- _typeaheadBuffer: "",
894
- _typeaheadTimer: null,
895
893
  /**
896
894
  * Initialize dropdown components
897
895
  */
@@ -955,7 +953,7 @@ module.exports = __toCommonJS(index_exports);
955
953
  item.addEventListener("keydown", itemKeydownHandler);
956
954
  cleanupFunctions.push(() => item.removeEventListener("keydown", itemKeydownHandler));
957
955
  });
958
- this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions });
956
+ this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions, typeaheadBuffer: "", typeaheadTimer: null });
959
957
  },
960
958
  /**
961
959
  * Toggle dropdown
@@ -1084,16 +1082,18 @@ module.exports = __toCommonJS(index_exports);
1084
1082
  break;
1085
1083
  default:
1086
1084
  if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
1087
- clearTimeout(this._typeaheadTimer);
1088
- this._typeaheadBuffer += e.key.toLowerCase();
1085
+ const instance = this.instances.get(dropdown);
1086
+ if (!instance) break;
1087
+ clearTimeout(instance.typeaheadTimer);
1088
+ instance.typeaheadBuffer += e.key.toLowerCase();
1089
1089
  const match = items.find(
1090
- (item) => item.textContent.trim().toLowerCase().startsWith(this._typeaheadBuffer)
1090
+ (item) => item.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
1091
1091
  );
1092
1092
  if (match) {
1093
1093
  match.focus();
1094
1094
  }
1095
- this._typeaheadTimer = setTimeout(() => {
1096
- this._typeaheadBuffer = "";
1095
+ instance.typeaheadTimer = setTimeout(() => {
1096
+ instance.typeaheadBuffer = "";
1097
1097
  }, 500);
1098
1098
  }
1099
1099
  break;
@@ -1801,9 +1801,10 @@ module.exports = __toCommonJS(index_exports);
1801
1801
  }));
1802
1802
  if (!this.img.complete) {
1803
1803
  this.img.style.opacity = "0";
1804
- this.img.onload = () => {
1804
+ this._imgLoadHandler = () => {
1805
1805
  this.img.style.opacity = "";
1806
1806
  };
1807
+ this.img.addEventListener("load", this._imgLoadHandler, { once: true });
1807
1808
  }
1808
1809
  },
1809
1810
  /**
@@ -1822,6 +1823,10 @@ module.exports = __toCommonJS(index_exports);
1822
1823
  }
1823
1824
  setTimeout(() => {
1824
1825
  if (!this.isOpen) {
1826
+ if (this._imgLoadHandler) {
1827
+ this.img.removeEventListener("load", this._imgLoadHandler);
1828
+ this._imgLoadHandler = null;
1829
+ }
1825
1830
  this.img.src = "";
1826
1831
  this.img.alt = "";
1827
1832
  }
@@ -1881,6 +1886,8 @@ module.exports = __toCommonJS(index_exports);
1881
1886
  zIndexCounter: 1050,
1882
1887
  // Store trigger cleanup functions
1883
1888
  _triggerCleanups: [],
1889
+ // Shared ESC key handler (installed once)
1890
+ _sharedEscHandler: null,
1884
1891
  /**
1885
1892
  * Initialize modals
1886
1893
  */
@@ -1945,16 +1952,17 @@ module.exports = __toCommonJS(index_exports);
1945
1952
  };
1946
1953
  backdrop.addEventListener("click", backdropClickHandler);
1947
1954
  cleanupFunctions.push(() => backdrop.removeEventListener("click", backdropClickHandler));
1948
- const escKeyHandler = (e) => {
1949
- if (e.key === "Escape" && this.openModals.length > 0) {
1950
- const topModal = this.openModals[this.openModals.length - 1];
1951
- if (topModal === modal && topModal.dataset.keyboard !== "false") {
1952
- this.close(topModal);
1955
+ if (!this._sharedEscHandler) {
1956
+ this._sharedEscHandler = (e) => {
1957
+ if (e.key === "Escape" && this.openModals.length > 0) {
1958
+ const topModal = this.openModals[this.openModals.length - 1];
1959
+ if (topModal.dataset.keyboard !== "false") {
1960
+ this.close(topModal);
1961
+ }
1953
1962
  }
1954
- }
1955
- };
1956
- document.addEventListener("keydown", escKeyHandler);
1957
- cleanupFunctions.push(() => document.removeEventListener("keydown", escKeyHandler));
1963
+ };
1964
+ document.addEventListener("keydown", this._sharedEscHandler);
1965
+ }
1958
1966
  this.modals.set(modal, { backdrop, dialog, trapHandler: null, cleanup: cleanupFunctions });
1959
1967
  },
1960
1968
  /**
@@ -2134,6 +2142,10 @@ module.exports = __toCommonJS(index_exports);
2134
2142
  });
2135
2143
  this._triggerCleanups.forEach((fn) => fn());
2136
2144
  this._triggerCleanups = [];
2145
+ if (this._sharedEscHandler) {
2146
+ document.removeEventListener("keydown", this._sharedEscHandler);
2147
+ this._sharedEscHandler = null;
2148
+ }
2137
2149
  }
2138
2150
  };
2139
2151
  if (typeof window.Vanduo !== "undefined") {
@@ -2299,7 +2311,7 @@ module.exports = __toCommonJS(index_exports);
2299
2311
  if (overlay) {
2300
2312
  overlay.classList.add("is-active");
2301
2313
  }
2302
- document.body.style.overflow = "hidden";
2314
+ document.body.classList.add("body-navbar-open");
2303
2315
  toggle.setAttribute("aria-expanded", "true");
2304
2316
  menu.setAttribute("aria-hidden", "false");
2305
2317
  },
@@ -2316,7 +2328,7 @@ module.exports = __toCommonJS(index_exports);
2316
2328
  if (overlay) {
2317
2329
  overlay.classList.remove("is-active");
2318
2330
  }
2319
- document.body.style.overflow = "";
2331
+ document.body.classList.remove("body-navbar-open");
2320
2332
  const dropdownMenus = menu.querySelectorAll(".vd-navbar-dropdown-menu.is-open");
2321
2333
  dropdownMenus.forEach((dropdownMenu) => {
2322
2334
  dropdownMenu.classList.remove("is-open");
@@ -2875,9 +2887,6 @@ module.exports = __toCommonJS(index_exports);
2875
2887
  const Select = {
2876
2888
  // Store initialized selects and their cleanup functions
2877
2889
  instances: /* @__PURE__ */ new Map(),
2878
- // Typeahead state
2879
- _typeaheadBuffer: "",
2880
- _typeaheadTimer: null,
2881
2890
  /**
2882
2891
  * Initialize select components
2883
2892
  */
@@ -2958,7 +2967,7 @@ module.exports = __toCommonJS(index_exports);
2958
2967
  };
2959
2968
  select.addEventListener("change", changeHandler);
2960
2969
  cleanupFunctions.push(() => select.removeEventListener("change", changeHandler));
2961
- this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions });
2970
+ this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions, typeaheadBuffer: "", typeaheadTimer: null });
2962
2971
  },
2963
2972
  /**
2964
2973
  * Build options in dropdown
@@ -3052,7 +3061,7 @@ module.exports = __toCommonJS(index_exports);
3052
3061
  * @param {HTMLElement} dropdown - Dropdown container
3053
3062
  */
3054
3063
  updateSelectedOptions: function(select, dropdown) {
3055
- const options = dropdown.querySelectorAll(".vd-custom-select-option");
3064
+ const options = dropdown.querySelectorAll(".custom-select-option");
3056
3065
  const selectedValues = Array.from(select.selectedOptions).map((opt) => opt.value);
3057
3066
  options.forEach((optionEl) => {
3058
3067
  const value = optionEl.dataset.value;
@@ -3086,7 +3095,7 @@ module.exports = __toCommonJS(index_exports);
3086
3095
  openDropdown: function(button, dropdown) {
3087
3096
  dropdown.classList.add("is-open");
3088
3097
  button.setAttribute("aria-expanded", "true");
3089
- const firstOption = dropdown.querySelector(".vd-custom-select-option:not(.is-disabled)");
3098
+ const firstOption = dropdown.querySelector(".custom-select-option:not(.is-disabled)");
3090
3099
  if (firstOption) {
3091
3100
  firstOption.focus();
3092
3101
  }
@@ -3109,7 +3118,7 @@ module.exports = __toCommonJS(index_exports);
3109
3118
  */
3110
3119
  handleKeydown: function(e, select, button, dropdown) {
3111
3120
  const isOpen = dropdown.classList.contains("is-open");
3112
- const options = Array.from(dropdown.querySelectorAll(".vd-custom-select-option:not(.is-disabled)"));
3121
+ const options = Array.from(dropdown.querySelectorAll(".custom-select-option:not(.is-disabled)"));
3113
3122
  const currentIndex = options.findIndex((opt) => opt === document.activeElement);
3114
3123
  switch (e.key) {
3115
3124
  case "Enter":
@@ -3160,16 +3169,18 @@ module.exports = __toCommonJS(index_exports);
3160
3169
  break;
3161
3170
  default:
3162
3171
  if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
3163
- clearTimeout(this._typeaheadTimer);
3164
- this._typeaheadBuffer += e.key.toLowerCase();
3172
+ const instance = this.instances.get(select);
3173
+ if (!instance) break;
3174
+ clearTimeout(instance.typeaheadTimer);
3175
+ instance.typeaheadBuffer += e.key.toLowerCase();
3165
3176
  const match = options.find(
3166
- (opt) => opt.textContent.trim().toLowerCase().startsWith(this._typeaheadBuffer)
3177
+ (opt) => opt.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
3167
3178
  );
3168
3179
  if (match) {
3169
3180
  match.focus();
3170
3181
  }
3171
- this._typeaheadTimer = setTimeout(() => {
3172
- this._typeaheadBuffer = "";
3182
+ instance.typeaheadTimer = setTimeout(() => {
3183
+ instance.typeaheadBuffer = "";
3173
3184
  }, 500);
3174
3185
  }
3175
3186
  break;
@@ -3201,7 +3212,9 @@ module.exports = __toCommonJS(index_exports);
3201
3212
  if (element.id) {
3202
3213
  return element.id;
3203
3214
  }
3204
- return "select-" + Math.random().toString(36).substr(2, 9);
3215
+ const id = "select-" + Math.random().toString(36).substr(2, 9);
3216
+ element.id = id;
3217
+ return id;
3205
3218
  },
3206
3219
  /**
3207
3220
  * Destroy a select instance and clean up event listeners
@@ -4255,7 +4268,6 @@ module.exports = __toCommonJS(index_exports);
4255
4268
  this.applyNeutral(this.DEFAULTS.NEUTRAL);
4256
4269
  this.applyRadius(this.DEFAULTS.RADIUS);
4257
4270
  this.applyFont(this.DEFAULTS.FONT);
4258
- this.applyTheme(this.DEFAULTS.THEME);
4259
4271
  this.updateUI();
4260
4272
  this.dispatchEvent("reset", { state: { ...this.state } });
4261
4273
  },
@@ -7040,6 +7052,11 @@ module.exports = __toCommonJS(index_exports);
7040
7052
  // js/components/suggest.js
7041
7053
  (function() {
7042
7054
  "use strict";
7055
+ function _escapeHtml(text) {
7056
+ const div = document.createElement("div");
7057
+ div.textContent = text;
7058
+ return div.innerHTML;
7059
+ }
7043
7060
  const Suggest = {
7044
7061
  instances: /* @__PURE__ */ new Map(),
7045
7062
  init: function() {
@@ -7099,8 +7116,9 @@ module.exports = __toCommonJS(index_exports);
7099
7116
  li.id = listId + "-item-" + i;
7100
7117
  const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
7101
7118
  if (query) {
7119
+ const escaped = _escapeHtml(text);
7102
7120
  const re = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
7103
- li.innerHTML = text.replace(re, '<span class="vd-suggest-match">$1</span>');
7121
+ li.innerHTML = escaped.replace(re, '<span class="vd-suggest-match">$1</span>');
7104
7122
  } else {
7105
7123
  li.textContent = text;
7106
7124
  }
@@ -7253,14 +7271,20 @@ module.exports = __toCommonJS(index_exports);
7253
7271
  maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
7254
7272
  pattern: (value, param) => {
7255
7273
  try {
7274
+ if (param.length > 100) return false;
7256
7275
  return new RegExp(param).test(value);
7257
7276
  } catch (_e) {
7258
7277
  return false;
7259
7278
  }
7260
7279
  },
7261
7280
  match: (value, param) => {
7262
- const other = document.querySelector('[name="' + param + '"]');
7263
- return other ? value === other.value : false;
7281
+ try {
7282
+ const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(param) : param;
7283
+ const other = document.querySelector('[name="' + escaped + '"]');
7284
+ return other ? value === other.value : false;
7285
+ } catch (_e) {
7286
+ return false;
7287
+ }
7264
7288
  }
7265
7289
  },
7266
7290
  messages: {