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