@birdapi/velinstyle 0.8.0 → 0.9.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 (313) hide show
  1. package/README.de.md +135 -246
  2. package/README.md +134 -245
  3. package/cli/blueprints/onboarding.html +2 -2
  4. package/cli/cli-manifest.json +97 -0
  5. package/cli/docgen/extract-a11y.js +89 -0
  6. package/cli/docgen/extract-attributes.js +95 -0
  7. package/cli/docgen/extract-cli.js +50 -0
  8. package/cli/docgen/extract-components.js +175 -0
  9. package/cli/docgen/extract-meta.js +32 -0
  10. package/cli/docgen/extract-rules.js +76 -0
  11. package/cli/docgen/extract-tokens.js +79 -0
  12. package/cli/docgen/extract-utilities.js +75 -0
  13. package/cli/docgen/markdown.js +43 -0
  14. package/cli/docs-generate.js +196 -0
  15. package/cli/index.js +121 -5
  16. package/cli/meta.js +97 -0
  17. package/cli/perf-audit.js +174 -0
  18. package/cli/pii-scanner.js +134 -0
  19. package/cli/scanner-rules-data.js +62 -0
  20. package/cli/scanner.js +68 -1
  21. package/cli/search-index.js +163 -0
  22. package/cli/tokens-build.js +21 -1
  23. package/cli/tokens-validate.js +69 -0
  24. package/components/a11y-entry.js +42 -0
  25. package/components/a11y-utils.js +71 -0
  26. package/components/focus-manager.js +45 -8
  27. package/components/index.js +22 -12
  28. package/components/runtime/component-loaders.js +43 -0
  29. package/components/runtime/index.js +77 -0
  30. package/components/runtime-entry.js +10 -0
  31. package/components/sanitize.js +135 -54
  32. package/components/velin-accordion.js +12 -0
  33. package/components/velin-bottom-nav.js +19 -0
  34. package/components/velin-carousel.js +2 -2
  35. package/components/velin-code-block.js +142 -0
  36. package/components/velin-command.js +15 -0
  37. package/components/velin-copy.js +18 -5
  38. package/components/velin-counter.js +3 -0
  39. package/components/velin-dialog.js +9 -1
  40. package/components/velin-email.js +94 -0
  41. package/components/velin-icon.js +65 -22
  42. package/components/velin-lightbox.js +8 -3
  43. package/components/velin-live-dot.js +5 -0
  44. package/components/velin-modal.js +6 -3
  45. package/components/velin-popover.js +34 -2
  46. package/components/velin-reveal.js +7 -65
  47. package/components/velin-scroll-top.js +1 -1
  48. package/components/velin-search.js +267 -0
  49. package/components/velin-secure-field.js +99 -0
  50. package/components/velin-sparkline.js +27 -7
  51. package/components/{velin-stepper-wc.js → velin-stepper.js} +37 -8
  52. package/components/velin-theme-toggle.js +12 -5
  53. package/components/velin-toast.js +4 -0
  54. package/components/{velin-tooltip-wc.js → velin-tooltip.js} +22 -3
  55. package/core/a11y/component-contracts.json +221 -0
  56. package/core/attributes/index.js +6 -0
  57. package/core/attributes/registry.js +268 -0
  58. package/core/highlight/highlight.js +114 -0
  59. package/core/highlight/index.js +29 -0
  60. package/core/highlight/languages/_utils.js +52 -0
  61. package/core/highlight/languages/blade.js +16 -0
  62. package/core/highlight/languages/css.js +26 -0
  63. package/core/highlight/languages/html.js +17 -0
  64. package/core/highlight/languages/js.js +58 -0
  65. package/core/highlight/languages/json.js +14 -0
  66. package/core/highlight/languages/markdown.js +16 -0
  67. package/core/highlight/languages/php.js +39 -0
  68. package/core/highlight/languages/plain.js +7 -0
  69. package/core/highlight/languages/shell.js +33 -0
  70. package/core/highlight/languages/sql.js +20 -0
  71. package/core/highlight/languages/typescript.js +28 -0
  72. package/core/highlight/observe.js +53 -0
  73. package/core/highlight/registry.js +107 -0
  74. package/core/highlight/render.js +35 -0
  75. package/core/highlight/types.js +28 -0
  76. package/core/meta/build.js +293 -0
  77. package/core/meta/index.js +17 -0
  78. package/core/meta/schema.js +49 -0
  79. package/core/motion/effects.js +54 -0
  80. package/core/motion/index.js +76 -0
  81. package/core/motion/scheduler.js +72 -0
  82. package/core/motion/scroll.js +32 -0
  83. package/core/motion/stagger.js +24 -0
  84. package/core/search/docs-url.js +96 -0
  85. package/core/search/engine.js +139 -0
  86. package/core/search/highlight.js +48 -0
  87. package/core/search/index.js +126 -0
  88. package/core/search/providers.js +36 -0
  89. package/core/search/types.js +61 -0
  90. package/core/search/worker-client.js +56 -0
  91. package/core/search/worker.js +20 -0
  92. package/dist/chunks/a11y-utils-IRC7LOPF.js +16 -0
  93. package/dist/chunks/attributes-3BZLI7XF.js +364 -0
  94. package/dist/chunks/attributes-4N5JP4CG.js +364 -0
  95. package/dist/chunks/attributes-5Q2WXGRC.js +374 -0
  96. package/dist/chunks/attributes-AIR7SVLK.js +364 -0
  97. package/dist/chunks/attributes-BGRN5XZO.js +364 -0
  98. package/dist/chunks/attributes-BVSQAYLR.js +364 -0
  99. package/dist/chunks/attributes-CDYFUY7Y.js +364 -0
  100. package/dist/chunks/attributes-JMYJZBPE.js +364 -0
  101. package/dist/chunks/attributes-R5JY3X4O.js +374 -0
  102. package/dist/chunks/attributes-STYCS3O5.js +391 -0
  103. package/dist/chunks/attributes-TNSPQTHX.js +374 -0
  104. package/dist/chunks/attributes-TPANLMLF.js +391 -0
  105. package/dist/chunks/blade-LIQLVKZQ.js +19 -0
  106. package/dist/chunks/browser-MRKYZPEH.js +1149 -0
  107. package/dist/chunks/chunk-2354NEBP.js +109 -0
  108. package/dist/chunks/chunk-2VNA7G7S.js +109 -0
  109. package/dist/chunks/chunk-42VVPW6N.js +132 -0
  110. package/dist/chunks/chunk-7FFAMRFV.js +44 -0
  111. package/dist/chunks/chunk-7MCGPHVC.js +69 -0
  112. package/dist/chunks/chunk-AQLRJSGG.js +109 -0
  113. package/dist/chunks/chunk-EWPSDMQT.js +109 -0
  114. package/dist/chunks/chunk-F7OLHTGX.js +22 -0
  115. package/dist/chunks/chunk-J472ERN2.js +242 -0
  116. package/dist/chunks/chunk-LA7264K4.js +85 -0
  117. package/dist/chunks/chunk-M5CHQVP6.js +61 -0
  118. package/dist/chunks/chunk-MKZP2TBH.js +89 -0
  119. package/dist/chunks/chunk-QTJJ3DPO.js +109 -0
  120. package/dist/chunks/chunk-RDE3QE7Z.js +89 -0
  121. package/dist/chunks/chunk-RSMRR27R.js +109 -0
  122. package/dist/chunks/chunk-RZQVWFHW.js +109 -0
  123. package/dist/chunks/chunk-SRL6EWHP.js +109 -0
  124. package/dist/chunks/chunk-U4XHMZ7D.js +109 -0
  125. package/dist/chunks/chunk-V5YIUA6G.js +109 -0
  126. package/dist/chunks/chunk-XIET2VSZ.js +109 -0
  127. package/dist/chunks/chunk-XYLJ2GCB.js +205 -0
  128. package/dist/chunks/chunk-Z4JYQ2XX.js +51 -0
  129. package/dist/chunks/chunk-ZPSPKUYD.js +109 -0
  130. package/dist/chunks/css-MQ5YU77P.js +28 -0
  131. package/dist/chunks/highlight-GJHHV4KM.js +37 -0
  132. package/dist/chunks/highlight-XTHRKBA3.js +37 -0
  133. package/dist/chunks/html-ZODC4DLQ.js +20 -0
  134. package/dist/chunks/js-MNSNYTJU.js +7 -0
  135. package/dist/chunks/json-VEM2HFH3.js +17 -0
  136. package/dist/chunks/markdown-E7OPGS3S.js +19 -0
  137. package/dist/chunks/php-MLL7T3LT.js +41 -0
  138. package/dist/chunks/plain-LJJDC5TN.js +8 -0
  139. package/dist/chunks/runtime-entry.js +14 -0
  140. package/dist/chunks/shell-QTRK4U4E.js +35 -0
  141. package/dist/chunks/sql-PPWLHGJ2.js +23 -0
  142. package/dist/chunks/typescript-JT3CCYWL.js +30 -0
  143. package/dist/chunks/velin-accordion-FBIPAEXG.js +105 -0
  144. package/dist/chunks/velin-accordion-QAQO7BI7.js +117 -0
  145. package/dist/chunks/velin-announcer-PVTXXCWR.js +33 -0
  146. package/dist/chunks/velin-bottom-nav-2N2TYWOF.js +86 -0
  147. package/dist/chunks/velin-bottom-nav-7GEMPIM6.js +86 -0
  148. package/dist/chunks/velin-bottom-nav-CTU6FFZV.js +101 -0
  149. package/dist/chunks/velin-bottom-nav-N3DFQR66.js +86 -0
  150. package/dist/chunks/velin-carousel-Y5VCOABZ.js +213 -0
  151. package/dist/chunks/velin-carousel-ZTBANFQQ.js +213 -0
  152. package/dist/chunks/velin-code-block-ASUBTAQV.js +119 -0
  153. package/dist/chunks/velin-code-block-CIAURDRJ.js +132 -0
  154. package/dist/chunks/velin-code-block-TSUKPY4C.js +123 -0
  155. package/dist/chunks/velin-code-block-Y6CM726Q.js +119 -0
  156. package/dist/chunks/velin-collapse-5XYBUIID.js +80 -0
  157. package/dist/chunks/velin-combobox-2KDTCQ7N.js +155 -0
  158. package/dist/chunks/velin-combobox-NXS4QB4R.js +155 -0
  159. package/dist/chunks/velin-combobox-UGA56HP3.js +155 -0
  160. package/dist/chunks/velin-command-CKDNO4BW.js +148 -0
  161. package/dist/chunks/velin-command-M7RNHUPR.js +136 -0
  162. package/dist/chunks/velin-command-ONKO2A3J.js +136 -0
  163. package/dist/chunks/velin-command-TMLSRGIU.js +136 -0
  164. package/dist/chunks/velin-copy-G2TZ5YBB.js +81 -0
  165. package/dist/chunks/velin-copy-NYDBOKXT.js +81 -0
  166. package/dist/chunks/velin-copy-T5KDTDXP.js +81 -0
  167. package/dist/chunks/velin-countdown-4ZVDGK4R.js +101 -0
  168. package/dist/chunks/velin-countdown-5XSTN3BH.js +101 -0
  169. package/dist/chunks/velin-countdown-7AIY66CU.js +101 -0
  170. package/dist/chunks/velin-counter-ISCTEAHI.js +118 -0
  171. package/dist/chunks/velin-counter-L5FYJF7A.js +121 -0
  172. package/dist/chunks/velin-dialog-D2GUHSCD.js +144 -0
  173. package/dist/chunks/velin-dialog-ELFOZIXZ.js +152 -0
  174. package/dist/chunks/velin-dialog-GTTZI564.js +147 -0
  175. package/dist/chunks/velin-dialog-IJLFDXAJ.js +144 -0
  176. package/dist/chunks/velin-dialog-TNS3WXMB.js +144 -0
  177. package/dist/chunks/velin-drawer-BE6TYN3H.js +130 -0
  178. package/dist/chunks/velin-drawer-DLL2GLT6.js +130 -0
  179. package/dist/chunks/velin-drawer-MJS6WCF7.js +130 -0
  180. package/dist/chunks/velin-dropdown-PLUIFA5M.js +172 -0
  181. package/dist/chunks/velin-email-N5B5ZYTE.js +90 -0
  182. package/dist/chunks/velin-email-NTM6DG3V.js +90 -0
  183. package/dist/chunks/velin-email-RBX6ZR4S.js +90 -0
  184. package/dist/chunks/velin-haptic-4QQTM3RK.js +50 -0
  185. package/dist/chunks/velin-icon-7M76DOPB.js +164 -0
  186. package/dist/chunks/velin-icon-KZO3IEHG.js +164 -0
  187. package/dist/chunks/velin-icon-WTFPG2DD.js +191 -0
  188. package/dist/chunks/velin-icon-ZTSKUOVN.js +193 -0
  189. package/dist/chunks/velin-lightbox-6E7CAATP.js +149 -0
  190. package/dist/chunks/velin-lightbox-I75U6JF4.js +140 -0
  191. package/dist/chunks/velin-lightbox-QOTB4PRL.js +140 -0
  192. package/dist/chunks/velin-lightbox-UCD6N7HF.js +140 -0
  193. package/dist/chunks/velin-live-dot-RJHX2DGY.js +74 -0
  194. package/dist/chunks/velin-live-dot-XHUZCIDZ.js +67 -0
  195. package/dist/chunks/velin-menubar-I426AED2.js +86 -0
  196. package/dist/chunks/velin-menubar-J66JXRWW.js +86 -0
  197. package/dist/chunks/velin-menubar-SFEXJE56.js +86 -0
  198. package/dist/chunks/velin-modal-2HOBXE6F.js +176 -0
  199. package/dist/chunks/velin-modal-5HYNNNUI.js +176 -0
  200. package/dist/chunks/velin-modal-AEOTZ67O.js +177 -0
  201. package/dist/chunks/velin-modal-GLI2XZZB.js +176 -0
  202. package/dist/chunks/velin-persist-DFPL3QP4.js +110 -0
  203. package/dist/chunks/velin-popover-EXINRIGJ.js +145 -0
  204. package/dist/chunks/velin-popover-HQW6Q2Y6.js +172 -0
  205. package/dist/chunks/velin-popover-OXX7MRZ6.js +145 -0
  206. package/dist/chunks/velin-popover-T72HK3DF.js +151 -0
  207. package/dist/chunks/velin-popover-YKCCROJG.js +145 -0
  208. package/dist/chunks/velin-progress-ring-E4YRLZHH.js +66 -0
  209. package/dist/chunks/velin-rating-6CDBI6DG.js +91 -0
  210. package/dist/chunks/velin-rating-N4DI2XNL.js +91 -0
  211. package/dist/chunks/velin-rating-XRQJV4LO.js +91 -0
  212. package/dist/chunks/velin-scroll-top-F2FCZNYD.js +58 -0
  213. package/dist/chunks/velin-scroll-top-OHYVVHHY.js +58 -0
  214. package/dist/chunks/velin-scrollspy-OKCT4FDV.js +50 -0
  215. package/dist/chunks/velin-search-2XRQFBCS.js +556 -0
  216. package/dist/chunks/velin-search-4YHJWYSS.js +556 -0
  217. package/dist/chunks/velin-search-7B4KJ6TX.js +557 -0
  218. package/dist/chunks/velin-search-LWHY23Q5.js +474 -0
  219. package/dist/chunks/velin-secure-field-6PV7RZDJ.js +95 -0
  220. package/dist/chunks/velin-secure-field-H7FSBUJW.js +95 -0
  221. package/dist/chunks/velin-secure-field-OFNAXCDP.js +95 -0
  222. package/dist/chunks/velin-segmented-control-EKL4E3BL.js +109 -0
  223. package/dist/chunks/velin-segmented-control-GYD5BIXQ.js +109 -0
  224. package/dist/chunks/velin-segmented-control-PQX6LKOJ.js +109 -0
  225. package/dist/chunks/velin-sheet-3OGI4HD7.js +119 -0
  226. package/dist/chunks/velin-sheet-OUK6572T.js +119 -0
  227. package/dist/chunks/velin-sheet-R2Y67X6H.js +119 -0
  228. package/dist/chunks/velin-sparkline-GVEANJVW.js +184 -0
  229. package/dist/chunks/velin-sparkline-KDA3GQAN.js +162 -0
  230. package/dist/chunks/velin-sparkline-XVTFC6GE.js +181 -0
  231. package/dist/chunks/velin-stepper-2ZIHLKGM.js +141 -0
  232. package/dist/chunks/velin-stepper-7ECRUYHD.js +148 -0
  233. package/dist/chunks/velin-stepper-FYEUEWE6.js +141 -0
  234. package/dist/chunks/velin-stepper-X54WGHDG.js +141 -0
  235. package/dist/chunks/velin-stepper-wc-QU3KRVQR.js +133 -0
  236. package/dist/chunks/velin-tabs-5UQ5ZVMV.js +138 -0
  237. package/dist/chunks/velin-theme-toggle-32J2CBNC.js +291 -0
  238. package/dist/chunks/velin-theme-toggle-BNNS7WLV.js +285 -0
  239. package/dist/chunks/velin-theme-toggle-NO7V4TWI.js +285 -0
  240. package/dist/chunks/velin-theme-toggle-QELCJM2Q.js +291 -0
  241. package/dist/chunks/velin-toast-OMMOA2DK.js +109 -0
  242. package/dist/chunks/velin-toast-RCZ4CSUC.js +109 -0
  243. package/dist/chunks/velin-toast-RSF2IHT3.js +113 -0
  244. package/dist/chunks/velin-toast-WEUBTJG6.js +109 -0
  245. package/dist/chunks/velin-tooltip-664IOHZA.js +112 -0
  246. package/dist/chunks/velin-tooltip-BDHGRTQB.js +112 -0
  247. package/dist/chunks/velin-tooltip-HVUOKNVX.js +112 -0
  248. package/dist/chunks/velin-tooltip-wc-LXDVT6DE.js +103 -0
  249. package/dist/chunks/worker-client-R6DBYPFT.js +46 -0
  250. package/dist/llms.test.txt +40 -0
  251. package/dist/llms.txt +40 -0
  252. package/dist/search-index.json +1773 -0
  253. package/dist/search-index.test.json +1773 -0
  254. package/dist/themes/brutalist.min.css +1 -1
  255. package/dist/themes/midnight.min.css +1 -1
  256. package/dist/themes/pastel.min.css +1 -1
  257. package/dist/themes/retro.min.css +1 -1
  258. package/dist/themes/sunset.min.css +1 -1
  259. package/dist/velin-agent.json +684 -0
  260. package/dist/velin-agent.test.json +684 -0
  261. package/dist/velinstyle-components.iife.js +7715 -3515
  262. package/dist/velinstyle-components.js +7087 -2859
  263. package/dist/velinstyle-components.min.js +196 -109
  264. package/dist/velinstyle.css +604 -65
  265. package/dist/velinstyle.d.ts +54 -0
  266. package/dist/velinstyle.min.css +1 -1
  267. package/examples/search-index.schema.json +33 -0
  268. package/package.json +56 -10
  269. package/src/a11y/authentication.css +37 -0
  270. package/src/a11y/consistent-help.css +22 -0
  271. package/src/a11y/dragging-alternatives.css +30 -0
  272. package/src/a11y/focus-appearance.css +20 -0
  273. package/src/a11y/high-contrast-aaa.css +16 -26
  274. package/src/a11y/skip-link.css +3 -1
  275. package/src/a11y/sr-only.css +1 -1
  276. package/src/base/reset.css +0 -36
  277. package/src/base/wc-placeholder.css +102 -0
  278. package/src/components/attributes.css +42 -0
  279. package/src/components/highlight.css +182 -0
  280. package/src/components/input-group.css +1 -2
  281. package/src/components/nav.css +2 -3
  282. package/src/components/search.css +95 -0
  283. package/src/components/stepper.css +2 -4
  284. package/src/components/timeline.css +1 -2
  285. package/src/components/tooltip.css +2 -4
  286. package/src/layout/container.css +5 -5
  287. package/src/layout/grid.css +8 -8
  288. package/src/layout/patterns.css +5 -5
  289. package/src/themes/brutalist.css +2 -11
  290. package/src/themes/corporate.css +0 -9
  291. package/src/themes/earth.css +0 -9
  292. package/src/themes/forest.css +0 -9
  293. package/src/themes/midnight.css +1 -10
  294. package/src/themes/neon.css +0 -9
  295. package/src/themes/nordic.css +0 -9
  296. package/src/themes/ocean.css +0 -9
  297. package/src/themes/pastel.css +1 -10
  298. package/src/themes/retro.css +2 -11
  299. package/src/themes/sharp.css +0 -7
  300. package/src/themes/soft.css +0 -7
  301. package/src/themes/sunset.css +1 -10
  302. package/src/tokens/color.css +45 -46
  303. package/src/tokens/fonts.css +10 -0
  304. package/src/tokens/typography.css +4 -5
  305. package/src/utilities/animation.css +2 -2
  306. package/src/utilities/display.css +3 -3
  307. package/src/utilities/divide.css +2 -6
  308. package/src/utilities/responsive.css +4 -4
  309. package/src/utilities/safe-area.css +1 -1
  310. package/src/utilities/scroll-animation.css +34 -0
  311. package/src/utilities/spacing.css +17 -8
  312. package/src/utilities/text.css +4 -1
  313. package/src/velinstyle.css +9 -0
