@birdapi/velinstyle 0.6.1 → 0.8.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.
Files changed (64) hide show
  1. package/README.de.md +337 -316
  2. package/README.md +33 -12
  3. package/cli/blueprint.js +8 -0
  4. package/cli/blueprints/bottom-nav-mobile.html +17 -0
  5. package/cli/blueprints/cookie-consent.html +9 -0
  6. package/cli/blueprints/empty-state.html +5 -0
  7. package/cli/blueprints/filter-bar.html +15 -0
  8. package/cli/blueprints/notification-center.html +13 -0
  9. package/cli/blueprints/onboarding.html +23 -0
  10. package/cli/blueprints/pricing-table.html +20 -0
  11. package/cli/blueprints/settings-panel.html +20 -0
  12. package/cli/index.js +119 -3
  13. package/cli/layout-audit.js +325 -0
  14. package/cli/scaffold-recipes.json +70 -0
  15. package/cli/scaffold.js +155 -0
  16. package/cli/scanner.js +114 -0
  17. package/components/focus-manager.js +106 -80
  18. package/components/index.js +20 -1
  19. package/components/sanitize.js +29 -3
  20. package/components/shadow-a11y-styles.js +18 -0
  21. package/components/velin-accordion.js +112 -98
  22. package/components/velin-announcer.js +35 -0
  23. package/components/velin-bottom-nav.js +89 -0
  24. package/components/velin-carousel.js +40 -5
  25. package/components/velin-collapse.js +95 -65
  26. package/components/velin-combobox.js +149 -0
  27. package/components/velin-command.js +127 -0
  28. package/components/velin-counter.js +152 -0
  29. package/components/velin-drawer.js +6 -3
  30. package/components/velin-dropdown.js +33 -2
  31. package/components/velin-flip.js +220 -0
  32. package/components/velin-icon.js +43 -9
  33. package/components/velin-live-dot.js +85 -0
  34. package/components/velin-menubar.js +83 -0
  35. package/components/velin-modal.js +3 -1
  36. package/components/velin-popover.js +61 -21
  37. package/components/velin-rating.js +91 -0
  38. package/components/velin-reveal.js +80 -0
  39. package/components/velin-segmented-control.js +108 -0
  40. package/components/velin-sheet.js +107 -0
  41. package/components/velin-sparkline.js +207 -0
  42. package/components/velin-theme-toggle.js +277 -60
  43. package/components/velin-tooltip-wc.js +26 -2
  44. package/dist/velinstyle-components.iife.js +1849 -120
  45. package/dist/velinstyle-components.js +1871 -120
  46. package/dist/velinstyle-components.min.js +434 -91
  47. package/dist/velinstyle.css +640 -44
  48. package/dist/velinstyle.min.css +1 -1
  49. package/package.json +7 -3
  50. package/src/a11y/focus-not-obscured.css +21 -0
  51. package/src/a11y/forced-colors.css +86 -38
  52. package/src/a11y/high-contrast-aaa.css +37 -0
  53. package/src/a11y/preferences.css +106 -85
  54. package/src/a11y/security.css +119 -104
  55. package/src/a11y/target-size.css +32 -0
  56. package/src/base/reset.css +12 -1
  57. package/src/base/root.css +4 -4
  58. package/src/components/nav.css +152 -151
  59. package/src/tokens/motion.css +7 -0
  60. package/src/utilities/animation.css +97 -1
  61. package/src/utilities/chart-animation.css +101 -0
  62. package/src/utilities/filter-effects.css +103 -0
  63. package/src/utilities/safe-area.css +39 -0
  64. package/src/velinstyle.css +9 -1
@@ -9,10 +9,15 @@ var FOCUSABLE_SELECTOR = [
9
9
  "summary",
10
10
  "details"
11
11
  ].join(", ");
12
+ function isFocusable(el) {
13
+ if (el.hasAttribute("disabled") || el.getAttribute("aria-hidden") === "true") return false;
14
+ if (el.closest("[inert]")) return false;
15
+ const style = el.ownerDocument.defaultView?.getComputedStyle(el);
16
+ if (style && (style.visibility === "hidden" || style.display === "none")) return false;
17
+ return el.getClientRects().length > 0;
18
+ }
12
19
  function getFocusableElements(root) {
13
- return [...root.querySelectorAll(FOCUSABLE_SELECTOR)].filter(
14
- (el) => !el.hasAttribute("disabled") && el.offsetParent !== null
15
- );
20
+ return [...root.querySelectorAll(FOCUSABLE_SELECTOR)].filter(isFocusable);
16
21
  }
17
22
  function trapFocus(root, event) {
18
23
  if (event.key !== "Tab") return;
@@ -68,24 +73,71 @@ function restoreFocus(element) {
68
73
  element.focus();
69
74
  }
70
75
  }
76
+ var _inertSiblings = [];
77
+ function setBackgroundInert(except) {
78
+ _inertSiblings = [];
79
+ for (const child of document.body.children) {
80
+ if (child === except || child.contains(except)) continue;
81
+ if (!child.hasAttribute("inert")) {
82
+ child.setAttribute("inert", "");
83
+ _inertSiblings.push(child);
84
+ }
85
+ }
86
+ }
87
+ function clearBackgroundInert() {
88
+ for (const el of _inertSiblings) {
89
+ el.removeAttribute("inert");
90
+ }
91
+ _inertSiblings = [];
92
+ }
71
93
 
72
94
  // components/sanitize.js
73
95
  var ESCAPE_MAP = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
74
96
  var ESCAPE_RE = /[&<>"']/g;
97
+ var CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
98
+ var ALLOWED_URL_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "data:", "mailto:", "tel:"]);
99
+ var BLOCKED_DATA_MIME = /^data:text\/html/i;
75
100
  function escapeHTML(str) {
76
101
  if (typeof str !== "string") return "";
77
102
  return str.replace(ESCAPE_RE, (ch) => ESCAPE_MAP[ch]);
78
103
  }
104
+ function stripControlChars(str) {
105
+ if (typeof str !== "string") return "";
106
+ return str.replace(CONTROL_RE, "");
107
+ }
108
+ function escapeHTMLAttribute(str) {
109
+ return escapeHTML(stripControlChars(str));
110
+ }
79
111
  function sanitizeURL(url) {
80
112
  if (typeof url !== "string") return "";
113
+ const trimmed = url.trim();
114
+ if (/^\s*javascript:/i.test(trimmed) || /^\s*vbscript:/i.test(trimmed)) return "";
115
+ if (BLOCKED_DATA_MIME.test(trimmed)) return "";
81
116
  try {
82
- const parsed = new URL(url, location.href);
83
- if (["http:", "https:", "data:"].includes(parsed.protocol)) return url;
84
- return "";
117
+ const parsed = new URL(trimmed, typeof location !== "undefined" ? location.href : "https://example.invalid/");
118
+ if (!ALLOWED_URL_PROTOCOLS.has(parsed.protocol)) return "";
119
+ if (parsed.protocol === "data:" && BLOCKED_DATA_MIME.test(trimmed)) return "";
120
+ return trimmed;
85
121
  } catch {
86
122
  return "";
87
123
  }
88
124
  }
125
+ var _policy = null;
126
+ function getTrustedPolicy() {
127
+ if (_policy) return _policy;
128
+ if (typeof window !== "undefined" && window.trustedTypes?.createPolicy) {
129
+ _policy = window.trustedTypes.createPolicy("velinstyle", {
130
+ createHTML: (input) => escapeHTML(input)
131
+ });
132
+ }
133
+ return _policy;
134
+ }
135
+ function createSafeHTML(str) {
136
+ const policy = getTrustedPolicy();
137
+ const safe = escapeHTML(stripControlChars(str));
138
+ if (policy?.createHTML) return policy.createHTML(safe);
139
+ return safe;
140
+ }
89
141
 
90
142
  // components/velin-modal.js
91
143
  var styles = `
@@ -220,6 +272,7 @@ var VelinModal = class extends HTMLElement {
220
272
  }
221
273
  _open() {
222
274
  this._previouslyFocused = saveFocus();
275
+ setBackgroundInert(this);
223
276
  document.addEventListener("keydown", this._onKeydown);
224
277
  document.body.style.overflow = "hidden";
225
278
  requestAnimationFrame(() => {
@@ -230,6 +283,7 @@ var VelinModal = class extends HTMLElement {
230
283
  _close() {
231
284
  document.removeEventListener("keydown", this._onKeydown);
232
285
  document.body.style.overflow = "";
286
+ clearBackgroundInert();
233
287
  restoreFocus(this._previouslyFocused);
234
288
  }
235
289
  _onKeydown(event) {
@@ -308,6 +362,8 @@ var VelinDropdown = class extends HTMLElement {
308
362
  this.attachShadow({ mode: "open", delegatesFocus: true });
309
363
  this._onDocClick = this._onDocClick.bind(this);
310
364
  this._onKeydown = this._onKeydown.bind(this);
365
+ this._typeahead = "";
366
+ this._typeaheadTimer = null;
311
367
  }
312
368
  connectedCallback() {
313
369
  const align = this.getAttribute("align") || "start";
@@ -319,16 +375,29 @@ var VelinDropdown = class extends HTMLElement {
319
375
  </div>
320
376
  `;
321
377
  const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
378
+ const menuSlot = this.shadowRoot.querySelector("slot:not([name])");
322
379
  triggerSlot.addEventListener("click", () => this.toggle());
323
380
  triggerSlot.addEventListener("slotchange", () => {
324
381
  const trigger = triggerSlot.assignedElements()[0];
325
382
  if (trigger) {
326
383
  trigger.setAttribute("aria-haspopup", "menu");
327
384
  trigger.setAttribute("aria-expanded", this.hasAttribute("open") ? "true" : "false");
385
+ const menuId = this._menuId || (this._menuId = `velin-dropdown-menu-${Math.random().toString(36).slice(2, 9)}`);
386
+ trigger.setAttribute("aria-controls", menuId);
387
+ this.shadowRoot.querySelector(".menu")?.setAttribute("id", menuId);
328
388
  }
329
389
  });
390
+ menuSlot?.addEventListener("slotchange", () => this._normalizeMenuItems());
391
+ this._normalizeMenuItems();
330
392
  this.addEventListener("keydown", this._onKeydown);
331
393
  }