@@ -26,6 +26,18 @@ const styles = `
26
26
  ::slotted(details > summary::-webkit-details-marker) {
27
27
  display: none;
28
28
  }
29
+ ::slotted(details[open] > summary) {
30
+ background: var(--velin-color-primary-subtle, #eff6ff);
31
+ color: var(--velin-color-primary, #2563eb);
32
+ }
33
+ ::slotted(details > :not(summary)) {
34
+ padding: var(--velin-space-4, 1rem) var(--velin-space-5, 1.25rem);
35
+ background: var(--velin-color-bg-subtle, #f8fafc);
36
+ color: var(--velin-color-text-muted, #64748b);
37
+ font-size: var(--velin-text-sm, 0.875rem);
38
+ line-height: 1.6;
39
+ border-block-start: 1px solid var(--velin-color-border, #e2e8f0);
40
+ }
29
41
  `;
30
42
 
31
43
  class VelinAccordion extends HTMLElement {
@@ -52,6 +52,25 @@ class VelinBottomNav extends HTMLElement {
52
52
 
53
53
  _onSlot() {
54
54
  this._syncCurrent();
55
+ this._syncSlotLabels();
56
+ }
57
+
58
+ _syncSlotLabels() {
59
+ const slot = this.shadowRoot?.querySelector('slot');
60
+ if (!slot) return;
61
+ slot.assignedElements().forEach((el) => {
62
+ const text = el.textContent?.replace(/\s+/g, ' ').trim();
63
+ if (!text && !el.getAttribute('aria-label')) {
64
+ const hint =
65
+ el.getAttribute('data-nav') ||
66
+ el.getAttribute('title') ||
67
+ el.getAttribute('aria-labelledby');
68
+ if (hint) el.setAttribute('aria-label', hint);
69
+ }
70
+ if (el.tagName === 'A' && !el.getAttribute('href')) {
71
+ el.setAttribute('role', 'button');
72
+ }
73
+ });
55
74
  }
56
75
 
57
76
  _syncCurrent() {
@@ -69,10 +69,10 @@ class VelinCarousel extends HTMLElement {
69
69
  <div class="track" role="group" aria-roledescription="carousel" part="track"><slot></slot></div>
70
70
  <div class="controls" part="controls">
71
71
  <button class="prev" aria-label="Previous slide" part="prev">
72
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
72
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><polyline points="15 18 9 12 15 6"/></svg>
73
73
  </button>
74
74
  <button class="next" aria-label="Next slide" part="next">
75
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 6 15 12 9 18"/></svg>
75
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><polyline points="9 6 15 12 9 18"/></svg>
76
76
  </button>
77
77
  </div>
78
78
  <div class="indicators" role="group" aria-label="Slide indicators" part="indicators"></div>
@@ -0,0 +1,142 @@
1
+ import { highlightElement, initHighlight } from '../core/highlight/index.js';
2
+
3
+ /**
4
+ * @param {string} spec
5
+ * @returns {Set<number>}
6
+ */
7
+ function parseLineHighlight(spec) {
8
+ const lines = new Set();
9
+ if (!spec) return lines;
10
+ for (const part of spec.split(',')) {
11
+ const p = part.trim();
12
+ if (!p) continue;
13
+ if (p.includes('-')) {
14
+ const [a, b] = p.split('-').map((n) => parseInt(n, 10));
15
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
16
+ for (let i = Math.min(a, b); i <= Math.max(a, b); i += 1) lines.add(i);
17
+ }
18
+ } else {
19
+ const n = parseInt(p, 10);
20
+ if (!Number.isNaN(n)) lines.add(n);
21
+ }
22
+ }
23
+ return lines;
24
+ }
25
+
26
+ class VelinCodeBlock extends HTMLElement {
27
+ static get observedAttributes() {
28
+ return ['language', 'highlight', 'line-numbers', 'collapsed'];
29
+ }
30
+
31
+ connectedCallback() {
32
+ if (this._built) return;
33
+ this._built = true;
34
+ this.classList.add('velin-code-block');
35
+ this._render();
36
+ this._setupHighlight();
37
+ }
38
+
39
+ attributeChangedCallback() {
40
+ if (!this._built) return;
41
+ this._render();
42
+ this._setupHighlight();
43
+ }
44
+
45
+ _render() {
46
+ const lang = this.getAttribute('language') || '';
47
+ const showLines = this.hasAttribute('line-numbers');
48
+ const collapsed = this.hasAttribute('collapsed');
49
+ const highlightLines = parseLineHighlight(this.getAttribute('highlight') || '');
50
+
51
+ const source = this.textContent.trim();
52
+ this.textContent = '';
53
+
54
+ const toolbar = document.createElement('div');
55
+ toolbar.className = 'velin-code-block__toolbar';
56
+ toolbar.innerHTML = '';
57
+
58
+ const copyWc = document.createElement('velin-copy');
59
+ copyWc.setAttribute('value', source);
60
+ copyWc.setAttribute('label', 'Copy');
61
+ toolbar.appendChild(copyWc);
62
+
63
+ const panelId = `velin-code-panel-${Math.random().toString(36).slice(2, 9)}`;
64
+ const wrap = document.createElement('div');
65
+ wrap.id = panelId;
66
+ wrap.className = showLines ? 'velin-code-block__gutter' : '';
67
+
68
+ if (collapsed) {
69
+ const toggle = document.createElement('button');
70
+ toggle.type = 'button';
71
+ toggle.className = 'velin-btn velin-btn--sm velin-btn--ghost';
72
+ toggle.textContent = 'Expand';
73
+ toggle.setAttribute('aria-expanded', 'false');
74
+ toggle.setAttribute('aria-controls', panelId);
75
+ toggle.setAttribute('aria-label', 'Expand code block');
76
+ toggle.addEventListener('click', () => {
77
+ const on = this.hasAttribute('data-collapsed');
78
+ if (on) {
79
+ this.removeAttribute('data-collapsed');
80
+ toggle.textContent = 'Collapse';
81
+ toggle.setAttribute('aria-expanded', 'true');
82
+ toggle.setAttribute('aria-label', 'Collapse code block');
83
+ } else {
84
+ this.setAttribute('data-collapsed', '');
85
+ toggle.textContent = 'Expand';
86
+ toggle.setAttribute('aria-expanded', 'false');
87
+ toggle.setAttribute('aria-label', 'Expand code block');
88
+ }
89
+ });
90
+ this.setAttribute('data-collapsed', '');
91
+ toolbar.appendChild(toggle);
92
+ }
93
+
94
+ if (showLines) {
95
+ const gutter = document.createElement('div');
96
+ gutter.className = 'velin-code-block__line-numbers';
97
+ const lines = source.split('\n');
98
+ const lineCount = lines.length;
99
+ gutter.textContent = '';
100
+ for (let i = 0; i < lineCount; i += 1) {
101
+ const n = i + 1;
102
+ const span = document.createElement('span');
103
+ if (highlightLines.has(n)) span.className = 'velin-code-block__line--highlight';
104
+ span.textContent = String(n);
105
+ gutter.appendChild(span);
106
+ }
107
+ wrap.appendChild(gutter);
108
+ }
109
+
110
+ const pre = document.createElement('pre');
111
+ if (lang) {
112
+ pre.setAttribute('language', lang);
113
+ pre.dataset.language = lang;
114
+ }
115
+ const code = document.createElement('code');
116
+ if (lang) code.className = `language-${lang}`;
117
+ code.textContent = source;
118
+ pre.appendChild(code);
119
+ wrap.appendChild(pre);
120
+
121
+ this.appendChild(toolbar);
122
+ this.appendChild(wrap);
123
+ this._pre = pre;
124
+ }
125
+
126
+ async _setupHighlight() {
127
+ if (!this._pre) return;
128
+ delete this._pre.dataset.velinHighlighted;
129
+ delete this._pre.dataset.velinObserve;
130
+ const immediate =
131
+ typeof matchMedia !== 'undefined' &&
132
+ matchMedia('(prefers-reduced-motion: reduce)').matches;
133
+ if (immediate) {
134
+ await highlightElement(this._pre);
135
+ } else {
136
+ initHighlight(this, { root: this });
137
+ }
138
+ }
139
+ }
140
+
141
+ customElements.define('velin-code-block', VelinCodeBlock);
142
+ export default VelinCodeBlock;
@@ -113,8 +113,23 @@ class VelinCommand extends HTMLElement {
113
113
  });
114
114
  }
115
115
 
116
+ _visibleItems() {
117
+ const slot = this.shadowRoot.querySelector('slot');
118
+ return slot?.assignedElements().filter((el) => el.tagName === 'BUTTON' && !el.hidden) || [];
119
+ }
120
+
116
121
  _onKey(e) {
117
122
  if (e.key === 'Escape') { this.close(); return; }
123
+ const items = this._visibleItems();
124
+ if (items.length && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
125
+ e.preventDefault();
126
+ const idx = items.indexOf(document.activeElement);
127
+ const next = e.key === 'ArrowDown'
128
+ ? items[(idx + 1) % items.length]
129
+ : items[(idx <= 0 ? items.length : idx) - 1];
130
+ next?.focus();
131
+ return;
132
+ }
118
133
  trapFocus(this.shadowRoot, e);
119
134
  }
120
135
 
@@ -1,3 +1,5 @@
1
+ import { escapeHTML } from './sanitize.js';
2
+
1
3
  const styles = `
2
4
  :host { display: inline-flex; }
3
5
  button {
@@ -27,14 +29,14 @@ class VelinCopy extends HTMLElement {
27
29
  }
28
30
 
29
31
  connectedCallback() {
30
- const label = this.getAttribute('label') || '';
32
+ const label = escapeHTML(this.getAttribute('label') || '');
31
33
  this.shadowRoot.innerHTML = `
32
34
  <style>${styles}</style>
33
35
  <button part="button" aria-label="Copy">
34
- <svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
36
+ <svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
35
37
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
36
38
  </svg>
37
- <svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
39
+ <svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
38
40
  <polyline points="20 6 9 17 4 12"/>
39
41
  </svg>
40
42
  ${label ? `<span>${label}</span>` : ''}
@@ -44,8 +46,17 @@ class VelinCopy extends HTMLElement {
44
46
  this.shadowRoot.querySelector('button').addEventListener('click', () => this._copy());
45
47
  }
46
48
 
49
+ _getCopyText() {
50
+ return (
51
+ this.getAttribute('value') ||
52
+ this.getAttribute('text') ||
53
+ this.dataset.source ||
54
+ ''
55
+ );
56
+ }
57
+
47
58
  async _copy() {
48
- const value = this.getAttribute('value') || '';
59
+ const value = this._getCopyText();
49
60
  if (!value) return;
50
61
 
51
62
  try {
@@ -72,5 +83,7 @@ class VelinCopy extends HTMLElement {
72
83
  }
73
84
  }
74
85
 
75
- customElements.define('velin-copy', VelinCopy);
86
+ if (!customElements.get('velin-copy')) {
87
+ customElements.define('velin-copy', VelinCopy);
88
+ }
76
89
  export default VelinCopy;
@@ -61,6 +61,9 @@ class VelinCounter extends HTMLElement {
61
61
  }
62
62
 
63
63
  connectedCallback() {
64
+ this.setAttribute('role', 'status');
65
+ this.setAttribute('aria-live', 'polite');
66
+ this.setAttribute('aria-atomic', 'true');
64
67
  this._render(this._fromValue());
65
68
  if (this.getAttribute('autostart') === 'false') return;
66
69
  this._scheduleStart();
@@ -69,6 +69,10 @@ class VelinDialog extends HTMLElement {
69
69
 
70
70
  connectedCallback() {
71
71
  this.shadowRoot.innerHTML = `<style>${styles}</style><dialog part="dialog"></dialog>`;
72
+ const dialog = this.shadowRoot.querySelector('dialog');
73
+ if (dialog) {
74
+ dialog.setAttribute('aria-label', this.getAttribute('aria-label') || 'Dialog');
75
+ }
72
76
  }
73
77
 
74
78
  alert(message, { title = 'Alert', confirmText = 'OK' } = {}) {
@@ -102,11 +106,15 @@ class VelinDialog extends HTMLElement {
102
106
  ? `<input class="input" placeholder="${safePlaceholder}" value="${safeDefault}" part="input">`
103
107
  : '';
104
108
 
109
+ const titleId = `velin-dialog-title-${Math.random().toString(36).slice(2, 9)}`;
105
110
  dialog.innerHTML = `
106
- <div class="header"><h3 class="title">${safeTitle}</h3><button class="close" aria-label="Close">&times;</button></div>
111
+ <div class="header"><h3 class="title" id="${titleId}">${safeTitle}</h3><button class="close" aria-label="Close">&times;</button></div>
107
112
  <div class="body"><p>${safeMsg}</p>${input}</div>
108
113
  <div class="footer">${footerBtns}</div>
109
114
  `;
115
+ dialog.setAttribute('aria-modal', 'true');
116
+ dialog.removeAttribute('aria-label');
117
+ dialog.setAttribute('aria-labelledby', titleId);
110
118
 
111
119
  dialog.showModal();
112
120
  if (type !== 'prompt') {
@@ -0,0 +1,94 @@
1
+ import { escapeHTML, escapeHTMLAttribute } from './sanitize.js';
2
+
3
+ const styles = `
4
+ :host { display: inline; }
5
+ button {
6
+ font: inherit;
7
+ color: var(--velin-color-primary, #4338ca);
8
+ background: none;
9
+ border: none;
10
+ padding: 0;
11
+ cursor: pointer;
12
+ text-decoration: underline;
13
+ text-underline-offset: 0.15em;
14
+ min-height: 2.75rem;
15
+ min-width: 2.75rem;
16
+ }
17
+ button:focus-visible {
18
+ outline: 3px solid var(--velin-color-focus, #2563eb);
19
+ outline-offset: 2px;
20
+ border-radius: 2px;
21
+ }
22
+ .revealed { text-decoration: none; user-select: text; }
23
+ `;
24
+
25
+ function decodeObfuscated(raw, method) {
26
+ if (!raw) return '';
27
+ if (method === 'rot13') {
28
+ return raw.replace(/[a-zA-Z]/g, (c) => {
29
+ const base = c <= 'Z' ? 65 : 97;
30
+ return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
31
+ });
32
+ }
33
+ try {
34
+ return atob(raw);
35
+ } catch {
36
+ return raw;
37
+ }
38
+ }
39
+
40
+ class VelinEmail extends HTMLElement {
41
+ static get observedAttributes() {
42
+ return ['value', 'obfuscate', 'label'];
43
+ }
44
+
45
+ constructor() {
46
+ super();
47
+ this.attachShadow({ mode: 'open' });
48
+ this._revealed = false;
49
+ }
50
+
51
+ connectedCallback() {
52
+ this.render();
53
+ }
54
+
55
+ attributeChangedCallback() {
56
+ if (this.isConnected) this.render();
57
+ }
58
+
59
+ get email() {
60
+ const method = this.getAttribute('obfuscate') || '';
61
+ const raw = this.getAttribute('value') || '';
62
+ return method ? decodeObfuscated(raw, method) : raw;
63
+ }
64
+
65
+ render() {
66
+ const label = this.getAttribute('label') || 'Show email address';
67
+ const email = this.email;
68
+ const safeEmail = escapeHTML(email);
69
+
70
+ if (this._revealed) {
71
+ this.shadowRoot.innerHTML = `
72
+ <style>${styles}</style>
73
+ <span class="revealed" part="email"><a href="mailto:${escapeHTMLAttribute(email)}">${safeEmail}</a></span>
74
+ `;
75
+ return;
76
+ }
77
+
78
+ this.shadowRoot.innerHTML = `
79
+ <style>${styles}</style>
80
+ <button type="button" part="reveal" aria-label="${escapeHTMLAttribute(label)}">${escapeHTML(label)}</button>
81
+ `;
82
+ this.shadowRoot.querySelector('button').addEventListener('click', () => {
83
+ this._revealed = true;
84
+ this.render();
85
+ this.dispatchEvent(new CustomEvent('velin-email-reveal', { bubbles: true, detail: { email } }));
86
+ });
87
+ }
88
+ }
89
+
90
+ if (!customElements.get('velin-email')) {
91
+ customElements.define('velin-email', VelinEmail);
92
+ }
93
+
94
+ export default VelinEmail;
@@ -1,16 +1,22 @@
1
+ import { sanitizeURL, sanitizeSVG } from './sanitize.js';
2
+
1
3
  const PROVIDER_CDNS = {
2
4
  lucide: 'https://unpkg.com/lucide-static@latest/icons/{name}.svg',
3
5
  heroicons: 'https://unpkg.com/heroicons@2/24/outline/{name}.svg',
4
6
  bootstrap: 'https://unpkg.com/bootstrap-icons@latest/icons/{name}.svg',
5
7
  material: 'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/{name}/default/24px.svg',
6
- fontawesome: 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/{name}.svg',
8
+ fontawesome:
9
+ 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/svgs/solid/{name}.svg',
7
10
  };
8
11
 
9
12
  const PROVIDER_VARIANTS = {
10
13
  fontawesome: {
11
- regular: 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/regular/{name}.svg',
12
- solid: 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/{name}.svg',
13
- brands: 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/{name}.svg',
14
+ regular:
15
+ 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/svgs/regular/{name}.svg',
16
+ solid:
17
+ 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/svgs/solid/{name}.svg',
18
+ brands:
19
+ 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/svgs/brands/{name}.svg',
14
20
  },
15
21
  heroicons: {
16
22
  outline: 'https://unpkg.com/heroicons@2/24/outline/{name}.svg',
@@ -48,7 +54,7 @@ class VelinIcon extends HTMLElement {
48
54
  _render() {
49
55
  const name = this.getAttribute('name');
50
56
  const size = this.getAttribute('size') || '24';
51
- const label = this.getAttribute('label');
57
+ const a11y = this._resolveA11y();
52
58
  const provider = this.getAttribute('provider');
53
59
  const variant = this.getAttribute('variant');
54
60
 
@@ -58,14 +64,21 @@ class VelinIcon extends HTMLElement {
58
64
  }
59
65
 
60
66
  if (provider && (PROVIDER_CDNS[provider] || PROVIDER_VARIANTS[provider])) {
61
- this._renderFromCDN(name, size, label, provider, variant);
67
+ this._renderFromCDN(name, size, a11y, provider, variant);
62
68
  return;
63
69
  }
64
70
 
65
- this._renderFromSprite(name, size, label);
71
+ this._renderFromSprite(name, size, a11y);
72
+ }
73
+
74
+ _resolveA11y() {
75
+ const label = (this.getAttribute('label') || this.getAttribute('aria-label') || '').trim();
76
+ if (label) return { label, hidden: false };
77
+ const hidden = this.getAttribute('aria-hidden') !== 'false';
78
+ return { label: '', hidden };
66
79
  }
67
80
 
68
- _renderFromSprite(name, size, label) {
81
+ _renderFromSprite(name, size, a11y) {
69
82
  const svgNS = 'http://www.w3.org/2000/svg';
70
83
  const svg = document.createElementNS(svgNS, 'svg');
71
84
  svg.setAttribute('width', size);
@@ -77,7 +90,6 @@ class VelinIcon extends HTMLElement {
77
90
  svg.setAttribute('stroke-linecap', 'round');
78
91
  svg.setAttribute('stroke-linejoin', 'round');
79
92
  this._applyStyle(svg);
80
- this._applyA11y(svg, label);
81
93
 
82
94
  const use = document.createElementNS(svgNS, 'use');
83
95
  const spriteAttr = this.getAttribute('sprite');
@@ -87,54 +99,70 @@ class VelinIcon extends HTMLElement {
87
99
  if (spriteAttr === '' || (spriteAttr == null && isLocalSymbol)) {
88
100
  href = `#${name}`;
89
101
  } else {
90
- const spriteUrl = spriteAttr || 'velin-icons.svg';
102
+ const spriteUrl = sanitizeURL(spriteAttr || '/dist/velin-icons.svg') || '/dist/velin-icons.svg';
91
103
  href = `${spriteUrl}#${name}`;
92
104
  }
93
105
  use.setAttribute('href', href);
94
106
  svg.appendChild(use);
107
+ this._applyA11y(svg, a11y);
95
108
 
96
109
  this.innerHTML = '';
97
110
  this.appendChild(svg);
98
111
  this._rendered = true;
99
112
  }
100
113
 
101
- async _renderFromCDN(name, size, label, provider, variant) {
114
+ async _renderFromCDN(name, size, a11y, provider, variant) {
102
115
  const cacheKey = `${provider}:${variant || 'default'}:${name}`;
103
116
 
104
117
  if (_svgCache.has(cacheKey)) {
105
- this._injectSVG(_svgCache.get(cacheKey), size, label);
118
+ this._injectSVG(_svgCache.get(cacheKey), size, a11y);
106
119
  return;
107
120
  }
108
121
 
109
122
  const template = resolveProviderUrl(provider, variant);
110
123
  if (!template) {
111
- this._renderFromSprite(name, size, label);
124
+ this._renderFromSprite(name, size, a11y);
125
+ return;
126
+ }
127
+ const url = sanitizeURL(template.replace('{name}', name));
128
+ if (!url) {
129
+ this._renderFromSprite(name, size, a11y);
112
130
  return;
113
131
  }
114
- const url = template.replace('{name}', name);
115
132
  try {
116
133
  const res = await fetch(url);
117
134
  if (!res.ok) throw new Error(`${res.status}`);
118
135
  const text = await res.text();
119
136
  if (!text.includes('<svg')) throw new Error('Not SVG');
120
- _svgCache.set(cacheKey, text);
121
- this._injectSVG(text, size, label);
137
+ const clean = await sanitizeSVG(text);
138
+ _svgCache.set(cacheKey, clean);
139
+ this._injectSVG(clean, size, a11y);
122
140
  } catch {
123
- this._renderFromSprite(name, size, label);
141
+ this._renderFromSprite(name, size, a11y);
124
142
  }
125
143
  }
126
144
 
127
- _injectSVG(svgText, size, label) {
145
+ _stripForeignA11y(svg) {
146
+ svg.removeAttribute('role');
147
+ svg.removeAttribute('aria-label');
148
+ svg.removeAttribute('aria-labelledby');
149
+ svg.removeAttribute('aria-hidden');
150
+ svg.querySelector('title')?.remove();
151
+ svg.querySelector('desc')?.remove();
152
+ }
153
+
154
+ _injectSVG(svgText, size, a11y) {
128
155
  const parser = new DOMParser();
129
156
  const doc = parser.parseFromString(svgText, 'image/svg+xml');
130
157
  const svg = doc.querySelector('svg');
131
158
  if (!svg) { this.innerHTML = ''; return; }
132
159
 
160
+ this._stripForeignA11y(svg);
133
161
  svg.setAttribute('width', size);
134
162
  svg.setAttribute('height', size);
135
163
  if (!svg.getAttribute('viewBox')) svg.setAttribute('viewBox', '0 0 24 24');
136
164
  this._applyStyle(svg);
137
- this._applyA11y(svg, label);
165
+ this._applyA11y(svg, a11y);
138
166
 
139
167
  this.innerHTML = '';
140
168
  this.appendChild(document.importNode(svg, true));
@@ -147,19 +175,34 @@ class VelinIcon extends HTMLElement {
147
175
  svg.style.flexShrink = '0';
148
176
  }
149
177
 
150
- _applyA11y(svg, label) {
178
+ _applyA11y(svg, a11y) {
179
+ const { label, hidden } = a11y;
180
+ const use = svg.querySelector('use');
151
181
  if (label) {
152
182
  svg.setAttribute('role', 'img');
153
183
  svg.setAttribute('aria-label', label);
154
- } else {
184
+ svg.removeAttribute('aria-hidden');
185
+ svg.removeAttribute('focusable');
186
+ if (use) use.removeAttribute('aria-hidden');
187
+ } else if (hidden) {
188
+ svg.setAttribute('role', 'presentation');
155
189
  svg.setAttribute('aria-hidden', 'true');
190
+ svg.setAttribute('focusable', 'false');
191
+ svg.removeAttribute('aria-label');
192
+ if (use) {
193
+ use.setAttribute('aria-hidden', 'true');
194
+ use.setAttribute('focusable', 'false');
195
+ }
156
196
  }
157
197
  }
158
198
 
159
199
  static get providers() { return Object.keys(PROVIDER_CDNS); }
160
200
 
161
- static registerProvider(name, urlTemplate) {
201
+ static registerProvider(name, urlTemplate, variantTemplates) {
162
202
  PROVIDER_CDNS[name] = urlTemplate;
203
+ if (variantTemplates && PROVIDER_VARIANTS[name]) {
204
+ Object.assign(PROVIDER_VARIANTS[name], variantTemplates);
205
+ }
163
206
  }
164
207
  }
165
208
 
@@ -1,4 +1,5 @@
1
- import { trapFocus } from './focus-manager.js';
1
+ import { trapFocus, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
2
+ import { announce } from './a11y-utils.js';
2
3
  import { escapeHTML, sanitizeURL } from './sanitize.js';
3
4
 
4
5
  const styles = `
@@ -61,7 +62,7 @@ class VelinLightbox extends HTMLElement {
61
62
  <div class="content" part="content"></div>
62
63
  <button class="nav nav--next" aria-label="Next">&#8250;</button>
63
64
  <button class="close" aria-label="Close">&times;</button>
64
- <div class="counter" part="counter"></div>
65
+ <div class="counter" part="counter" aria-live="polite" aria-atomic="true"></div>
65
66
  </div>
66
67
  `;
67
68
 
@@ -89,6 +90,7 @@ class VelinLightbox extends HTMLElement {
89
90
  this._previousFocus = document.activeElement;
90
91
  this._index = index;
91
92
  this.setAttribute('open', '');
93
+ setBackgroundInert(this);
92
94
  this._render();
93
95
  document.body.style.overflow = 'hidden';
94
96
  const overlay = this.shadowRoot.querySelector('.overlay');
@@ -99,6 +101,7 @@ class VelinLightbox extends HTMLElement {
99
101
 
100
102
  close() {
101
103
  this.removeAttribute('open');
104
+ clearBackgroundInert();
102
105
  document.body.style.overflow = '';
103
106
  if (this._previousFocus) {
104
107
  this._previousFocus.focus();
@@ -122,7 +125,9 @@ class VelinLightbox extends HTMLElement {
122
125
  container.innerHTML = `<img src="${escapeHTML(src)}" alt="${alt}">`;
123
126
  }
124
127
  const counter = this.shadowRoot.querySelector('.counter');
125
- counter.textContent = `${this._index + 1} / ${this._items.length}`;
128
+ const label = `${this._index + 1} / ${this._items.length}`;
129
+ counter.textContent = label;
130
+ announce(`Slide ${label}`, 'polite');
126
131
  }
127
132
  }
128
133
 
@@ -11,6 +11,8 @@
11
11
  * pulse "true" (default) | "false" Disables the looped pulse.
12
12
  */
13
13
 
14
+ import { liveDotLabel } from './a11y-utils.js';
15
+
14
16
  const STATUS_COLORS = {
15
17
  live: 'var(--velin-color-success, oklch(60% 0.16 145))',
16
18
  paused: 'var(--velin-color-text-muted, oklch(60% 0.02 240))',
@@ -70,6 +72,9 @@ class VelinLiveDot extends HTMLElement {
70
72
  _render() {
71
73
  const status = this.getAttribute('status') || 'live';
72
74
  const color = STATUS_COLORS[status] || STATUS_COLORS.live;
75
+ const label = this.getAttribute('aria-label') || liveDotLabel(status);
76
+ this.setAttribute('role', 'status');
77
+ this.setAttribute('aria-label', label);
73
78
  this.style.setProperty('--velin-live-color', color);
74
79
  this.shadowRoot.innerHTML = `
75
80
  <style>${styles}${KEYFRAMES_FALLBACK}</style>