394
+ _normalizeMenuItems() {
395
+ const items = this._getMenuItems();
396
+ items.forEach((el, i) => {
397
+ if (!el.hasAttribute("role")) el.setAttribute("role", "menuitem");
398
+ el.setAttribute("tabindex", i === 0 ? "0" : "-1");
399
+ });
400
+ }
332
401
  toggle() {
333
402
  if (this.hasAttribute("open")) {
334
403
  this.close();
@@ -372,9 +441,24 @@ var VelinDropdown = class extends HTMLElement {
372
441
  return;
373
442
  }
374
443
  const items = this._getMenuItems();
375
- if (items.length > 0) {
376
- rovingTabindex(this, items, event);
444
+ if (items.length === 0) return;
445
+ if (event.key.length === 1 && /[a-z0-9]/i.test(event.key)) {
446
+ clearTimeout(this._typeaheadTimer);
447
+ this._typeahead += event.key.toLowerCase();
448
+ this._typeaheadTimer = setTimeout(() => {
449
+ this._typeahead = "";
450
+ }, 500);
451
+ const match = items.find(
452
+ (el) => (el.textContent?.trim().toLowerCase() || "").startsWith(this._typeahead)
453
+ );
454
+ if (match) {
455
+ event.preventDefault();
456
+ items.forEach((item) => item.setAttribute("tabindex", item === match ? "0" : "-1"));
457
+ match.focus();
458
+ }
459
+ return;
377
460
  }
461
+ rovingTabindex(this, items, event);
378
462
  }
379
463
  disconnectedCallback() {
380
464
  document.removeEventListener("click", this._onDocClick, true);
@@ -422,12 +506,25 @@ var VelinAccordion = class extends HTMLElement {
422
506
  connectedCallback() {
423
507
  this.shadowRoot.innerHTML = `
424
508
  <style>${styles3}</style>
425
- <div role="region" part="region"><slot></slot></div>
509
+ <slot></slot>
426
510
  `;
427
511
  this._exclusive = this.hasAttribute("exclusive");
512
+ this._wireDetails();
428
513
  this.addEventListener("toggle", this._onToggle, true);
429
514
  this.addEventListener("keydown", this._onKeydown.bind(this));
430
515
  }
516
+ _wireDetails() {
517
+ let panelIndex = 0;
518
+ for (const details of this.querySelectorAll("details")) {
519
+ const summary = details.querySelector("summary");
520
+ const panel = details.querySelector(":scope > :not(summary)");
521
+ const panelId = panel?.id || `velin-accordion-panel-${++panelIndex}`;
522
+ if (panel && !panel.id) panel.id = panelId;
523
+ if (summary && panel) {
524
+ summary.setAttribute("aria-controls", panelId);
525
+ }
526
+ }
527
+ }
431
528
  _onToggle(event) {
432
529
  if (!this._exclusive) return;
433
530
  const openedDetail = event.target;
@@ -714,12 +811,29 @@ var PROVIDER_CDNS = {
714
811
  heroicons: "https://unpkg.com/heroicons@2/24/outline/{name}.svg",
715
812
  bootstrap: "https://unpkg.com/bootstrap-icons@latest/icons/{name}.svg",
716
813
  material: "https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/{name}/default/24px.svg",
717
- fontawesome: "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/regular/{name}.svg"
814
+ fontawesome: "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/{name}.svg"
815
+ };
816
+ var PROVIDER_VARIANTS = {
817
+ fontawesome: {
818
+ regular: "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/regular/{name}.svg",
819
+ solid: "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/{name}.svg",
820
+ brands: "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/{name}.svg"
821
+ },
822
+ heroicons: {
823
+ outline: "https://unpkg.com/heroicons@2/24/outline/{name}.svg",
824
+ solid: "https://unpkg.com/heroicons@2/24/solid/{name}.svg",
825
+ mini: "https://unpkg.com/heroicons@2/20/solid/{name}.svg"
826
+ }
718
827
  };
828
+ function resolveProviderUrl(provider, variant) {
829
+ const variants = PROVIDER_VARIANTS[provider];
830
+ if (variant && variants?.[variant]) return variants[variant];
831
+ return PROVIDER_CDNS[provider];
832
+ }
719
833
  var _svgCache = /* @__PURE__ */ new Map();
720
834
  var VelinIcon = class extends HTMLElement {
721
835
  static get observedAttributes() {
722
- return ["name", "size", "label", "provider", "sprite"];
836
+ return ["name", "size", "label", "provider", "variant", "sprite"];
723
837
  }
724
838
  constructor() {
725
839
  super();
@@ -736,12 +850,13 @@ var VelinIcon = class extends HTMLElement {
736
850
  const size = this.getAttribute("size") || "24";
737
851
  const label = this.getAttribute("label");
738
852
  const provider = this.getAttribute("provider");
853
+ const variant = this.getAttribute("variant");
739
854
  if (!name) {
740
855
  this.innerHTML = "";
741
856
  return;
742
857
  }
743
- if (provider && PROVIDER_CDNS[provider]) {
744
- this._renderFromCDN(name, size, label, provider);
858
+ if (provider && (PROVIDER_CDNS[provider] || PROVIDER_VARIANTS[provider])) {
859
+ this._renderFromCDN(name, size, label, provider, variant);
745
860
  return;
746
861
  }
747
862
  this._renderFromSprite(name, size, label);
@@ -760,20 +875,34 @@ var VelinIcon = class extends HTMLElement {
760
875
  this._applyStyle(svg);
761
876
  this._applyA11y(svg, label);
762
877
  const use = document.createElementNS(svgNS, "use");
763
- const spriteUrl = this.getAttribute("sprite") || "velin-icons.svg";
764
- use.setAttribute("href", `${spriteUrl}#${name}`);
878
+ const spriteAttr = this.getAttribute("sprite");
879
+ const localSymbol = document.getElementById(name);
880
+ const isLocalSymbol = localSymbol && localSymbol.tagName && localSymbol.tagName.toLowerCase() === "symbol";
881
+ let href;
882
+ if (spriteAttr === "" || spriteAttr == null && isLocalSymbol) {
883
+ href = `#${name}`;
884
+ } else {
885
+ const spriteUrl = spriteAttr || "velin-icons.svg";
886
+ href = `${spriteUrl}#${name}`;
887
+ }
888
+ use.setAttribute("href", href);
765
889
  svg.appendChild(use);
766
890
  this.innerHTML = "";
767
891
  this.appendChild(svg);
768
892
  this._rendered = true;
769
893
  }
770
- async _renderFromCDN(name, size, label, provider) {
771
- const cacheKey = `${provider}:${name}`;
894
+ async _renderFromCDN(name, size, label, provider, variant) {
895
+ const cacheKey = `${provider}:${variant || "default"}:${name}`;
772
896
  if (_svgCache.has(cacheKey)) {
773
897
  this._injectSVG(_svgCache.get(cacheKey), size, label);
774
898
  return;
775
899
  }
776
- const url = PROVIDER_CDNS[provider].replace("{name}", name);
900
+ const template = resolveProviderUrl(provider, variant);
901
+ if (!template) {
902
+ this._renderFromSprite(name, size, label);
903
+ return;
904
+ }
905
+ const url = template.replace("{name}", name);
777
906
  try {
778
907
  const res = await fetch(url);
779
908
  if (!res.ok) throw new Error(`${res.status}`);
@@ -885,12 +1014,13 @@ var VelinDrawer = class extends HTMLElement {
885
1014
  connectedCallback() {
886
1015
  const title = this.getAttribute("title") || "";
887
1016
  const safeTitle = escapeHTML(title);
1017
+ const titleId = "velin-drawer-title";
888
1018
  this.shadowRoot.innerHTML = `
889
1019
  <style>${styles6}</style>
890
1020
  <div class="overlay" part="overlay"></div>
891
- <div class="drawer" role="dialog" aria-modal="true" aria-label="${safeTitle}" part="drawer">
1021
+ <div class="drawer" role="dialog" aria-modal="true" aria-labelledby="${titleId}" part="drawer">
892
1022
  <div class="header" part="header">
893
- <h2 class="title">${safeTitle}</h2>
1023
+ <h2 class="title" id="${titleId}">${safeTitle}</h2>
894
1024
  <button class="close-btn" aria-label="Close" part="close">&#215;</button>
895
1025
  </div>
896
1026
  <div class="body" part="body"><slot></slot></div>
@@ -911,6 +1041,7 @@ var VelinDrawer = class extends HTMLElement {
911
1041
  }
912
1042
  _open() {
913
1043
  this._prev = saveFocus();
1044
+ setBackgroundInert(this);
914
1045
  document.addEventListener("keydown", this._onKey);
915
1046
  document.body.style.overflow = "hidden";
916
1047
  requestAnimationFrame(() => {
@@ -921,6 +1052,7 @@ var VelinDrawer = class extends HTMLElement {
921
1052
  _close() {
922
1053
  document.removeEventListener("keydown", this._onKey);
923
1054
  document.body.style.overflow = "";
1055
+ clearBackgroundInert();
924
1056
  restoreFocus(this._prev);
925
1057
  }
926
1058
  _onKey(e) {
@@ -939,86 +1071,284 @@ customElements.define("velin-drawer", VelinDrawer);
939
1071
  var velin_drawer_default = VelinDrawer;
940
1072
 
941
1073
  // components/velin-theme-toggle.js
1074
+ var THEMES = [
1075
+ { slug: "", label: "Default (Light)" },
1076
+ { slug: "dark", label: "Dark" },
1077
+ { slug: "brutalist", label: "Brutalist" },
1078
+ { slug: "corporate", label: "Corporate" },
1079
+ { slug: "earth", label: "Earth" },
1080
+ { slug: "forest", label: "Forest" },
1081
+ { slug: "midnight", label: "Midnight" },
1082
+ { slug: "neon", label: "Neon" },
1083
+ { slug: "nordic", label: "Nordic" },
1084
+ { slug: "ocean", label: "Ocean" },
1085
+ { slug: "pastel", label: "Pastel" },
1086
+ { slug: "retro", label: "Retro" },
1087
+ { slug: "sharp", label: "Sharp" },
1088
+ { slug: "soft", label: "Soft" },
1089
+ { slug: "sunset", label: "Sunset" }
1090
+ ];
1091
+ var BUILTIN_THEMES = /* @__PURE__ */ new Set(["", "dark"]);
1092
+ var loadedThemeStylesheets = /* @__PURE__ */ new Set();
1093
+ function ensureThemeStylesheet(slug, base) {
1094
+ if (!slug || BUILTIN_THEMES.has(slug)) return;
1095
+ if (loadedThemeStylesheets.has(slug)) return;
1096
+ const existing = document.querySelector(`link[data-velin-theme-css="${slug}"]`);
1097
+ if (existing) {
1098
+ loadedThemeStylesheets.add(slug);
1099
+ return;
1100
+ }
1101
+ const link = document.createElement("link");
1102
+ link.rel = "stylesheet";
1103
+ link.href = `${base.replace(/\/$/, "")}/${slug}.min.css`;
1104
+ link.setAttribute("data-velin-theme-css", slug);
1105
+ document.head.appendChild(link);
1106
+ loadedThemeStylesheets.add(slug);
1107
+ }
942
1108
  var styles7 = `
943
- :host { display: inline-flex; }
1109
+ :host { display: inline-flex; position: relative; }
1110
+ .group {
1111
+ display: inline-flex; align-items: stretch;
1112
+ border: 2px solid var(--velin-color-border, #ddd);
1113
+ border-radius: var(--velin-radius-md, 0.5rem);
1114
+ background: none;
1115
+ overflow: hidden;
1116
+ }
944
1117
  button {
945
1118
  display: inline-flex; align-items: center; justify-content: center;
946
- min-width: 2.75rem; min-height: 2.75rem; padding: 0.5rem;
947
- background: none; border: 2px solid var(--velin-color-border, #ddd);
948
- border-radius: var(--velin-radius-md, 0.5rem); cursor: pointer;
949
- color: var(--velin-color-text, #111); transition: background 150ms ease, border-color 150ms ease;
1119
+ min-height: 2.75rem; padding: 0.5rem;
1120
+ background: none; border: 0; cursor: pointer;
1121
+ color: var(--velin-color-text, #111);
1122
+ transition: background 150ms ease;
1123
+ }
1124
+ button:hover { background: var(--velin-color-surface-dim, #eee); }
1125
+ button:focus-visible {
1126
+ outline: 3px solid var(--velin-color-focus, #2563eb);
1127
+ outline-offset: 2px;
1128
+ }
1129
+ .toggle { min-width: 2.75rem; }
1130
+ .picker {
1131
+ min-width: 1.75rem;
1132
+ border-inline-start: 1px solid var(--velin-color-border, #ddd);
1133
+ color: var(--velin-color-text-muted, #555);
950
1134
  }
951
- button:hover { background: var(--velin-color-surface-dim, #eee); border-color: var(--velin-color-border-strong, #999); }
952
- button:focus-visible { outline: 3px solid var(--velin-color-focus, #2563eb); outline-offset: 2px; }
953
1135
  svg { width: 1.25rem; height: 1.25rem; transition: transform 300ms ease; }
1136
+ .chev { width: 0.75rem; height: 0.75rem; }
954
1137
  :host([theme="dark"]) .sun { display: none; }
955
1138
  :host(:not([theme="dark"])) .moon { display: none; }
1139
+ :host([compact]) .picker { display: none; }
1140
+ :host([compact]) .toggle { border-inline-end: 0; }
956
1141
  @media (prefers-reduced-motion: reduce) { svg { transition: none; } }
1142
+
1143
+ .menu {
1144
+ position: absolute;
1145
+ top: calc(100% + 0.5rem);
1146
+ inset-inline-end: 0;
1147
+ z-index: 1000;
1148
+ min-width: 12rem;
1149
+ padding: 0.375rem;
1150
+ background: var(--velin-color-surface-bright, #fff);
1151
+ border: 1px solid var(--velin-color-border, #ddd);
1152
+ border-radius: var(--velin-radius-md, 0.5rem);
1153
+ box-shadow: var(--velin-shadow-lg, 0 12px 32px rgba(0,0,0,0.12));
1154
+ list-style: none;
1155
+ margin: 0;
1156
+ display: none;
1157
+ max-height: min(70vh, 24rem);
1158
+ overflow-y: auto;
1159
+ }
1160
+ :host([menu-open]) .menu { display: block; }
1161
+ .menu li { margin: 0; }
1162
+ .menu button {
1163
+ width: 100%;
1164
+ justify-content: flex-start;
1165
+ padding: 0.4rem 0.75rem;
1166
+ font-size: 0.875rem;
1167
+ color: var(--velin-color-text, #111);
1168
+ border-radius: var(--velin-radius-sm, 0.25rem);
1169
+ min-height: 2rem;
1170
+ text-align: start;
1171
+ }
1172
+ .menu button:hover,
1173
+ .menu button[aria-current="true"] {
1174
+ background: var(--velin-color-primary-subtle, #eef);
1175
+ color: var(--velin-color-primary, #2a4cf0);
1176
+ }
1177
+ .menu button[aria-current="true"] {
1178
+ font-weight: 600;
1179
+ }
1180
+ .menu .swatch {
1181
+ width: 0.75rem; height: 0.75rem;
1182
+ border-radius: 50%;
1183
+ margin-inline-end: 0.5rem;
1184
+ background: currentColor;
1185
+ border: 1px solid var(--velin-color-border, #ddd);
1186
+ }
957
1187
  `;
958
1188
  var VelinThemeToggle = class extends HTMLElement {
959
1189
  constructor() {
960
1190
  super();
961
1191
  this.attachShadow({ mode: "open" });
1192
+ this._onDocClick = this._onDocClick.bind(this);
1193
+ this._onKeyDown = this._onKeyDown.bind(this);
962
1194
  }
963
1195
  connectedCallback() {
964
1196
  this.shadowRoot.innerHTML = `
965
1197
  <style>${styles7}</style>
966
- <button part="button" aria-label="Toggle dark mode">
967
- <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
968
- <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
969
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
970
- <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
971
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
972
- </svg>
973
- <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
974
- <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
975
- </svg>
976
- </button>
1198
+ <div class="group" part="group">
1199
+ <button class="toggle" part="button" aria-label="Toggle dark mode">
1200
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1201
+ <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
1202
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
1203
+ <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
1204
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
1205
+ </svg>
1206
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1207
+ <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
1208
+ </svg>
1209
+ </button>
1210
+ <button class="picker" part="picker" aria-label="Choose theme" aria-haspopup="menu" aria-expanded="false">
1211
+ <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1212
+ <polyline points="6 9 12 15 18 9"/>
1213
+ </svg>
1214
+ </button>
1215
+ </div>
1216
+ <ul class="menu" role="menu" hidden></ul>
977
1217
  `;
978
- const target = this.getAttribute("target") || "html";
979
- const el = document.querySelector(target);
980
- const stored = localStorage.getItem("velin-theme");
1218
+ this._target = document.querySelector(this.getAttribute("target") || "html");
1219
+ this._themesBase = this.getAttribute("themes-base") || "dist/themes";
1220
+ this._menu = this.shadowRoot.querySelector(".menu");
1221
+ this._toggleBtn = this.shadowRoot.querySelector(".toggle");
1222
+ this._pickerBtn = this.shadowRoot.querySelector(".picker");
1223
+ this._renderMenu();
1224
+ this._initPreference();
1225
+ this._toggleBtn.addEventListener("click", () => this._toggleDarkMode());
1226
+ this._pickerBtn.addEventListener("click", (e) => {
1227
+ e.stopPropagation();
1228
+ this._toggleMenu();
1229
+ });
1230
+ document.addEventListener("click", this._onDocClick);
1231
+ this.shadowRoot.addEventListener("keydown", this._onKeyDown);
981
1232
  const prefersDarkMq = window.matchMedia("(prefers-color-scheme: dark)");
982
- const applyFromPreference = () => {
983
- if (localStorage.getItem("velin-theme")) return;
984
- if (prefersDarkMq.matches) {
985
- el?.setAttribute("data-velin-theme", "dark");
986
- this.setAttribute("theme", "dark");
987
- } else {
988
- el?.removeAttribute("data-velin-theme");
989
- this.removeAttribute("theme");
990
- }
991
- };
992
- const prefersDark = prefersDarkMq.matches;
993
- if (stored === "dark" || !stored && prefersDark) {
994
- el?.setAttribute("data-velin-theme", "dark");
995
- this.setAttribute("theme", "dark");
996
- }
997
- prefersDarkMq.addEventListener("change", applyFromPreference);
1233
+ prefersDarkMq.addEventListener("change", () => this._applyFromPreference());
998
1234
  window.addEventListener("storage", (e) => {
999
- if (e.key !== "velin-theme" || !el) return;
1000
- if (e.newValue === "dark") {
1001
- el.setAttribute("data-velin-theme", "dark");
1002
- this.setAttribute("theme", "dark");
1003
- } else {
1004
- el.removeAttribute("data-velin-theme");
1005
- this.removeAttribute("theme");
1006
- }
1235
+ if (e.key === "velin-theme") this._readStorage();
1007
1236
  });
1008
- this.shadowRoot.querySelector("button").addEventListener("click", () => {
1009
- const isDark = el?.getAttribute("data-velin-theme") === "dark";
1010
- if (isDark) {
1011
- el?.removeAttribute("data-velin-theme");
1012
- this.removeAttribute("theme");
1013
- localStorage.setItem("velin-theme", "light");
1014
- } else {
1015
- el?.setAttribute("data-velin-theme", "dark");
1016
- this.setAttribute("theme", "dark");
1017
- localStorage.setItem("velin-theme", "dark");
1018
- }
1019
- this.dispatchEvent(new CustomEvent("velin-theme-change", { bubbles: true, detail: { theme: isDark ? "light" : "dark", dark: !isDark } }));
1237
+ }
1238
+ disconnectedCallback() {
1239
+ document.removeEventListener("click", this._onDocClick);
1240
+ }
1241
+ _renderMenu() {
1242
+ this._menu.innerHTML = THEMES.map((t) => `
1243
+ <li role="none">
1244
+ <button type="button" role="menuitem" data-theme="${t.slug}">
1245
+ <span class="swatch" aria-hidden="true" data-theme-swatch="${t.slug}"></span>
1246
+ ${t.label}
1247
+ </button>
1248
+ </li>
1249
+ `).join("");
1250
+ this._menu.removeAttribute("hidden");
1251
+ this._menu.querySelectorAll("button[data-theme]").forEach((btn) => {
1252
+ btn.addEventListener("click", () => {
1253
+ const slug = btn.getAttribute("data-theme");
1254
+ this._applyTheme(slug, { persist: true });
1255
+ this._closeMenu();
1256
+ });
1257
+ });
1258
+ }
1259
+ _toggleMenu() {
1260
+ if (this.hasAttribute("menu-open")) this._closeMenu();
1261
+ else this._openMenu();
1262
+ }
1263
+ _openMenu() {
1264
+ this.setAttribute("menu-open", "");
1265
+ this._pickerBtn.setAttribute("aria-expanded", "true");
1266
+ this._highlightActive();
1267
+ const first = this._menu.querySelector("button[data-theme]");
1268
+ if (first) first.focus();
1269
+ }
1270
+ _closeMenu() {
1271
+ this.removeAttribute("menu-open");
1272
+ this._pickerBtn.setAttribute("aria-expanded", "false");
1273
+ }
1274
+ _onDocClick(e) {
1275
+ if (!this.hasAttribute("menu-open")) return;
1276
+ if (e.composedPath().includes(this)) return;
1277
+ this._closeMenu();
1278
+ }
1279
+ _onKeyDown(e) {
1280
+ if (!this.hasAttribute("menu-open")) return;
1281
+ if (e.key === "Escape") {
1282
+ e.preventDefault();
1283
+ this._closeMenu();
1284
+ this._pickerBtn.focus();
1285
+ return;
1286
+ }
1287
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1288
+ e.preventDefault();
1289
+ const items = Array.from(this._menu.querySelectorAll("button[data-theme]"));
1290
+ const idx = items.indexOf(this.shadowRoot.activeElement);
1291
+ const next = e.key === "ArrowDown" ? items[(idx + 1) % items.length] : items[(idx - 1 + items.length) % items.length];
1292
+ next?.focus();
1293
+ }
1294
+ }
1295
+ _highlightActive() {
1296
+ const current = this._currentSlug();
1297
+ this._menu.querySelectorAll("button[data-theme]").forEach((btn) => {
1298
+ const slug = btn.getAttribute("data-theme");
1299
+ if (slug === current) btn.setAttribute("aria-current", "true");
1300
+ else btn.removeAttribute("aria-current");
1020
1301
  });
1021
1302
  }
1303
+ _currentSlug() {
1304
+ if (!this._target) return "";
1305
+ const value = this._target.getAttribute("data-velin-theme");
1306
+ if (!value || value === "light") return "";
1307
+ return value;
1308
+ }
1309
+ _initPreference() {
1310
+ const stored = localStorage.getItem("velin-theme");
1311
+ if (stored) {
1312
+ this._applyTheme(stored, { persist: false });
1313
+ return;
1314
+ }
1315
+ this._applyFromPreference();
1316
+ }
1317
+ _readStorage() {
1318
+ const stored = localStorage.getItem("velin-theme");
1319
+ this._applyTheme(stored || "", { persist: false });
1320
+ }
1321
+ _applyFromPreference() {
1322
+ if (localStorage.getItem("velin-theme")) return;
1323
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
1324
+ this._applyTheme(prefersDark ? "dark" : "", { persist: false });
1325
+ }
1326
+ _applyTheme(slug, { persist }) {
1327
+ const normalized = !slug || slug === "light" ? "" : slug;
1328
+ if (!this._target) return;
1329
+ if (!normalized) {
1330
+ this._target.removeAttribute("data-velin-theme");
1331
+ this.removeAttribute("theme");
1332
+ } else {
1333
+ this._target.setAttribute("data-velin-theme", normalized);
1334
+ this.setAttribute("theme", normalized === "dark" ? "dark" : normalized);
1335
+ ensureThemeStylesheet(normalized, this._themesBase);
1336
+ }
1337
+ if (persist) {
1338
+ if (!normalized) localStorage.removeItem("velin-theme");
1339
+ else localStorage.setItem("velin-theme", normalized);
1340
+ }
1341
+ this._highlightActive();
1342
+ this.dispatchEvent(new CustomEvent("velin-theme-change", {
1343
+ bubbles: true,
1344
+ detail: { theme: normalized || "light", dark: normalized === "dark", slug: normalized }
1345
+ }));
1346
+ }
1347
+ _toggleDarkMode() {
1348
+ const current = this._currentSlug();
1349
+ const next = current === "dark" ? "" : "dark";
1350
+ this._applyTheme(next, { persist: true });
1351
+ }
1022
1352
  };
1023
1353
  customElements.define("velin-theme-toggle", VelinThemeToggle);
1024
1354
  var velin_theme_toggle_default = VelinThemeToggle;
@@ -1038,7 +1368,6 @@ var styles8 = `
1038
1368
  transition: opacity 150ms ease, visibility 150ms ease;
1039
1369
  }
1040
1370
  :host([open]) .popover { opacity: 1; visibility: visible; }
1041
- /* Placement */
1042
1371
  .popover--top { inset-block-end: calc(100% + 0.5rem); inset-inline-start: 50%; transform: translateX(-50%); }
1043
1372
  .popover--bottom { inset-block-start: calc(100% + 0.5rem); inset-inline-start: 50%; transform: translateX(-50%); }
1044
1373
  .popover--start { inset-inline-end: calc(100% + 0.5rem); inset-block-start: 50%; transform: translateY(-50%); }
@@ -1051,6 +1380,7 @@ var styles8 = `
1051
1380
  }
1052
1381
  @media (prefers-reduced-motion: reduce) { .popover { transition: none; } }
1053
1382
  `;
1383
+ var popoverId = 0;
1054
1384
  var VelinPopover = class extends HTMLElement {
1055
1385
  static get observedAttributes() {
1056
1386
  return ["open"];
@@ -1058,42 +1388,52 @@ var VelinPopover = class extends HTMLElement {
1058
1388
  constructor() {
1059
1389
  super();
1060
1390
  this.attachShadow({ mode: "open" });
1391
+ this._popoverId = `velin-popover-${++popoverId}`;
1061
1392
  this._onOutside = this._onOutside.bind(this);
1062
1393
  this._onKey = this._onKey.bind(this);
1394
+ this._prevFocus = null;
1395
+ this._isDialog = false;
1063
1396
  }
1064
1397
  connectedCallback() {
1065
1398
  const placement = this.getAttribute("placement") || "bottom";
1066
1399
  const triggerType = this.getAttribute("trigger") || "click";
1067
1400
  const title = this.getAttribute("title") || "";
1068
1401
  const role = triggerType === "hover" ? "tooltip" : "dialog";
1402
+ this._isDialog = role === "dialog";
1069
1403
  this.shadowRoot.innerHTML = `
1070
1404
  <style>${styles8}</style>
1071
1405
  <slot name="trigger"></slot>
1072
- <div class="popover popover--${placement}" role="${role}" part="popover">
1406
+ <div class="popover popover--${placement}" id="${this._popoverId}" role="${role}" part="popover">
1073
1407
  ${title ? `<div class="popover__title" part="title">${escapeHTML(title)}</div>` : ""}
1074
1408
  <slot></slot>
1075
1409
  </div>
1076
1410
  `;
1077
1411
  const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
1078
- triggerSlot.addEventListener("slotchange", () => {
1079
- const trigger = triggerSlot.assignedElements()[0];
1080
- if (!trigger) return;
1081
- trigger.setAttribute("aria-haspopup", role === "tooltip" ? "true" : "dialog");
1082
- trigger.setAttribute("aria-expanded", "false");
1083
- if (triggerType === "click") {
1084
- trigger.addEventListener("click", () => this.toggle());
1085
- } else if (triggerType === "hover") {
1086
- this.addEventListener("mouseenter", () => this.open());
1087
- this.addEventListener("mouseleave", () => this.close());
1088
- this.addEventListener("focusin", () => this.open());
1089
- this.addEventListener("focusout", (e) => {
1090
- if (!this.contains(e.relatedTarget)) this.close();
1091
- });
1092
- } else if (triggerType === "focus") {
1093
- trigger.addEventListener("focusin", () => this.open());
1094
- trigger.addEventListener("focusout", () => this.close());
1095
- }
1096
- });
1412
+ triggerSlot.addEventListener("slotchange", () => this._wireTrigger(triggerType));
1413
+ this._wireTrigger(triggerType);
1414
+ }
1415
+ _wireTrigger(triggerType) {
1416
+ const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
1417
+ if (!trigger) return;
1418
+ const isHover = triggerType === "hover";
1419
+ trigger.setAttribute("aria-haspopup", isHover ? "true" : "dialog");
1420
+ trigger.setAttribute("aria-expanded", this.hasAttribute("open") ? "true" : "false");
1421
+ if (this._isDialog) {
1422
+ trigger.setAttribute("aria-controls", this._popoverId);
1423
+ }
1424
+ if (triggerType === "click") {
1425
+ trigger.onclick = () => this.toggle();
1426
+ } else if (triggerType === "hover") {
1427
+ this.onmouseenter = () => this.open();
1428
+ this.onmouseleave = () => this.close();
1429
+ this.onfocusin = () => this.open();
1430
+ this.onfocusout = (e) => {
1431
+ if (!this.contains(e.relatedTarget)) this.close();
1432
+ };
1433
+ } else if (triggerType === "focus") {
1434
+ trigger.onfocusin = () => this.open();
1435
+ trigger.onfocusout = () => this.close();
1436
+ }
1097
1437
  }
1098
1438
  open() {
1099
1439
  this.setAttribute("open", "");
@@ -1101,6 +1441,14 @@ var VelinPopover = class extends HTMLElement {
1101
1441
  if (trigger) trigger.setAttribute("aria-expanded", "true");
1102
1442
  document.addEventListener("click", this._onOutside, true);
1103
1443
  document.addEventListener("keydown", this._onKey);
1444
+ if (this._isDialog) {
1445
+ this._prevFocus = saveFocus();
1446
+ requestAnimationFrame(() => {
1447
+ const focusable = getFocusableElements(this.shadowRoot.querySelector(".popover"));
1448
+ if (focusable.length) focusable[0].focus();
1449
+ else this.shadowRoot.querySelector(".popover")?.focus();
1450
+ });
1451
+ }
1104
1452
  }
1105
1453
  close() {
1106
1454
  this.removeAttribute("open");
@@ -1108,6 +1456,10 @@ var VelinPopover = class extends HTMLElement {
1108
1456
  if (trigger) trigger.setAttribute("aria-expanded", "false");
1109
1457
  document.removeEventListener("click", this._onOutside, true);
1110
1458
  document.removeEventListener("keydown", this._onKey);
1459
+ if (this._isDialog && this._prevFocus) {
1460
+ restoreFocus(this._prevFocus);
1461
+ this._prevFocus = null;
1462
+ }
1111
1463
  }
1112
1464
  toggle() {
1113
1465
  this.hasAttribute("open") ? this.close() : this.open();
@@ -1116,7 +1468,15 @@ var VelinPopover = class extends HTMLElement {
1116
1468
  if (!this.contains(e.target)) this.close();
1117
1469
  }
1118
1470
  _onKey(e) {
1119
- if (e.key === "Escape") this.close();
1471
+ if (e.key === "Escape") {
1472
+ this.close();
1473
+ const trigger = this.querySelector('[slot="trigger"]');
1474
+ if (trigger) trigger.focus();
1475
+ return;
1476
+ }
1477
+ if (this._isDialog && this.hasAttribute("open")) {
1478
+ trapFocus(this.shadowRoot.querySelector(".popover"), e);
1479
+ }
1120
1480
  }
1121
1481
  disconnectedCallback() {
1122
1482
  document.removeEventListener("click", this._onOutside, true);
@@ -1280,9 +1640,15 @@ var styles11 = `
1280
1640
  button:disabled { opacity: 0.3; cursor: not-allowed; }
1281
1641
  svg { width: 1.25rem; height: 1.25rem; }
1282
1642
  .indicators {
1283
- display: flex; justify-content: center; gap: var(--velin-space-2, 0.5rem);
1643
+ display: flex; justify-content: center; align-items: center; gap: var(--velin-space-2, 0.5rem);
1284
1644
  padding-block: var(--velin-space-3, 0.75rem);
1285
1645
  }
1646
+ .pause-btn {
1647
+ font-size: var(--velin-text-xs, 0.75rem);
1648
+ padding-inline: var(--velin-space-3, 0.75rem);
1649
+ min-inline-size: auto;
1650
+ border-radius: var(--velin-radius-md, 0.375rem);
1651
+ }
1286
1652
  .dot {
1287
1653
  display: inline-flex; align-items: center; justify-content: center;
1288
1654
  min-width: 2.75rem; min-height: 2.75rem;
@@ -1309,6 +1675,7 @@ var VelinCarousel = class extends HTMLElement {
1309
1675
  this._index = 0;
1310
1676
  this._timer = null;
1311
1677
  this._startX = 0;
1678
+ this._autoplayPaused = false;
1312
1679
  }
1313
1680
  connectedCallback() {
1314
1681
  this.shadowRoot.innerHTML = `
@@ -1322,7 +1689,7 @@ var VelinCarousel = class extends HTMLElement {
1322
1689
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 6 15 12 9 18"/></svg>
1323
1690
  </button>
1324
1691
  </div>
1325
- <div class="indicators" role="tablist" aria-label="Slide indicators" part="indicators"></div>
1692
+ <div class="indicators" role="group" aria-label="Slide indicators" part="indicators"></div>
1326
1693
  `;
1327
1694
  this.shadowRoot.querySelector(".prev").addEventListener("click", () => this.prev());
1328
1695
  this.shadowRoot.querySelector(".next").addEventListener("click", () => this.next());
@@ -1401,12 +1768,37 @@ var VelinCarousel = class extends HTMLElement {
1401
1768
  c.innerHTML = "";
1402
1769
  this._slides.forEach((_, i) => {
1403
1770
  const d = document.createElement("button");
1771
+ d.type = "button";
1404
1772
  d.className = "dot";
1405
- d.setAttribute("role", "tab");
1406
- d.setAttribute("aria-label", `Slide ${i + 1}`);
1773
+ d.setAttribute("aria-label", `Go to slide ${i + 1}`);
1407
1774
  d.addEventListener("click", () => this.goTo(i));
1408
1775
  c.appendChild(d);
1409
1776
  });
1777
+ if (this.hasAttribute("autoplay")) {
1778
+ const pause = document.createElement("button");
1779
+ pause.type = "button";
1780
+ pause.className = "pause-btn";
1781
+ pause.setAttribute("aria-pressed", "false");
1782
+ pause.setAttribute("aria-label", "Pause automatic slide show");
1783
+ pause.textContent = "Pause";
1784
+ pause.addEventListener("click", () => this._toggleAutoplayPause(pause));
1785
+ c.appendChild(pause);
1786
+ }
1787
+ this._update();
1788
+ }
1789
+ _toggleAutoplayPause(btn) {
1790
+ this._autoplayPaused = !this._autoplayPaused;
1791
+ if (this._autoplayPaused) {
1792
+ this._pause();
1793
+ btn.setAttribute("aria-pressed", "true");
1794
+ btn.setAttribute("aria-label", "Resume automatic slide show");
1795
+ btn.textContent = "Play";
1796
+ } else {
1797
+ this._resume();
1798
+ btn.setAttribute("aria-pressed", "false");
1799
+ btn.setAttribute("aria-label", "Pause automatic slide show");
1800
+ btn.textContent = "Pause";
1801
+ }
1410
1802
  }
1411
1803
  _emit() {
1412
1804
  this.dispatchEvent(new CustomEvent("velin-slide-change", { bubbles: true, detail: { index: this._index } }));
@@ -1422,7 +1814,7 @@ var VelinCarousel = class extends HTMLElement {
1422
1814
  }
1423
1815
  }
1424
1816
  _resume() {
1425
- if (this.hasAttribute("autoplay") && !this._timer) this._startAutoplay();
1817
+ if (this.hasAttribute("autoplay") && !this._timer && !this._autoplayPaused) this._startAutoplay();
1426
1818
  }
1427
1819
  disconnectedCallback() {
1428
1820
  this._pause();
@@ -1444,6 +1836,11 @@ var styles12 = `
1444
1836
  .inner { min-height: 0; }
1445
1837
  @media (prefers-reduced-motion: reduce) { .content { transition: none; } }
1446
1838
  `;
1839
+ var collapseId = 0;
1840
+ function isButtonLike(el) {
1841
+ const tag = el.tagName;
1842
+ return tag === "BUTTON" || tag === "A" && el.hasAttribute("href") || el.getAttribute("role") === "button";
1843
+ }
1447
1844
  var VelinCollapse = class extends HTMLElement {
1448
1845
  static get observedAttributes() {
1449
1846
  return ["open"];
@@ -1451,28 +1848,41 @@ var VelinCollapse = class extends HTMLElement {
1451
1848
  constructor() {
1452
1849
  super();
1453
1850
  this.attachShadow({ mode: "open" });
1851
+ this._contentId = `velin-collapse-panel-${++collapseId}`;
1852
+ this._onTriggerKey = this._onTriggerKey.bind(this);
1454
1853
  }
1455
1854
  connectedCallback() {
1456
- this.shadowRoot.innerHTML = `
1457
- <style>${styles12}</style>
1458
- <slot name="trigger"></slot>
1459
- <div class="content" role="region" part="content">
1460
- <div class="inner"><slot></slot></div>
1461
- </div>
1462
- `;
1855
+ const panelId = this._contentId;
1856
+ this.shadowRoot.innerHTML = "<style>" + styles12 + '</style><slot name="trigger"></slot><div class="content" id="' + panelId + '" part="content"><div class="inner"><slot></slot></div></div>';
1463
1857
  const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
1464
- triggerSlot.addEventListener("slotchange", () => {
1465
- const trigger = triggerSlot.assignedElements()[0];
1466
- if (trigger) {
1467
- trigger.addEventListener("click", () => this.toggle());
1468
- trigger.setAttribute("aria-expanded", this.hasAttribute("open"));
1469
- }
1470
- });
1858
+ triggerSlot.addEventListener("slotchange", () => this._wireTrigger());
1859
+ this._wireTrigger();
1860
+ }
1861
+ _wireTrigger() {
1862
+ const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
1863
+ if (!trigger) return;
1864
+ if (!isButtonLike(trigger)) {
1865
+ trigger.setAttribute("role", "button");
1866
+ if (!trigger.hasAttribute("tabindex")) trigger.setAttribute("tabindex", "0");
1867
+ }
1868
+ trigger.setAttribute("aria-controls", this._contentId);
1869
+ trigger.setAttribute("aria-expanded", this.hasAttribute("open") ? "true" : "false");
1870
+ trigger.removeEventListener("click", this._onClick);
1871
+ trigger.removeEventListener("keydown", this._onTriggerKey);
1872
+ this._onClick = () => this.toggle();
1873
+ trigger.addEventListener("click", this._onClick);
1874
+ trigger.addEventListener("keydown", this._onTriggerKey);
1875
+ }
1876
+ _onTriggerKey(e) {
1877
+ if (e.key === "Enter" || e.key === " ") {
1878
+ e.preventDefault();
1879
+ this.toggle();
1880
+ }
1471
1881
  }
1472
1882
  attributeChangedCallback(name) {
1473
1883
  if (name === "open") {
1474
1884
  const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
1475
- if (trigger) trigger.setAttribute("aria-expanded", this.hasAttribute("open"));
1885
+ if (trigger) trigger.setAttribute("aria-expanded", this.hasAttribute("open") ? "true" : "false");
1476
1886
  }
1477
1887
  }
1478
1888
  toggle() {
@@ -1560,6 +1970,7 @@ var styles13 = `
1560
1970
  .tip[data-placement="end"] { left: calc(100% + 6px); top: 50%; transform: translateY(-50%); }
1561
1971
  @media (prefers-reduced-motion: reduce) { .tip { transition: none; } }
1562
1972
  `;
1973
+ var tooltipId = 0;
1563
1974
  var VelinTooltipWC = class extends HTMLElement {
1564
1975
  static get observedAttributes() {
1565
1976
  return ["content", "placement"];
@@ -1567,13 +1978,15 @@ var VelinTooltipWC = class extends HTMLElement {
1567
1978
  constructor() {
1568
1979
  super();
1569
1980
  this.attachShadow({ mode: "open" });
1981
+ this._tipId = `velin-tooltip-${++tooltipId}`;
1570
1982
  }
1571
1983
  connectedCallback() {
1572
1984
  const placement = this.getAttribute("placement") || "top";
1985
+ const D = "div";
1573
1986
  this.shadowRoot.innerHTML = `
1574
1987
  <style>${styles13}</style>
1575
1988
  <slot></slot>
1576
- <div class="tip" role="tooltip" data-placement="${escapeHTML(placement)}" part="tip">${escapeHTML(this.getAttribute("content") || "")}</div>
1989
+ <${D} class="tip" id="${this._tipId}" role="tooltip" data-placement="${escapeHTML(placement)}" part="tip">${escapeHTML(this.getAttribute("content") || "")}</${D}>
1577
1990
  `;
1578
1991
  this.addEventListener("mouseenter", () => this._show());
1579
1992
  this.addEventListener("mouseleave", () => this._hide());
@@ -1582,13 +1995,29 @@ var VelinTooltipWC = class extends HTMLElement {
1582
1995
  this.addEventListener("keydown", (e) => {
1583
1996
  if (e.key === "Escape") this._hide();
1584
1997
  });
1998
+ const slot = this.shadowRoot.querySelector("slot");
1999
+ slot.addEventListener("slotchange", () => this._linkTrigger());
2000
+ this._linkTrigger();
2001
+ }
2002
+ _linkTrigger() {
2003
+ const trigger = this.shadowRoot.querySelector("slot")?.assignedElements()[0];
2004
+ if (!trigger) return;
2005
+ if (this.hasAttribute("visible")) {
2006
+ trigger.setAttribute("aria-describedby", this._tipId);
2007
+ } else {
2008
+ trigger.removeAttribute("aria-describedby");
2009
+ }
1585
2010
  }
1586
2011
  _show() {
1587
2012
  this.setAttribute("visible", "");
2013
+ const trigger = this.shadowRoot.querySelector("slot")?.assignedElements()[0];
2014
+ if (trigger) trigger.setAttribute("aria-describedby", this._tipId);
1588
2015
  this._flip();
1589
2016
  }
1590
2017
  _hide() {
1591
2018
  this.removeAttribute("visible");
2019
+ const trigger = this.shadowRoot.querySelector("slot")?.assignedElements()[0];
2020
+ if (trigger) trigger.removeAttribute("aria-describedby");
1592
2021
  }
1593
2022
  _flip() {
1594
2023
  const tip = this.shadowRoot.querySelector(".tip");
@@ -2263,6 +2692,1306 @@ var VelinPersist = class extends HTMLElement {
2263
2692
  customElements.define("velin-persist", VelinPersist);
2264
2693
  var velin_persist_default = VelinPersist;
2265
2694
 
2695
+ // components/velin-combobox.js
2696
+ var styles19 = `
2697
+ :host { display: inline-block; position: relative; }
2698
+ .listbox {
2699
+ position: absolute; z-index: var(--velin-z-dropdown, 100);
2700
+ inset-block-start: 100%; inset-inline-start: 0;
2701
+ min-inline-size: 100%; margin-block-start: var(--velin-space-1, 0.25rem);
2702
+ padding-block: var(--velin-space-1, 0.25rem);
2703
+ background: var(--velin-color-surface-bright, #fff);
2704
+ border: 1px solid var(--velin-color-border, #ddd);
2705
+ border-radius: var(--velin-radius-md, 0.5rem);
2706
+ box-shadow: var(--velin-shadow-lg, 0 10px 15px rgba(0,0,0,0.08));
2707
+ opacity: 0; visibility: hidden;
2708
+ transition: opacity 150ms ease, visibility 150ms ease;
2709
+ }
2710
+ :host([open]) .listbox { opacity: 1; visibility: visible; }
2711
+ ::slotted([role="option"]) {
2712
+ display: block; inline-size: 100%;
2713
+ padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
2714
+ min-block-size: 2.5rem;
2715
+ text-align: start; background: none; border: none;
2716
+ cursor: pointer; font-size: var(--velin-text-base, 1rem);
2717
+ }
2718
+ ::slotted([role="option"][aria-selected="true"]) {
2719
+ background: var(--velin-color-surface-dim, #eee);
2720
+ }
2721
+ `;
2722
+ var VelinCombobox = class extends HTMLElement {
2723
+ static get observedAttributes() {
2724
+ return ["open", "aria-label"];
2725
+ }
2726
+ constructor() {
2727
+ super();
2728
+ this.attachShadow({ mode: "open", delegatesFocus: true });
2729
+ this._onDocClick = this._onDocClick.bind(this);
2730
+ this._onKey = this._onKey.bind(this);
2731
+ }
2732
+ connectedCallback() {
2733
+ const listId = `velin-combobox-list-${Math.random().toString(36).slice(2, 9)}`;
2734
+ this._listId = listId;
2735
+ const listLabel = escapeHTML(this.getAttribute("aria-label") || "Options");
2736
+ this.shadowRoot.innerHTML = `
2737
+ <style>${styles19}</style>
2738
+ <slot name="trigger"></slot>
2739
+ <div class="listbox" id="${listId}" role="listbox" aria-label="${listLabel}" part="listbox"><slot></slot></div>
2740
+ `;
2741
+ const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
2742
+ triggerSlot.addEventListener("slotchange", () => this._wireTrigger());
2743
+ this.shadowRoot.querySelector("slot:not([name])")?.addEventListener("slotchange", () => this._wireOptions());
2744
+ this._wireTrigger();
2745
+ this._wireOptions();
2746
+ this.addEventListener("keydown", this._onKey);
2747
+ }
2748
+ _wireTrigger() {
2749
+ const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
2750
+ if (!trigger) return;
2751
+ trigger.setAttribute("role", "combobox");
2752
+ trigger.setAttribute("aria-expanded", this.hasAttribute("open") ? "true" : "false");
2753
+ trigger.setAttribute("aria-controls", this._listId);
2754
+ trigger.setAttribute("aria-autocomplete", "list");
2755
+ if (!trigger.id) trigger.id = `velin-combobox-trigger-${Math.random().toString(36).slice(2, 9)}`;
2756
+ const list = this.shadowRoot.querySelector(".listbox");
2757
+ if (list) list.setAttribute("aria-labelledby", trigger.id);
2758
+ if (!trigger.dataset.velinComboWired) {
2759
+ trigger.dataset.velinComboWired = "1";
2760
+ trigger.addEventListener("click", () => this.toggle());
2761
+ trigger.addEventListener("keydown", (e) => {
2762
+ if (e.key === "ArrowDown" || e.key === "Enter") {
2763
+ e.preventDefault();
2764
+ this.open();
2765
+ }
2766
+ });
2767
+ }
2768
+ }
2769
+ _wireOptions() {
2770
+ const options = this._getOptions();
2771
+ options.forEach((el, i) => {
2772
+ el.setAttribute("role", "option");
2773
+ el.setAttribute("aria-selected", el.hasAttribute("selected") ? "true" : "false");
2774
+ el.setAttribute("tabindex", i === 0 ? "0" : "-1");
2775
+ });
2776
+ }
2777
+ _getOptions() {
2778
+ const slot = this.shadowRoot.querySelector("slot:not([name])");
2779
+ return slot ? slot.assignedElements().filter((el) => !el.hidden) : [];
2780
+ }
2781
+ toggle() {
2782
+ this.hasAttribute("open") ? this.close() : this.open();
2783
+ }
2784
+ open() {
2785
+ this.setAttribute("open", "");
2786
+ const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
2787
+ if (trigger) trigger.setAttribute("aria-expanded", "true");
2788
+ document.addEventListener("click", this._onDocClick, true);
2789
+ requestAnimationFrame(() => {
2790
+ const opts = this._getOptions();
2791
+ if (opts.length) opts[0].focus();
2792
+ });
2793
+ }
2794
+ close() {
2795
+ this.removeAttribute("open");
2796
+ const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
2797
+ if (trigger) {
2798
+ trigger.setAttribute("aria-expanded", "false");
2799
+ trigger.focus();
2800
+ }
2801
+ document.removeEventListener("click", this._onDocClick, true);
2802
+ this.dispatchEvent(new CustomEvent("velin-close", { bubbles: true }));
2803
+ }
2804
+ _onDocClick(e) {
2805
+ if (!this.contains(e.target)) this.close();
2806
+ }
2807
+ _onKey(e) {
2808
+ if (!this.hasAttribute("open")) return;
2809
+ if (e.key === "Escape") {
2810
+ this.close();
2811
+ return;
2812
+ }
2813
+ const options = this._getOptions();
2814
+ if (!options.length) return;
2815
+ rovingTabindex(this, options, e);
2816
+ if (e.key === "Enter" && options.includes(e.target)) {
2817
+ this._selectOption(e.target);
2818
+ this.close();
2819
+ }
2820
+ }
2821
+ _selectOption(el) {
2822
+ this._getOptions().forEach((o) => o.setAttribute("aria-selected", o === el ? "true" : "false"));
2823
+ const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
2824
+ if (trigger && "value" in trigger) trigger.value = el.textContent?.trim() || "";
2825
+ this.dispatchEvent(new CustomEvent("velin-select", { bubbles: true, detail: { option: el } }));
2826
+ }
2827
+ attributeChangedCallback(name) {
2828
+ if (name === "open") this._wireTrigger();
2829
+ if (name === "aria-label") {
2830
+ const list = this.shadowRoot?.querySelector(".listbox");
2831
+ if (list) list.setAttribute("aria-label", escapeHTML(this.getAttribute("aria-label") || "Options"));
2832
+ }
2833
+ }
2834
+ disconnectedCallback() {
2835
+ document.removeEventListener("click", this._onDocClick, true);
2836
+ }
2837
+ };
2838
+ customElements.define("velin-combobox", VelinCombobox);
2839
+ var velin_combobox_default = VelinCombobox;
2840
+
2841
+ // components/velin-bottom-nav.js
2842
+ var styles20 = `
2843
+ :host { display: block; }
2844
+ nav {
2845
+ display: flex;
2846
+ justify-content: space-around;
2847
+ align-items: center;
2848
+ padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
2849
+ padding-block-end: max(var(--velin-space-2, 0.5rem), env(safe-area-inset-bottom, 0px));
2850
+ background: var(--velin-color-surface-bright, #fff);
2851
+ border-block-start: 1px solid var(--velin-color-border, #ddd);
2852
+ }
2853
+ ::slotted(a), ::slotted(button) {
2854
+ display: flex;
2855
+ flex-direction: column;
2856
+ align-items: center;
2857
+ gap: var(--velin-space-1, 0.25rem);
2858
+ min-inline-size: 2.75rem;
2859
+ min-block-size: 2.75rem;
2860
+ padding: var(--velin-space-2, 0.5rem);
2861
+ font-size: var(--velin-text-xs, 0.75rem);
2862
+ color: var(--velin-color-text-muted, #666);
2863
+ text-decoration: none;
2864
+ background: none;
2865
+ border: none;
2866
+ cursor: pointer;
2867
+ }
2868
+ ::slotted([current]) {
2869
+ color: var(--velin-color-primary, #2563eb);
2870
+ font-weight: var(--velin-weight-semibold, 600);
2871
+ }
2872
+ `;
2873
+ var VelinBottomNav = class extends HTMLElement {
2874
+ constructor() {
2875
+ super();
2876
+ this.attachShadow({ mode: "open" });
2877
+ this._onSlot = this._onSlot.bind(this);
2878
+ }
2879
+ connectedCallback() {
2880
+ const label = escapeHTML(this.getAttribute("aria-label") || "Bottom navigation");
2881
+ this.shadowRoot.innerHTML = `
2882
+ <style>${styles20}</style>
2883
+ <nav role="navigation" aria-label="${label}"><slot></slot></nav>
2884
+ `;
2885
+ const slot = this.shadowRoot.querySelector("slot");
2886
+ slot.addEventListener("slotchange", this._onSlot);
2887
+ this._onSlot();
2888
+ }
2889
+ _onSlot() {
2890
+ this._syncCurrent();
2891
+ }
2892
+ _syncCurrent() {
2893
+ const slot = this.shadowRoot?.querySelector("slot");
2894
+ if (!slot) return;
2895
+ const hostKey = this.getAttribute("current");
2896
+ slot.assignedElements().forEach((el) => {
2897
+ const active = el.hasAttribute("current") || hostKey && (el.dataset.nav === hostKey || el.getAttribute("data-nav") === hostKey);
2898
+ if (active) {
2899
+ el.setAttribute("current", "");
2900
+ el.setAttribute("aria-current", "page");
2901
+ } else {
2902
+ el.removeAttribute("current");
2903
+ el.removeAttribute("aria-current");
2904
+ }
2905
+ });
2906
+ }
2907
+ static get observedAttributes() {
2908
+ return ["aria-label", "current"];
2909
+ }
2910
+ attributeChangedCallback(name) {
2911
+ if (name === "aria-label") {
2912
+ const nav = this.shadowRoot?.querySelector("nav");
2913
+ if (nav) nav.setAttribute("aria-label", escapeHTML(this.getAttribute("aria-label") || "Bottom navigation"));
2914
+ }
2915
+ if (name === "current") this._syncCurrent();
2916
+ }
2917
+ };
2918
+ customElements.define("velin-bottom-nav", VelinBottomNav);
2919
+ var velin_bottom_nav_default = VelinBottomNav;
2920
+
2921
+ // components/shadow-a11y-styles.js
2922
+ var SHADOW_A11Y_STYLES = `
2923
+ :host { display: block; }
2924
+ button, [role="button"] {
2925
+ min-inline-size: 2.75rem;
2926
+ min-block-size: 2.75rem;
2927
+ cursor: pointer;
2928
+ }
2929
+ button:focus-visible, [role="button"]:focus-visible {
2930
+ outline: 3px solid var(--velin-color-focus, #2563eb);
2931
+ outline-offset: 2px;
2932
+ }
2933
+ @media (forced-colors: active) {
2934
+ button, [role="button"] {
2935
+ border: 1px solid ButtonText;
2936
+ }
2937
+ }
2938
+ `;
2939
+
2940
+ // components/velin-sheet.js
2941
+ var styles21 = `
2942
+ ${SHADOW_A11Y_STYLES}
2943
+ :host { display: contents; }
2944
+ .overlay {
2945
+ position: fixed; inset: 0; z-index: var(--velin-z-overlay, 400);
2946
+ background: var(--velin-color-overlay, rgba(0,0,0,0.4));
2947
+ opacity: 0; visibility: hidden;
2948
+ transition: opacity 200ms ease, visibility 200ms ease;
2949
+ }
2950
+ :host([open]) .overlay { opacity: 1; visibility: visible; }
2951
+ .sheet {
2952
+ position: fixed; inset-inline: 0; inset-block-end: 0;
2953
+ z-index: var(--velin-z-modal, 500);
2954
+ max-block-size: min(85vh, 32rem);
2955
+ background: var(--velin-color-surface-bright, #fff);
2956
+ border-radius: var(--velin-radius-lg, 0.75rem) var(--velin-radius-lg, 0.75rem) 0 0;
2957
+ box-shadow: var(--velin-shadow-xl, 0 -4px 24px rgba(0,0,0,0.12));
2958
+ display: flex; flex-direction: column;
2959
+ transform: translateY(100%);
2960
+ transition: transform 250ms ease;
2961
+ padding-block-end: env(safe-area-inset-bottom, 0px);
2962
+ }
2963
+ :host([open]) .sheet { transform: translateY(0); }
2964
+ .header {
2965
+ display: flex; align-items: center; justify-content: space-between;
2966
+ padding: var(--velin-space-4, 1rem) var(--velin-space-5, 1.25rem);
2967
+ border-bottom: 1px solid var(--velin-color-border, #ddd);
2968
+ }
2969
+ .title { font-size: var(--velin-text-lg, 1.25rem); font-weight: 600; margin: 0; }
2970
+ .body { flex: 1; overflow-y: auto; padding: var(--velin-space-5, 1.25rem); }
2971
+ @media (prefers-reduced-motion: reduce) { .overlay, .sheet { transition: none; } }
2972
+ `;
2973
+ var VelinSheet = class extends HTMLElement {
2974
+ static get observedAttributes() {
2975
+ return ["open"];
2976
+ }
2977
+ constructor() {
2978
+ super();
2979
+ this.attachShadow({ mode: "open", delegatesFocus: true });
2980
+ this._prev = null;
2981
+ this._onKey = this._onKey.bind(this);
2982
+ }
2983
+ connectedCallback() {
2984
+ const title = escapeHTML(this.getAttribute("title") || this.getAttribute("label") || "");
2985
+ const titleId = "velin-sheet-title";
2986
+ this.shadowRoot.innerHTML = `
2987
+ <style>${styles21}</style>
2988
+ <div class="overlay" part="overlay"></div>
2989
+ <div class="sheet" role="dialog" aria-modal="true" aria-labelledby="${titleId}" part="sheet">
2990
+ <div class="header" part="header">
2991
+ <h2 class="title" id="${titleId}">${title}</h2>
2992
+ <button class="close-btn" aria-label="Close" part="close">&#215;</button>
2993
+ </div>
2994
+ <div class="body" part="body"><slot></slot></div>
2995
+ </div>
2996
+ `;
2997
+ this.shadowRoot.querySelector(".close-btn").addEventListener("click", () => this.close());
2998
+ this.shadowRoot.querySelector(".overlay").addEventListener("click", () => this.close());
2999
+ if (this.hasAttribute("open")) this._open();
3000
+ }
3001
+ attributeChangedCallback(name) {
3002
+ if (name === "open") this.hasAttribute("open") ? this._open() : this._close();
3003
+ }
3004
+ open() {
3005
+ this.setAttribute("open", "");
3006
+ }
3007
+ close() {
3008
+ this.removeAttribute("open");
3009
+ this.dispatchEvent(new CustomEvent("velin-close", { bubbles: true }));
3010
+ }
3011
+ _open() {
3012
+ this._prev = saveFocus();
3013
+ setBackgroundInert(this);
3014
+ document.addEventListener("keydown", this._onKey);
3015
+ document.body.style.overflow = "hidden";
3016
+ requestAnimationFrame(() => {
3017
+ const f = getFocusableElements(this.shadowRoot);
3018
+ if (f.length) f[0].focus();
3019
+ });
3020
+ }
3021
+ _close() {
3022
+ document.removeEventListener("keydown", this._onKey);
3023
+ document.body.style.overflow = "";
3024
+ clearBackgroundInert();
3025
+ restoreFocus(this._prev);
3026
+ }
3027
+ _onKey(e) {
3028
+ if (e.key === "Escape") {
3029
+ this.close();
3030
+ return;
3031
+ }
3032
+ trapFocus(this.shadowRoot, e);
3033
+ }
3034
+ disconnectedCallback() {
3035
+ document.removeEventListener("keydown", this._onKey);
3036
+ document.body.style.overflow = "";
3037
+ }
3038
+ };
3039
+ customElements.define("velin-sheet", VelinSheet);
3040
+ var velin_sheet_default = VelinSheet;
3041
+
3042
+ // components/velin-segmented-control.js
3043
+ var styles22 = `
3044
+ ${SHADOW_A11Y_STYLES}
3045
+ :host { display: block; }
3046
+ .group {
3047
+ display: inline-flex;
3048
+ gap: var(--velin-space-1, 0.25rem);
3049
+ padding: var(--velin-space-1, 0.25rem);
3050
+ background: var(--velin-color-surface-dim, #eee);
3051
+ border-radius: var(--velin-radius-md, 0.5rem);
3052
+ }
3053
+ ::slotted(button) {
3054
+ min-inline-size: 2.75rem;
3055
+ min-block-size: 2.75rem;
3056
+ padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
3057
+ border: none;
3058
+ border-radius: var(--velin-radius-sm, 0.25rem);
3059
+ background: transparent;
3060
+ color: var(--velin-color-text-muted, #666);
3061
+ cursor: pointer;
3062
+ font-size: var(--velin-text-sm, 0.875rem);
3063
+ }
3064
+ ::slotted(button[aria-pressed="true"]) {
3065
+ background: var(--velin-color-surface-bright, #fff);
3066
+ color: var(--velin-color-text, #111);
3067
+ font-weight: var(--velin-weight-semibold, 600);
3068
+ box-shadow: var(--velin-shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
3069
+ }
3070
+ `;
3071
+ var VelinSegmentedControl = class extends HTMLElement {
3072
+ constructor() {
3073
+ super();
3074
+ this.attachShadow({ mode: "open", delegatesFocus: true });
3075
+ this._onClick = this._onClick.bind(this);
3076
+ this._onKey = this._onKey.bind(this);
3077
+ }
3078
+ connectedCallback() {
3079
+ const label = escapeHTML(this.getAttribute("aria-label") || "Segmented control");
3080
+ this.shadowRoot.innerHTML = `
3081
+ <style>${styles22}</style>
3082
+ <div class="group" role="group" aria-label="${label}"><slot></slot></div>
3083
+ `;
3084
+ this.addEventListener("click", this._onClick);
3085
+ this.addEventListener("keydown", this._onKey);
3086
+ this.shadowRoot.querySelector("slot")?.addEventListener("slotchange", () => this._init());
3087
+ this._init();
3088
+ }
3089
+ _getButtons() {
3090
+ const slot = this.shadowRoot.querySelector("slot");
3091
+ return slot ? slot.assignedElements().filter((el) => el.tagName === "BUTTON") : [];
3092
+ }
3093
+ _init() {
3094
+ const buttons = this._getButtons();
3095
+ const selected = buttons.find((b) => b.hasAttribute("selected")) || buttons[0];
3096
+ buttons.forEach((btn, i) => {
3097
+ btn.setAttribute("aria-pressed", btn === selected ? "true" : "false");
3098
+ btn.setAttribute("tabindex", btn === selected ? "0" : "-1");
3099
+ });
3100
+ }
3101
+ _onClick(e) {
3102
+ const btn = e.target.closest("button");
3103
+ if (!btn || !this.contains(btn)) return;
3104
+ this._select(btn);
3105
+ }
3106
+ _select(btn) {
3107
+ this._getButtons().forEach((b) => {
3108
+ b.setAttribute("aria-pressed", b === btn ? "true" : "false");
3109
+ b.setAttribute("tabindex", b === btn ? "0" : "-1");
3110
+ });
3111
+ this.dispatchEvent(new CustomEvent("velin-change", { bubbles: true, detail: { value: btn.value || btn.textContent?.trim() } }));
3112
+ }
3113
+ _onKey(e) {
3114
+ const buttons = this._getButtons();
3115
+ if (!buttons.includes(e.target)) return;
3116
+ rovingTabindex(this, buttons, e);
3117
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) {
3118
+ const focused = buttons.find((b) => b.getAttribute("tabindex") === "0");
3119
+ if (focused) this._select(focused);
3120
+ }
3121
+ }
3122
+ static get observedAttributes() {
3123
+ return ["aria-label"];
3124
+ }
3125
+ attributeChangedCallback(name) {
3126
+ if (name === "aria-label") {
3127
+ const group = this.shadowRoot?.querySelector(".group");
3128
+ if (group) group.setAttribute("aria-label", escapeHTML(this.getAttribute("aria-label") || "Segmented control"));
3129
+ }
3130
+ }
3131
+ disconnectedCallback() {
3132
+ this.removeEventListener("click", this._onClick);
3133
+ this.removeEventListener("keydown", this._onKey);
3134
+ }
3135
+ };
3136
+ customElements.define("velin-segmented-control", VelinSegmentedControl);
3137
+ var velin_segmented_control_default = VelinSegmentedControl;
3138
+
3139
+ // components/velin-rating.js
3140
+ var styles23 = `
3141
+ ${SHADOW_A11Y_STYLES}
3142
+ :host { display: inline-block; }
3143
+ .stars { display: inline-flex; gap: var(--velin-space-1, 0.25rem); }
3144
+ button {
3145
+ background: none; border: none; padding: var(--velin-space-1, 0.25rem);
3146
+ font-size: 1.5rem; line-height: 1; cursor: pointer;
3147
+ color: var(--velin-color-border, #ccc);
3148
+ }
3149
+ button[aria-checked="true"] { color: var(--velin-color-warning, #f59e0b); }
3150
+ `;
3151
+ var MAX = 5;
3152
+ var VelinRating = class extends HTMLElement {
3153
+ static get observedAttributes() {
3154
+ return ["value"];
3155
+ }
3156
+ constructor() {
3157
+ super();
3158
+ this.attachShadow({ mode: "open", delegatesFocus: true });
3159
+ this._onClick = this._onClick.bind(this);
3160
+ this._onKey = this._onKey.bind(this);
3161
+ }
3162
+ connectedCallback() {
3163
+ this.shadowRoot.innerHTML = `<style>${styles23}</style><div class="stars" role="radiogroup"></div>`;
3164
+ this._render();
3165
+ this.shadowRoot.querySelector(".stars").addEventListener("click", this._onClick);
3166
+ this.shadowRoot.querySelector(".stars").addEventListener("keydown", this._onKey);
3167
+ }
3168
+ _value() {
3169
+ const v = parseInt(this.getAttribute("value") || "0", 10);
3170
+ return Math.min(MAX, Math.max(0, Number.isNaN(v) ? 0 : v));
3171
+ }
3172
+ _render() {
3173
+ const group = this.shadowRoot.querySelector(".stars");
3174
+ const val = this._value();
3175
+ const label = escapeHTML(this.getAttribute("aria-label") || "Rating");
3176
+ group.setAttribute("aria-label", label);
3177
+ group.innerHTML = "";
3178
+ for (let i = 1; i <= MAX; i++) {
3179
+ const btn = document.createElement("button");
3180
+ btn.type = "button";
3181
+ btn.setAttribute("role", "radio");
3182
+ btn.setAttribute("aria-checked", i <= val ? "true" : "false");
3183
+ btn.setAttribute("aria-label", escapeHTML(`${i} star${i > 1 ? "s" : ""}`));
3184
+ btn.setAttribute("tabindex", i === (val || 1) ? "0" : "-1");
3185
+ btn.dataset.value = String(i);
3186
+ btn.textContent = i <= val ? "\u2605" : "\u2606";
3187
+ group.appendChild(btn);
3188
+ }
3189
+ }
3190
+ _getButtons() {
3191
+ return [...this.shadowRoot.querySelectorAll('button[role="radio"]')];
3192
+ }
3193
+ _onClick(e) {
3194
+ const btn = e.target.closest("button");
3195
+ if (!btn) return;
3196
+ this._setValue(parseInt(btn.dataset.value, 10));
3197
+ }
3198
+ _onKey(e) {
3199
+ const buttons = this._getButtons();
3200
+ if (!buttons.includes(e.target)) return;
3201
+ rovingTabindex(this, buttons, e);
3202
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) {
3203
+ const focused = buttons.find((b) => b.getAttribute("tabindex") === "0");
3204
+ if (focused) this._setValue(parseInt(focused.dataset.value, 10));
3205
+ }
3206
+ }
3207
+ _setValue(n) {
3208
+ this.setAttribute("value", String(n));
3209
+ this.dispatchEvent(new CustomEvent("velin-change", { bubbles: true, detail: { value: n } }));
3210
+ }
3211
+ attributeChangedCallback(name) {
3212
+ if (name === "value" && this.shadowRoot?.querySelector(".stars")) this._render();
3213
+ }
3214
+ };
3215
+ customElements.define("velin-rating", VelinRating);
3216
+ var velin_rating_default = VelinRating;
3217
+
3218
+ // components/velin-menubar.js
3219
+ var styles24 = `
3220
+ ${SHADOW_A11Y_STYLES}
3221
+ :host { display: block; }
3222
+ .menubar {
3223
+ display: flex;
3224
+ flex-wrap: wrap;
3225
+ gap: var(--velin-space-1, 0.25rem);
3226
+ padding: var(--velin-space-2, 0.5rem);
3227
+ background: var(--velin-color-surface-bright, #fff);
3228
+ border-bottom: 1px solid var(--velin-color-border, #ddd);
3229
+ }
3230
+ ::slotted([role="menuitem"]) {
3231
+ min-inline-size: 2.75rem;
3232
+ min-block-size: 2.75rem;
3233
+ padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
3234
+ background: none;
3235
+ border: none;
3236
+ border-radius: var(--velin-radius-sm, 0.25rem);
3237
+ cursor: pointer;
3238
+ font-size: var(--velin-text-base, 1rem);
3239
+ color: var(--velin-color-text, #111);
3240
+ }
3241
+ ::slotted([role="menuitem"]:hover) {
3242
+ background: var(--velin-color-surface-dim, #eee);
3243
+ }
3244
+ `;
3245
+ var VelinMenubar = class extends HTMLElement {
3246
+ constructor() {
3247
+ super();
3248
+ this.attachShadow({ mode: "open", delegatesFocus: true });
3249
+ this._onKey = this._onKey.bind(this);
3250
+ }
3251
+ connectedCallback() {
3252
+ const label = escapeHTML(this.getAttribute("aria-label") || "Menu bar");
3253
+ this.shadowRoot.innerHTML = `
3254
+ <style>${styles24}</style>
3255
+ <div class="menubar" role="menubar" aria-label="${label}"><slot></slot></div>
3256
+ `;
3257
+ this.addEventListener("keydown", this._onKey);
3258
+ this.shadowRoot.querySelector("slot")?.addEventListener("slotchange", () => this._init());
3259
+ this._init();
3260
+ }
3261
+ _getItems() {
3262
+ const slot = this.shadowRoot.querySelector("slot");
3263
+ return slot ? slot.assignedElements().filter((el) => !el.hasAttribute("disabled")) : [];
3264
+ }
3265
+ _init() {
3266
+ const items = this._getItems();
3267
+ items.forEach((el, i) => {
3268
+ if (!el.hasAttribute("role")) el.setAttribute("role", "menuitem");
3269
+ el.setAttribute("tabindex", i === 0 ? "0" : "-1");
3270
+ });
3271
+ }
3272
+ _onKey(e) {
3273
+ const items = this._getItems();
3274
+ if (items.includes(e.target)) rovingTabindex(this, items, e);
3275
+ }
3276
+ static get observedAttributes() {
3277
+ return ["aria-label"];
3278
+ }
3279
+ attributeChangedCallback(name) {
3280
+ if (name === "aria-label") {
3281
+ const bar = this.shadowRoot?.querySelector(".menubar");
3282
+ if (bar) bar.setAttribute("aria-label", escapeHTML(this.getAttribute("aria-label") || "Menu bar"));
3283
+ }
3284
+ }
3285
+ disconnectedCallback() {
3286
+ this.removeEventListener("keydown", this._onKey);
3287
+ }
3288
+ };
3289
+ customElements.define("velin-menubar", VelinMenubar);
3290
+ var velin_menubar_default = VelinMenubar;
3291
+
3292
+ // components/velin-command.js
3293
+ var styles25 = `
3294
+ ${SHADOW_A11Y_STYLES}
3295
+ :host { display: contents; }
3296
+ .overlay {
3297
+ position: fixed; inset: 0; z-index: var(--velin-z-modal, 500);
3298
+ display: flex; align-items: flex-start; justify-content: center;
3299
+ padding: 10vh var(--velin-space-4, 1rem) var(--velin-space-4, 1rem);
3300
+ background: var(--velin-color-overlay, rgba(0,0,0,0.4));
3301
+ opacity: 0; visibility: hidden;
3302
+ transition: opacity 150ms ease, visibility 150ms ease;
3303
+ }
3304
+ :host([open]) .overlay { opacity: 1; visibility: visible; }
3305
+ .panel {
3306
+ inline-size: min(32rem, 100%);
3307
+ background: var(--velin-color-surface-bright, #fff);
3308
+ border-radius: var(--velin-radius-lg, 0.75rem);
3309
+ box-shadow: var(--velin-shadow-xl, 0 20px 25px rgba(0,0,0,0.1));
3310
+ overflow: hidden;
3311
+ }
3312
+ .search {
3313
+ inline-size: 100%; padding: var(--velin-space-4, 1rem);
3314
+ border: none; border-bottom: 1px solid var(--velin-color-border, #ddd);
3315
+ font-size: var(--velin-text-base, 1rem);
3316
+ background: transparent;
3317
+ color: var(--velin-color-text, #111);
3318
+ }
3319
+ .results { max-block-size: 20rem; overflow-y: auto; padding: var(--velin-space-2, 0.5rem); }
3320
+ ::slotted(button) {
3321
+ display: flex; inline-size: 100%;
3322
+ padding: var(--velin-space-3, 0.75rem) var(--velin-space-4, 1rem);
3323
+ min-block-size: 2.5rem;
3324
+ border: none; background: none; text-align: start;
3325
+ cursor: pointer; font-size: var(--velin-text-base, 1rem);
3326
+ border-radius: var(--velin-radius-sm, 0.25rem);
3327
+ }
3328
+ ::slotted(button[hidden]) { display: none; }
3329
+ ::slotted(button:focus-visible) {
3330
+ background: var(--velin-color-surface-dim, #eee);
3331
+ }
3332
+ `;
3333
+ var VelinCommand = class extends HTMLElement {
3334
+ static get observedAttributes() {
3335
+ return ["open"];
3336
+ }
3337
+ constructor() {
3338
+ super();
3339
+ this.attachShadow({ mode: "open", delegatesFocus: true });
3340
+ this._prev = null;
3341
+ this._onKey = this._onKey.bind(this);
3342
+ this._onInput = this._onInput.bind(this);
3343
+ }
3344
+ connectedCallback() {
3345
+ const placeholder = escapeHTML(this.getAttribute("placeholder") || "Search commands\u2026");
3346
+ this.shadowRoot.innerHTML = `
3347
+ <style>${styles25}</style>
3348
+ <div class="overlay" part="overlay">
3349
+ <div class="panel" role="dialog" aria-modal="true" aria-label="Command palette" part="panel">
3350
+ <input class="search" type="search" autocomplete="off" placeholder="${placeholder}" aria-label="Search" part="search" />
3351
+ <div class="results" part="results"><slot></slot></div>
3352
+ </div>
3353
+ </div>
3354
+ `;
3355
+ this.shadowRoot.querySelector(".search").addEventListener("input", this._onInput);
3356
+ this.shadowRoot.querySelector("slot")?.addEventListener("slotchange", () => this._filter(""));
3357
+ this._filter("");
3358
+ }
3359
+ attributeChangedCallback(name) {
3360
+ if (name === "open") this.hasAttribute("open") ? this._open() : this._close();
3361
+ }
3362
+ open() {
3363
+ this.setAttribute("open", "");
3364
+ }
3365
+ close() {
3366
+ this.removeAttribute("open");
3367
+ this.dispatchEvent(new CustomEvent("velin-close", { bubbles: true }));
3368
+ }
3369
+ _open() {
3370
+ this._prev = saveFocus();
3371
+ setBackgroundInert(this);
3372
+ document.addEventListener("keydown", this._onKey);
3373
+ requestAnimationFrame(() => {
3374
+ this.shadowRoot.querySelector(".search")?.focus();
3375
+ this._filter("");
3376
+ });
3377
+ }
3378
+ _close() {
3379
+ document.removeEventListener("keydown", this._onKey);
3380
+ clearBackgroundInert();
3381
+ restoreFocus(this._prev);
3382
+ const input = this.shadowRoot.querySelector(".search");
3383
+ if (input) input.value = "";
3384
+ this._filter("");
3385
+ }
3386
+ _onInput(e) {
3387
+ this._filter(e.target.value);
3388
+ }
3389
+ _filter(query) {
3390
+ const q = query.trim().toLowerCase();
3391
+ const slot = this.shadowRoot.querySelector("slot");
3392
+ slot?.assignedElements().forEach((btn) => {
3393
+ const text = btn.textContent?.trim().toLowerCase() || "";
3394
+ const match = !q || text.includes(q);
3395
+ btn.hidden = !match;
3396
+ });
3397
+ }
3398
+ _onKey(e) {
3399
+ if (e.key === "Escape") {
3400
+ this.close();
3401
+ return;
3402
+ }
3403
+ trapFocus(this.shadowRoot, e);
3404
+ }
3405
+ disconnectedCallback() {
3406
+ document.removeEventListener("keydown", this._onKey);
3407
+ }
3408
+ };
3409
+ customElements.define("velin-command", VelinCommand);
3410
+ var velin_command_default = VelinCommand;
3411
+
3412
+ // components/velin-announcer.js
3413
+ var styles26 = `
3414
+ :host { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
3415
+ `;
3416
+ var VelinAnnouncer = class extends HTMLElement {
3417
+ connectedCallback() {
3418
+ if (!this.shadowRoot) this.attachShadow({ mode: "open" });
3419
+ const live = this.getAttribute("polite") === "false" ? "assertive" : "polite";
3420
+ this.shadowRoot.innerHTML = "<style>" + styles26 + '</style><div role="status" aria-live="' + live + '" aria-atomic="true" part="region"></div>';
3421
+ this._region = this.shadowRoot.querySelector('[role="status"]');
3422
+ }
3423
+ announce(message, { assertive = false } = {}) {
3424
+ if (!this._region) this.connectedCallback();
3425
+ this._region.setAttribute("aria-live", assertive ? "assertive" : "polite");
3426
+ this._region.textContent = "";
3427
+ requestAnimationFrame(() => {
3428
+ this._region.textContent = typeof message === "string" ? message : "";
3429
+ });
3430
+ }
3431
+ static announceGlobal(message, options) {
3432
+ let el = document.querySelector("velin-announcer");
3433
+ if (!el) {
3434
+ el = document.createElement("velin-announcer");
3435
+ document.body.appendChild(el);
3436
+ }
3437
+ el.announce(message, options);
3438
+ }
3439
+ };
3440
+ customElements.define("velin-announcer", VelinAnnouncer);
3441
+ var velin_announcer_default = VelinAnnouncer;
3442
+
3443
+ // components/velin-sparkline.js
3444
+ var NS = "http://www.w3.org/2000/svg";
3445
+ function parseValues(raw) {
3446
+ if (!raw) return [];
3447
+ const trimmed = String(raw).trim();
3448
+ if (trimmed.startsWith("[")) {
3449
+ try {
3450
+ const parsed = JSON.parse(trimmed);
3451
+ return Array.isArray(parsed) ? parsed.map(Number).filter((n) => Number.isFinite(n)) : [];
3452
+ } catch {
3453
+ return [];
3454
+ }
3455
+ }
3456
+ return trimmed.split(/[\s,]+/).map((s) => Number.parseFloat(s)).filter((n) => Number.isFinite(n));
3457
+ }
3458
+ function buildPoints(values, w, h, min, max) {
3459
+ const n = values.length;
3460
+ if (n === 0) return [];
3461
+ if (n === 1) {
3462
+ return [[0, h / 2], [w, h / 2]];
3463
+ }
3464
+ const range = Math.max(max - min, 1e-6);
3465
+ const stepX = w / (n - 1);
3466
+ return values.map((v, i) => {
3467
+ const x = i * stepX;
3468
+ const y = h - (v - min) / range * h;
3469
+ return [x, y];
3470
+ });
3471
+ }
3472
+ function pointsToPath(points) {
3473
+ if (!points.length) return "";
3474
+ return points.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}`).join(" ");
3475
+ }
3476
+ function pointsToArea(points, h) {
3477
+ if (!points.length) return "";
3478
+ const line = pointsToPath(points);
3479
+ const last = points[points.length - 1][0];
3480
+ const first = points[0][0];
3481
+ return `${line} L${last.toFixed(2)},${h} L${first.toFixed(2)},${h} Z`;
3482
+ }
3483
+ var VelinSparkline = class extends HTMLElement {
3484
+ static get observedAttributes() {
3485
+ return ["values", "width", "height", "min", "max", "area", "glow", "animate", "label"];
3486
+ }
3487
+ constructor() {
3488
+ super();
3489
+ this._values = [];
3490
+ this._gradientId = `velin-spark-grad-${Math.random().toString(36).slice(2, 8)}`;
3491
+ }
3492
+ connectedCallback() {
3493
+ this._render();
3494
+ }
3495
+ attributeChangedCallback() {
3496
+ if (this.isConnected) this._render();
3497
+ }
3498
+ get values() {
3499
+ return this._values.slice();
3500
+ }
3501
+ set values(arr) {
3502
+ if (!Array.isArray(arr)) return;
3503
+ this._values = arr.filter((n) => Number.isFinite(Number(n))).map(Number);
3504
+ this.setAttribute("values", this._values.join(","));
3505
+ }
3506
+ update(values) {
3507
+ if (!Array.isArray(values)) return;
3508
+ this._values = values.filter((n) => Number.isFinite(Number(n))).map(Number);
3509
+ this._render({ tick: true });
3510
+ }
3511
+ _render({ tick = false } = {}) {
3512
+ const w = Number.parseFloat(this.getAttribute("width")) || 320;
3513
+ const h = Number.parseFloat(this.getAttribute("height")) || 96;
3514
+ const values = this._values.length ? this._values : parseValues(this.getAttribute("values"));
3515
+ this._values = values;
3516
+ if (!values.length) {
3517
+ this.innerHTML = "";
3518
+ return;
3519
+ }
3520
+ const minAttr = Number.parseFloat(this.getAttribute("min"));
3521
+ const maxAttr = Number.parseFloat(this.getAttribute("max"));
3522
+ const min = Number.isFinite(minAttr) ? minAttr : Math.min(...values);
3523
+ const max = Number.isFinite(maxAttr) ? maxAttr : Math.max(...values);
3524
+ const wantsArea = this.hasAttribute("area") && this.getAttribute("area") !== "false";
3525
+ const wantsGlow = this.hasAttribute("glow") && this.getAttribute("glow") !== "false";
3526
+ const animate = (this.getAttribute("animate") || "draw").toLowerCase();
3527
+ const label = this.getAttribute("label");
3528
+ const points = buildPoints(values, w, h, min, max);
3529
+ const linePath = pointsToPath(points);
3530
+ const areaPath = wantsArea ? pointsToArea(points, h) : "";
3531
+ this.innerHTML = "";
3532
+ const svg = document.createElementNS(NS, "svg");
3533
+ svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
3534
+ svg.setAttribute("preserveAspectRatio", "none");
3535
+ svg.style.display = "block";
3536
+ svg.style.width = "100%";
3537
+ svg.style.height = "100%";
3538
+ if (label) {
3539
+ svg.setAttribute("role", "img");
3540
+ svg.setAttribute("aria-label", label);
3541
+ } else {
3542
+ svg.setAttribute("aria-hidden", "true");
3543
+ }
3544
+ if (wantsArea) {
3545
+ const defs = document.createElementNS(NS, "defs");
3546
+ const grad = document.createElementNS(NS, "linearGradient");
3547
+ grad.setAttribute("id", this._gradientId);
3548
+ grad.setAttribute("x1", "0");
3549
+ grad.setAttribute("x2", "0");
3550
+ grad.setAttribute("y1", "0");
3551
+ grad.setAttribute("y2", "1");
3552
+ const stops = [
3553
+ ["0%", "currentColor", "0.35"],
3554
+ ["100%", "currentColor", "0"]
3555
+ ];
3556
+ stops.forEach(([offset, color, op]) => {
3557
+ const stop = document.createElementNS(NS, "stop");
3558
+ stop.setAttribute("offset", offset);
3559
+ stop.setAttribute("stop-color", color);
3560
+ stop.setAttribute("stop-opacity", op);
3561
+ grad.appendChild(stop);
3562
+ });
3563
+ defs.appendChild(grad);
3564
+ svg.appendChild(defs);
3565
+ const area = document.createElementNS(NS, "path");
3566
+ area.setAttribute("d", areaPath);
3567
+ area.setAttribute("fill", `url(#${this._gradientId})`);
3568
+ area.setAttribute("stroke", "none");
3569
+ area.classList.add("velin-chart-area");
3570
+ svg.appendChild(area);
3571
+ }
3572
+ const line = document.createElementNS(NS, "path");
3573
+ line.setAttribute("d", linePath);
3574
+ line.setAttribute("fill", "none");
3575
+ line.setAttribute("stroke", "currentColor");
3576
+ line.setAttribute("stroke-width", "2");
3577
+ line.setAttribute("stroke-linecap", "round");
3578
+ line.setAttribute("stroke-linejoin", "round");
3579
+ line.setAttribute("vector-effect", "non-scaling-stroke");
3580
+ svg.appendChild(line);
3581
+ if (wantsGlow) svg.classList.add("velin-chart-glow");
3582
+ this.appendChild(svg);
3583
+ if (animate !== "none") {
3584
+ const len = typeof line.getTotalLength === "function" && line.getTotalLength() || w;
3585
+ line.style.setProperty("--velin-chart-len", len.toFixed(2));
3586
+ line.classList.add("velin-chart-line");
3587
+ } else {
3588
+ line.style.strokeDasharray = "";
3589
+ line.style.strokeDashoffset = "";
3590
+ }
3591
+ if (tick) {
3592
+ this.classList.remove("velin-spark-tick");
3593
+ void this.offsetWidth;
3594
+ this.classList.add("velin-spark-tick");
3595
+ }
3596
+ }
3597
+ };
3598
+ if (typeof customElements !== "undefined" && !customElements.get("velin-sparkline")) {
3599
+ customElements.define("velin-sparkline", VelinSparkline);
3600
+ }
3601
+ var velin_sparkline_default = VelinSparkline;
3602
+
3603
+ // components/velin-counter.js
3604
+ var easeOutExpo = (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
3605
+ function buildFormatter(host) {
3606
+ const format = (host.getAttribute("format") || "number").toLowerCase();
3607
+ const locale = host.getAttribute("locale") || void 0;
3608
+ const decimalsAttr = host.getAttribute("decimals");
3609
+ const decimals = decimalsAttr != null ? Math.max(0, Number.parseInt(decimalsAttr, 10) || 0) : null;
3610
+ const opts = {};
3611
+ if (decimals != null) {
3612
+ opts.minimumFractionDigits = decimals;
3613
+ opts.maximumFractionDigits = decimals;
3614
+ }
3615
+ if (format === "currency") {
3616
+ opts.style = "currency";
3617
+ opts.currency = host.getAttribute("currency") || "EUR";
3618
+ } else if (format === "percent") {
3619
+ opts.style = "percent";
3620
+ }
3621
+ try {
3622
+ return new Intl.NumberFormat(locale, opts);
3623
+ } catch {
3624
+ return new Intl.NumberFormat(void 0, opts);
3625
+ }
3626
+ }
3627
+ var VelinCounter = class extends HTMLElement {
3628
+ static get observedAttributes() {
3629
+ return ["from", "to", "duration", "decimals", "prefix", "suffix", "format", "currency", "locale"];
3630
+ }
3631
+ constructor() {
3632
+ super();
3633
+ this._rafId = 0;
3634
+ this._started = false;
3635
+ this._observer = null;
3636
+ }
3637
+ connectedCallback() {
3638
+ this._render(this._fromValue());
3639
+ if (this.getAttribute("autostart") === "false") return;
3640
+ this._scheduleStart();
3641
+ }
3642
+ disconnectedCallback() {
3643
+ cancelAnimationFrame(this._rafId);
3644
+ this._observer?.disconnect();
3645
+ }
3646
+ attributeChangedCallback(name) {
3647
+ if (!this.isConnected) return;
3648
+ if (name === "to" || name === "from") {
3649
+ this.start();
3650
+ } else {
3651
+ this._render(this._lastValue ?? this._toValue());
3652
+ }
3653
+ }
3654
+ _fromValue() {
3655
+ return Number.parseFloat(this.getAttribute("from")) || 0;
3656
+ }
3657
+ _toValue() {
3658
+ return Number.parseFloat(this.getAttribute("to")) || 0;
3659
+ }
3660
+ _duration() {
3661
+ return Math.max(0, Number.parseFloat(this.getAttribute("duration")) || 900);
3662
+ }
3663
+ _scheduleStart() {
3664
+ if (this._started) return;
3665
+ if (typeof IntersectionObserver === "undefined") {
3666
+ this.start();
3667
+ return;
3668
+ }
3669
+ this._observer = new IntersectionObserver((entries) => {
3670
+ for (const entry of entries) {
3671
+ if (entry.isIntersecting) {
3672
+ this.start();
3673
+ this._observer.disconnect();
3674
+ this._observer = null;
3675
+ break;
3676
+ }
3677
+ }
3678
+ }, { threshold: 0.2 });
3679
+ this._observer.observe(this);
3680
+ }
3681
+ start() {
3682
+ cancelAnimationFrame(this._rafId);
3683
+ this._started = true;
3684
+ const from = this._fromValue();
3685
+ const to = this._toValue();
3686
+ const duration = this._duration();
3687
+ const reduced = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3688
+ if (reduced || duration === 0) {
3689
+ this._render(to);
3690
+ return;
3691
+ }
3692
+ const start = performance.now();
3693
+ const tick = (now) => {
3694
+ const t = Math.min(1, (now - start) / duration);
3695
+ const value = from + (to - from) * easeOutExpo(t);
3696
+ this._render(value);
3697
+ if (t < 1) this._rafId = requestAnimationFrame(tick);
3698
+ };
3699
+ this._rafId = requestAnimationFrame(tick);
3700
+ }
3701
+ reset() {
3702
+ cancelAnimationFrame(this._rafId);
3703
+ this._started = false;
3704
+ this._render(this._fromValue());
3705
+ }
3706
+ _render(value) {
3707
+ this._lastValue = value;
3708
+ const formatter = buildFormatter(this);
3709
+ const prefix = this.getAttribute("prefix") || "";
3710
+ const suffix = this.getAttribute("suffix") || "";
3711
+ this.textContent = `${prefix}${formatter.format(value)}${suffix}`;
3712
+ }
3713
+ };
3714
+ if (typeof customElements !== "undefined" && !customElements.get("velin-counter")) {
3715
+ customElements.define("velin-counter", VelinCounter);
3716
+ }
3717
+ var velin_counter_default = VelinCounter;
3718
+
3719
+ // components/velin-live-dot.js
3720
+ var STATUS_COLORS = {
3721
+ live: "var(--velin-color-success, oklch(60% 0.16 145))",
3722
+ paused: "var(--velin-color-text-muted, oklch(60% 0.02 240))",
3723
+ warning: "var(--velin-color-warning, oklch(75% 0.16 80))",
3724
+ error: "var(--velin-color-danger, oklch(60% 0.2 25))",
3725
+ muted: "var(--velin-color-border, oklch(85% 0.01 240))"
3726
+ };
3727
+ var styles27 = `
3728
+ :host {
3729
+ display: inline-flex;
3730
+ align-items: center;
3731
+ gap: var(--velin-space-2, 0.5rem);
3732
+ font-size: inherit;
3733
+ color: inherit;
3734
+ line-height: 1.2;
3735
+ }
3736
+ .dot {
3737
+ inline-size: 0.55rem;
3738
+ block-size: 0.55rem;
3739
+ border-radius: 50%;
3740
+ background: var(--velin-live-color);
3741
+ flex-shrink: 0;
3742
+ }
3743
+ :host([pulse="false"]) .dot { animation: none; }
3744
+ :host(:not([pulse="false"])) .dot { animation: velin-live-pulse 1.8s var(--velin-ease-out, ease-out) infinite; }
3745
+ @media (prefers-reduced-motion: reduce) {
3746
+ .dot { animation: none !important; }
3747
+ }
3748
+ `;
3749
+ var KEYFRAMES_FALLBACK = `
3750
+ @keyframes velin-live-pulse {
3751
+ 0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--velin-live-color) 65%, transparent); }
3752
+ 70% { box-shadow: 0 0 0 0.6rem color-mix(in oklch, var(--velin-live-color) 0%, transparent); }
3753
+ 100% { box-shadow: 0 0 0 0 transparent; }
3754
+ }`;
3755
+ var VelinLiveDot = class extends HTMLElement {
3756
+ static get observedAttributes() {
3757
+ return ["status", "pulse"];
3758
+ }
3759
+ constructor() {
3760
+ super();
3761
+ this.attachShadow({ mode: "open" });
3762
+ }
3763
+ connectedCallback() {
3764
+ this._render();
3765
+ }
3766
+ attributeChangedCallback() {
3767
+ if (this.shadowRoot) this._render();
3768
+ }
3769
+ _render() {
3770
+ const status = this.getAttribute("status") || "live";
3771
+ const color = STATUS_COLORS[status] || STATUS_COLORS.live;
3772
+ this.style.setProperty("--velin-live-color", color);
3773
+ this.shadowRoot.innerHTML = `
3774
+ <style>${styles27}${KEYFRAMES_FALLBACK}</style>
3775
+ <span class="dot" aria-hidden="true"></span><slot></slot>
3776
+ `;
3777
+ }
3778
+ };
3779
+ if (typeof customElements !== "undefined" && !customElements.get("velin-live-dot")) {
3780
+ customElements.define("velin-live-dot", VelinLiveDot);
3781
+ }
3782
+ var velin_live_dot_default = VelinLiveDot;
3783
+
3784
+ // components/velin-reveal.js
3785
+ var DEFAULTS = {
3786
+ selector: ".velin-animate-on-scroll",
3787
+ threshold: 0.1,
3788
+ rootMargin: "0px 0px -40px 0px",
3789
+ once: true,
3790
+ visibleClass: "is-visible"
3791
+ };
3792
+ var _activeObservers = /* @__PURE__ */ new WeakMap();
3793
+ function initReveal(options = {}) {
3794
+ if (typeof document === "undefined") return () => {
3795
+ };
3796
+ const opts = { ...DEFAULTS, ...options };
3797
+ const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3798
+ const targets = Array.from(document.querySelectorAll(opts.selector));
3799
+ if (reduced || typeof IntersectionObserver === "undefined") {
3800
+ targets.forEach((el) => el.classList.add(opts.visibleClass));
3801
+ return () => {
3802
+ };
3803
+ }
3804
+ const observer = new IntersectionObserver(
3805
+ (entries) => {
3806
+ for (const entry of entries) {
3807
+ if (!entry.isIntersecting) continue;
3808
+ entry.target.classList.add(opts.visibleClass);
3809
+ if (opts.once) observer.unobserve(entry.target);
3810
+ }
3811
+ },
3812
+ { threshold: opts.threshold, rootMargin: opts.rootMargin }
3813
+ );
3814
+ targets.forEach((el) => {
3815
+ if (_activeObservers.has(el)) return;
3816
+ _activeObservers.set(el, observer);
3817
+ observer.observe(el);
3818
+ });
3819
+ return () => {
3820
+ observer.disconnect();
3821
+ targets.forEach((el) => _activeObservers.delete(el));
3822
+ };
3823
+ }
3824
+ if (typeof document !== "undefined") {
3825
+ const autoInit2 = () => {
3826
+ if (document.documentElement && document.documentElement.hasAttribute("data-velin-reveal-auto")) {
3827
+ initReveal();
3828
+ }
3829
+ };
3830
+ if (document.readyState === "loading") {
3831
+ document.addEventListener("DOMContentLoaded", autoInit2, { once: true });
3832
+ } else {
3833
+ autoInit2();
3834
+ }
3835
+ }
3836
+
3837
+ // components/velin-flip.js
3838
+ var REDUCED_MOTION_MQ = typeof window !== "undefined" && window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
3839
+ var DEFAULTS2 = {
3840
+ duration: 250,
3841
+ easing: "var(--velin-ease-expo-out, cubic-bezier(0.16, 1, 0.3, 1))",
3842
+ itemSelector: ":scope > *"
3843
+ };
3844
+ function getItems(container, selector) {
3845
+ return Array.from(container.querySelectorAll(selector));
3846
+ }
3847
+ function flipReorder(container, mutateFn, options = {}) {
3848
+ if (!container || typeof mutateFn !== "function") return;
3849
+ const opts = { ...DEFAULTS2, ...options };
3850
+ const reduced = REDUCED_MOTION_MQ && REDUCED_MOTION_MQ.matches;
3851
+ const items = getItems(container, opts.itemSelector);
3852
+ const before = /* @__PURE__ */ new Map();
3853
+ items.forEach((el) => {
3854
+ if (!el.hidden) before.set(el, el.getBoundingClientRect());
3855
+ });
3856
+ mutateFn();
3857
+ if (reduced) return;
3858
+ const items2 = getItems(container, opts.itemSelector);
3859
+ items2.forEach((el) => {
3860
+ if (el.hidden) return;
3861
+ const prev = before.get(el);
3862
+ const next = el.getBoundingClientRect();
3863
+ if (!prev) {
3864
+ if (typeof el.animate !== "function") return;
3865
+ el.animate(
3866
+ [
3867
+ { opacity: 0, transform: "scale(0.96)" },
3868
+ { opacity: 1, transform: "scale(1)" }
3869
+ ],
3870
+ { duration: opts.duration, easing: opts.easing, fill: "both" }
3871
+ );
3872
+ return;
3873
+ }
3874
+ const dx = prev.left - next.left;
3875
+ const dy = prev.top - next.top;
3876
+ if (dx === 0 && dy === 0) return;
3877
+ if (typeof el.animate !== "function") return;
3878
+ el.animate(
3879
+ [
3880
+ { transform: `translate(${dx}px, ${dy}px)` },
3881
+ { transform: "translate(0, 0)" }
3882
+ ],
3883
+ { duration: opts.duration, easing: opts.easing, fill: "both" }
3884
+ );
3885
+ });
3886
+ }
3887
+ function filterList(container, predicate, options = {}) {
3888
+ if (!container || typeof predicate !== "function") return;
3889
+ const opts = { ...DEFAULTS2, ...options };
3890
+ flipReorder(
3891
+ container,
3892
+ () => {
3893
+ getItems(container, opts.itemSelector).forEach((el) => {
3894
+ el.hidden = !predicate(el);
3895
+ });
3896
+ },
3897
+ opts
3898
+ );
3899
+ }
3900
+ function readTokens(value) {
3901
+ if (!value) return [];
3902
+ return String(value).toLowerCase().split(/[\s,|]+/).map((s) => s.trim()).filter(Boolean);
3903
+ }
3904
+ function matchTokens(itemTokens, queryTokens, mode) {
3905
+ if (!queryTokens.length) return true;
3906
+ if (mode === "all") return queryTokens.every((q) => itemTokens.includes(q));
3907
+ return queryTokens.some((q) => itemTokens.includes(q));
3908
+ }
3909
+ function matchSearch(item, query) {
3910
+ if (!query) return true;
3911
+ const haystack = (item.getAttribute("data-tags") || "") + " " + (item.getAttribute("data-search") || "") + " " + (item.textContent || "");
3912
+ return haystack.toLowerCase().includes(query.toLowerCase());
3913
+ }
3914
+ var FilterController = class {
3915
+ constructor(container) {
3916
+ this.container = container;
3917
+ this.tag = "";
3918
+ this.search = "";
3919
+ this.matchMode = container.getAttribute("data-velin-filter-mode") === "all" ? "all" : "any";
3920
+ this.itemSelector = container.getAttribute("data-velin-filter-item") || ":scope > *";
3921
+ }
3922
+ apply() {
3923
+ const queryTokens = readTokens(this.tag);
3924
+ const term = this.search;
3925
+ filterList(
3926
+ this.container,
3927
+ (el) => {
3928
+ const tokens = readTokens(el.getAttribute("data-tags"));
3929
+ return matchTokens(tokens, queryTokens, this.matchMode) && matchSearch(el, term);
3930
+ },
3931
+ { itemSelector: this.itemSelector }
3932
+ );
3933
+ }
3934
+ };
3935
+ var _controllers = /* @__PURE__ */ new WeakMap();
3936
+ function getController(container) {
3937
+ let ctrl = _controllers.get(container);
3938
+ if (!ctrl) {
3939
+ ctrl = new FilterController(container);
3940
+ _controllers.set(container, ctrl);
3941
+ }
3942
+ return ctrl;
3943
+ }
3944
+ function resolveTarget(triggerEl) {
3945
+ const sel = triggerEl.getAttribute("data-velin-filter-target");
3946
+ if (!sel) return null;
3947
+ try {
3948
+ return document.querySelector(sel);
3949
+ } catch {
3950
+ return null;
3951
+ }
3952
+ }
3953
+ function highlightActive(group, active) {
3954
+ if (!group) return;
3955
+ group.querySelectorAll("[data-velin-filter-value]").forEach((btn) => {
3956
+ if (btn === active) btn.setAttribute("data-velin-filter-active", "");
3957
+ else btn.removeAttribute("data-velin-filter-active");
3958
+ });
3959
+ }
3960
+ function autoInit() {
3961
+ if (typeof document === "undefined") return;
3962
+ document.addEventListener("click", (event) => {
3963
+ const target = event.target.closest("[data-velin-filter-value]");
3964
+ if (!target) return;
3965
+ const container = resolveTarget(target);
3966
+ if (!container) return;
3967
+ const group = target.closest("[data-velin-filter-group]") || target.parentElement;
3968
+ highlightActive(group, target);
3969
+ const ctrl = getController(container);
3970
+ ctrl.tag = target.getAttribute("data-velin-filter-value") || "";
3971
+ if (ctrl.tag.toLowerCase() === "all" || ctrl.tag === "*") ctrl.tag = "";
3972
+ ctrl.apply();
3973
+ });
3974
+ const handleInput = (event) => {
3975
+ const input = event.target.closest("[data-velin-filter-input]");
3976
+ if (!input) return;
3977
+ const container = resolveTarget(input);
3978
+ if (!container) return;
3979
+ const ctrl = getController(container);
3980
+ const raw = input.value || (typeof input.getAttribute === "function" ? input.getAttribute("value") : "");
3981
+ ctrl.search = (raw || "").trim();
3982
+ ctrl.apply();
3983
+ };
3984
+ document.addEventListener("input", handleInput);
3985
+ document.addEventListener("change", handleInput);
3986
+ }
3987
+ if (typeof document !== "undefined") {
3988
+ if (document.readyState === "loading") {
3989
+ document.addEventListener("DOMContentLoaded", autoInit, { once: true });
3990
+ } else {
3991
+ autoInit();
3992
+ }
3993
+ }
3994
+
2266
3995
  // components/velin-haptic.js
2267
3996
  var PATTERNS = {
2268
3997
  tap: [10],
@@ -2320,32 +4049,54 @@ if (typeof document !== "undefined") {
2320
4049
  export {
2321
4050
  PATTERNS as HapticPatterns,
2322
4051
  velin_accordion_default as VelinAccordion,
4052
+ velin_announcer_default as VelinAnnouncer,
4053
+ velin_bottom_nav_default as VelinBottomNav,
2323
4054
  velin_carousel_default as VelinCarousel,
2324
4055
  velin_collapse_default as VelinCollapse,
4056
+ velin_combobox_default as VelinCombobox,
4057
+ velin_command_default as VelinCommand,
2325
4058
  velin_copy_default as VelinCopy,
2326
4059
  velin_countdown_default as VelinCountdown,
4060
+ velin_counter_default as VelinCounter,
2327
4061
  velin_dialog_default as VelinDialog,
2328
4062
  velin_drawer_default as VelinDrawer,
2329
4063
  velin_dropdown_default as VelinDropdown,
2330
4064
  VelinHapticObserver,
2331
4065
  velin_icon_default as VelinIcon,
2332
4066
  velin_lightbox_default as VelinLightbox,
4067
+ velin_live_dot_default as VelinLiveDot,
4068
+ velin_menubar_default as VelinMenubar,
2333
4069
  velin_modal_default as VelinModal,
2334
4070
  velin_persist_default as VelinPersist,
2335
4071
  velin_popover_default as VelinPopover,
2336
4072
  velin_progress_ring_default as VelinProgressRing,
4073
+ velin_rating_default as VelinRating,
2337
4074
  velin_scroll_top_default as VelinScrollTop,
2338
4075
  velin_scrollspy_default as VelinScrollspy,
4076
+ velin_segmented_control_default as VelinSegmentedControl,
4077
+ velin_sheet_default as VelinSheet,
4078
+ velin_sparkline_default as VelinSparkline,
2339
4079
  velin_stepper_wc_default as VelinStepperWC,
2340
4080
  velin_tabs_default as VelinTabs,
2341
4081
  velin_theme_toggle_default as VelinThemeToggle,
2342
4082
  velin_toast_default as VelinToast,
2343
4083
  velin_tooltip_wc_default as VelinTooltipWC,
2344
4084
  applyHaptic,
4085
+ clearBackgroundInert,
4086
+ createSafeHTML,
4087
+ escapeHTML,
4088
+ escapeHTMLAttribute,
4089
+ filterList,
4090
+ flipReorder,
2345
4091
  getFocusableElements,
4092
+ getTrustedPolicy,
4093
+ initReveal,
2346
4094
  restoreFocus,
2347
4095
  rovingTabindex,
4096
+ sanitizeURL,
2348
4097
  saveFocus,
4098
+ setBackgroundInert,
4099
+ stripControlChars,
2349
4100
  trapFocus,
2350
4101
  vibrate
2351
4102
  };