@birdapi/velinstyle 0.7.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 (336) hide show
  1. package/README.de.md +135 -224
  2. package/README.md +134 -223
  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/cli-manifest.json +97 -0
  13. package/cli/docgen/extract-a11y.js +89 -0
  14. package/cli/docgen/extract-attributes.js +95 -0
  15. package/cli/docgen/extract-cli.js +50 -0
  16. package/cli/docgen/extract-components.js +175 -0
  17. package/cli/docgen/extract-meta.js +32 -0
  18. package/cli/docgen/extract-rules.js +76 -0
  19. package/cli/docgen/extract-tokens.js +79 -0
  20. package/cli/docgen/extract-utilities.js +75 -0
  21. package/cli/docgen/markdown.js +43 -0
  22. package/cli/docs-generate.js +196 -0
  23. package/cli/index.js +236 -5
  24. package/cli/layout-audit.js +325 -0
  25. package/cli/meta.js +97 -0
  26. package/cli/perf-audit.js +174 -0
  27. package/cli/pii-scanner.js +134 -0
  28. package/cli/scaffold-recipes.json +70 -0
  29. package/cli/scaffold.js +155 -0
  30. package/cli/scanner-rules-data.js +62 -0
  31. package/cli/scanner.js +136 -1
  32. package/cli/search-index.js +163 -0
  33. package/cli/tokens-build.js +21 -1
  34. package/cli/tokens-validate.js +69 -0
  35. package/components/a11y-entry.js +42 -0
  36. package/components/a11y-utils.js +71 -0
  37. package/components/focus-manager.js +45 -8
  38. package/components/index.js +40 -11
  39. package/components/runtime/component-loaders.js +43 -0
  40. package/components/runtime/index.js +77 -0
  41. package/components/runtime-entry.js +10 -0
  42. package/components/sanitize.js +135 -28
  43. package/components/shadow-a11y-styles.js +18 -0
  44. package/components/velin-accordion.js +12 -0
  45. package/components/velin-announcer.js +35 -0
  46. package/components/velin-bottom-nav.js +108 -0
  47. package/components/velin-carousel.js +2 -2
  48. package/components/velin-code-block.js +142 -0
  49. package/components/velin-combobox.js +149 -0
  50. package/components/velin-command.js +142 -0
  51. package/components/velin-copy.js +18 -5
  52. package/components/velin-counter.js +155 -0
  53. package/components/velin-dialog.js +9 -1
  54. package/components/velin-email.js +94 -0
  55. package/components/velin-flip.js +220 -0
  56. package/components/velin-icon.js +99 -22
  57. package/components/velin-lightbox.js +8 -3
  58. package/components/velin-live-dot.js +90 -0
  59. package/components/velin-menubar.js +83 -0
  60. package/components/velin-modal.js +6 -3
  61. package/components/velin-popover.js +34 -2
  62. package/components/velin-rating.js +91 -0
  63. package/components/velin-reveal.js +22 -0
  64. package/components/velin-scroll-top.js +1 -1
  65. package/components/velin-search.js +267 -0
  66. package/components/velin-secure-field.js +99 -0
  67. package/components/velin-segmented-control.js +108 -0
  68. package/components/velin-sheet.js +107 -0
  69. package/components/velin-sparkline.js +227 -0
  70. package/components/{velin-stepper-wc.js → velin-stepper.js} +37 -8
  71. package/components/velin-theme-toggle.js +284 -60
  72. package/components/velin-toast.js +4 -0
  73. package/components/{velin-tooltip-wc.js → velin-tooltip.js} +22 -3
  74. package/core/a11y/component-contracts.json +221 -0
  75. package/core/attributes/index.js +6 -0
  76. package/core/attributes/registry.js +268 -0
  77. package/core/highlight/highlight.js +114 -0
  78. package/core/highlight/index.js +29 -0
  79. package/core/highlight/languages/_utils.js +52 -0
  80. package/core/highlight/languages/blade.js +16 -0
  81. package/core/highlight/languages/css.js +26 -0
  82. package/core/highlight/languages/html.js +17 -0
  83. package/core/highlight/languages/js.js +58 -0
  84. package/core/highlight/languages/json.js +14 -0
  85. package/core/highlight/languages/markdown.js +16 -0
  86. package/core/highlight/languages/php.js +39 -0
  87. package/core/highlight/languages/plain.js +7 -0
  88. package/core/highlight/languages/shell.js +33 -0
  89. package/core/highlight/languages/sql.js +20 -0
  90. package/core/highlight/languages/typescript.js +28 -0
  91. package/core/highlight/observe.js +53 -0
  92. package/core/highlight/registry.js +107 -0
  93. package/core/highlight/render.js +35 -0
  94. package/core/highlight/types.js +28 -0
  95. package/core/meta/build.js +293 -0
  96. package/core/meta/index.js +17 -0
  97. package/core/meta/schema.js +49 -0
  98. package/core/motion/effects.js +54 -0
  99. package/core/motion/index.js +76 -0
  100. package/core/motion/scheduler.js +72 -0
  101. package/core/motion/scroll.js +32 -0
  102. package/core/motion/stagger.js +24 -0
  103. package/core/search/docs-url.js +96 -0
  104. package/core/search/engine.js +139 -0
  105. package/core/search/highlight.js +48 -0
  106. package/core/search/index.js +126 -0
  107. package/core/search/providers.js +36 -0
  108. package/core/search/types.js +61 -0
  109. package/core/search/worker-client.js +56 -0
  110. package/core/search/worker.js +20 -0
  111. package/dist/chunks/a11y-utils-IRC7LOPF.js +16 -0
  112. package/dist/chunks/attributes-3BZLI7XF.js +364 -0
  113. package/dist/chunks/attributes-4N5JP4CG.js +364 -0
  114. package/dist/chunks/attributes-5Q2WXGRC.js +374 -0
  115. package/dist/chunks/attributes-AIR7SVLK.js +364 -0
  116. package/dist/chunks/attributes-BGRN5XZO.js +364 -0
  117. package/dist/chunks/attributes-BVSQAYLR.js +364 -0
  118. package/dist/chunks/attributes-CDYFUY7Y.js +364 -0
  119. package/dist/chunks/attributes-JMYJZBPE.js +364 -0
  120. package/dist/chunks/attributes-R5JY3X4O.js +374 -0
  121. package/dist/chunks/attributes-STYCS3O5.js +391 -0
  122. package/dist/chunks/attributes-TNSPQTHX.js +374 -0
  123. package/dist/chunks/attributes-TPANLMLF.js +391 -0
  124. package/dist/chunks/blade-LIQLVKZQ.js +19 -0
  125. package/dist/chunks/browser-MRKYZPEH.js +1149 -0
  126. package/dist/chunks/chunk-2354NEBP.js +109 -0
  127. package/dist/chunks/chunk-2VNA7G7S.js +109 -0
  128. package/dist/chunks/chunk-42VVPW6N.js +132 -0
  129. package/dist/chunks/chunk-7FFAMRFV.js +44 -0
  130. package/dist/chunks/chunk-7MCGPHVC.js +69 -0
  131. package/dist/chunks/chunk-AQLRJSGG.js +109 -0
  132. package/dist/chunks/chunk-EWPSDMQT.js +109 -0
  133. package/dist/chunks/chunk-F7OLHTGX.js +22 -0
  134. package/dist/chunks/chunk-J472ERN2.js +242 -0
  135. package/dist/chunks/chunk-LA7264K4.js +85 -0
  136. package/dist/chunks/chunk-M5CHQVP6.js +61 -0
  137. package/dist/chunks/chunk-MKZP2TBH.js +89 -0
  138. package/dist/chunks/chunk-QTJJ3DPO.js +109 -0
  139. package/dist/chunks/chunk-RDE3QE7Z.js +89 -0
  140. package/dist/chunks/chunk-RSMRR27R.js +109 -0
  141. package/dist/chunks/chunk-RZQVWFHW.js +109 -0
  142. package/dist/chunks/chunk-SRL6EWHP.js +109 -0
  143. package/dist/chunks/chunk-U4XHMZ7D.js +109 -0
  144. package/dist/chunks/chunk-V5YIUA6G.js +109 -0
  145. package/dist/chunks/chunk-XIET2VSZ.js +109 -0
  146. package/dist/chunks/chunk-XYLJ2GCB.js +205 -0
  147. package/dist/chunks/chunk-Z4JYQ2XX.js +51 -0
  148. package/dist/chunks/chunk-ZPSPKUYD.js +109 -0
  149. package/dist/chunks/css-MQ5YU77P.js +28 -0
  150. package/dist/chunks/highlight-GJHHV4KM.js +37 -0
  151. package/dist/chunks/highlight-XTHRKBA3.js +37 -0
  152. package/dist/chunks/html-ZODC4DLQ.js +20 -0
  153. package/dist/chunks/js-MNSNYTJU.js +7 -0
  154. package/dist/chunks/json-VEM2HFH3.js +17 -0
  155. package/dist/chunks/markdown-E7OPGS3S.js +19 -0
  156. package/dist/chunks/php-MLL7T3LT.js +41 -0
  157. package/dist/chunks/plain-LJJDC5TN.js +8 -0
  158. package/dist/chunks/runtime-entry.js +14 -0
  159. package/dist/chunks/shell-QTRK4U4E.js +35 -0
  160. package/dist/chunks/sql-PPWLHGJ2.js +23 -0
  161. package/dist/chunks/typescript-JT3CCYWL.js +30 -0
  162. package/dist/chunks/velin-accordion-FBIPAEXG.js +105 -0
  163. package/dist/chunks/velin-accordion-QAQO7BI7.js +117 -0
  164. package/dist/chunks/velin-announcer-PVTXXCWR.js +33 -0
  165. package/dist/chunks/velin-bottom-nav-2N2TYWOF.js +86 -0
  166. package/dist/chunks/velin-bottom-nav-7GEMPIM6.js +86 -0
  167. package/dist/chunks/velin-bottom-nav-CTU6FFZV.js +101 -0
  168. package/dist/chunks/velin-bottom-nav-N3DFQR66.js +86 -0
  169. package/dist/chunks/velin-carousel-Y5VCOABZ.js +213 -0
  170. package/dist/chunks/velin-carousel-ZTBANFQQ.js +213 -0
  171. package/dist/chunks/velin-code-block-ASUBTAQV.js +119 -0
  172. package/dist/chunks/velin-code-block-CIAURDRJ.js +132 -0
  173. package/dist/chunks/velin-code-block-TSUKPY4C.js +123 -0
  174. package/dist/chunks/velin-code-block-Y6CM726Q.js +119 -0
  175. package/dist/chunks/velin-collapse-5XYBUIID.js +80 -0
  176. package/dist/chunks/velin-combobox-2KDTCQ7N.js +155 -0
  177. package/dist/chunks/velin-combobox-NXS4QB4R.js +155 -0
  178. package/dist/chunks/velin-combobox-UGA56HP3.js +155 -0
  179. package/dist/chunks/velin-command-CKDNO4BW.js +148 -0
  180. package/dist/chunks/velin-command-M7RNHUPR.js +136 -0
  181. package/dist/chunks/velin-command-ONKO2A3J.js +136 -0
  182. package/dist/chunks/velin-command-TMLSRGIU.js +136 -0
  183. package/dist/chunks/velin-copy-G2TZ5YBB.js +81 -0
  184. package/dist/chunks/velin-copy-NYDBOKXT.js +81 -0
  185. package/dist/chunks/velin-copy-T5KDTDXP.js +81 -0
  186. package/dist/chunks/velin-countdown-4ZVDGK4R.js +101 -0
  187. package/dist/chunks/velin-countdown-5XSTN3BH.js +101 -0
  188. package/dist/chunks/velin-countdown-7AIY66CU.js +101 -0
  189. package/dist/chunks/velin-counter-ISCTEAHI.js +118 -0
  190. package/dist/chunks/velin-counter-L5FYJF7A.js +121 -0
  191. package/dist/chunks/velin-dialog-D2GUHSCD.js +144 -0
  192. package/dist/chunks/velin-dialog-ELFOZIXZ.js +152 -0
  193. package/dist/chunks/velin-dialog-GTTZI564.js +147 -0
  194. package/dist/chunks/velin-dialog-IJLFDXAJ.js +144 -0
  195. package/dist/chunks/velin-dialog-TNS3WXMB.js +144 -0
  196. package/dist/chunks/velin-drawer-BE6TYN3H.js +130 -0
  197. package/dist/chunks/velin-drawer-DLL2GLT6.js +130 -0
  198. package/dist/chunks/velin-drawer-MJS6WCF7.js +130 -0
  199. package/dist/chunks/velin-dropdown-PLUIFA5M.js +172 -0
  200. package/dist/chunks/velin-email-N5B5ZYTE.js +90 -0
  201. package/dist/chunks/velin-email-NTM6DG3V.js +90 -0
  202. package/dist/chunks/velin-email-RBX6ZR4S.js +90 -0
  203. package/dist/chunks/velin-haptic-4QQTM3RK.js +50 -0
  204. package/dist/chunks/velin-icon-7M76DOPB.js +164 -0
  205. package/dist/chunks/velin-icon-KZO3IEHG.js +164 -0
  206. package/dist/chunks/velin-icon-WTFPG2DD.js +191 -0
  207. package/dist/chunks/velin-icon-ZTSKUOVN.js +193 -0
  208. package/dist/chunks/velin-lightbox-6E7CAATP.js +149 -0
  209. package/dist/chunks/velin-lightbox-I75U6JF4.js +140 -0
  210. package/dist/chunks/velin-lightbox-QOTB4PRL.js +140 -0
  211. package/dist/chunks/velin-lightbox-UCD6N7HF.js +140 -0
  212. package/dist/chunks/velin-live-dot-RJHX2DGY.js +74 -0
  213. package/dist/chunks/velin-live-dot-XHUZCIDZ.js +67 -0
  214. package/dist/chunks/velin-menubar-I426AED2.js +86 -0
  215. package/dist/chunks/velin-menubar-J66JXRWW.js +86 -0
  216. package/dist/chunks/velin-menubar-SFEXJE56.js +86 -0
  217. package/dist/chunks/velin-modal-2HOBXE6F.js +176 -0
  218. package/dist/chunks/velin-modal-5HYNNNUI.js +176 -0
  219. package/dist/chunks/velin-modal-AEOTZ67O.js +177 -0
  220. package/dist/chunks/velin-modal-GLI2XZZB.js +176 -0
  221. package/dist/chunks/velin-persist-DFPL3QP4.js +110 -0
  222. package/dist/chunks/velin-popover-EXINRIGJ.js +145 -0
  223. package/dist/chunks/velin-popover-HQW6Q2Y6.js +172 -0
  224. package/dist/chunks/velin-popover-OXX7MRZ6.js +145 -0
  225. package/dist/chunks/velin-popover-T72HK3DF.js +151 -0
  226. package/dist/chunks/velin-popover-YKCCROJG.js +145 -0
  227. package/dist/chunks/velin-progress-ring-E4YRLZHH.js +66 -0
  228. package/dist/chunks/velin-rating-6CDBI6DG.js +91 -0
  229. package/dist/chunks/velin-rating-N4DI2XNL.js +91 -0
  230. package/dist/chunks/velin-rating-XRQJV4LO.js +91 -0
  231. package/dist/chunks/velin-scroll-top-F2FCZNYD.js +58 -0
  232. package/dist/chunks/velin-scroll-top-OHYVVHHY.js +58 -0
  233. package/dist/chunks/velin-scrollspy-OKCT4FDV.js +50 -0
  234. package/dist/chunks/velin-search-2XRQFBCS.js +556 -0
  235. package/dist/chunks/velin-search-4YHJWYSS.js +556 -0
  236. package/dist/chunks/velin-search-7B4KJ6TX.js +557 -0
  237. package/dist/chunks/velin-search-LWHY23Q5.js +474 -0
  238. package/dist/chunks/velin-secure-field-6PV7RZDJ.js +95 -0
  239. package/dist/chunks/velin-secure-field-H7FSBUJW.js +95 -0
  240. package/dist/chunks/velin-secure-field-OFNAXCDP.js +95 -0
  241. package/dist/chunks/velin-segmented-control-EKL4E3BL.js +109 -0
  242. package/dist/chunks/velin-segmented-control-GYD5BIXQ.js +109 -0
  243. package/dist/chunks/velin-segmented-control-PQX6LKOJ.js +109 -0
  244. package/dist/chunks/velin-sheet-3OGI4HD7.js +119 -0
  245. package/dist/chunks/velin-sheet-OUK6572T.js +119 -0
  246. package/dist/chunks/velin-sheet-R2Y67X6H.js +119 -0
  247. package/dist/chunks/velin-sparkline-GVEANJVW.js +184 -0
  248. package/dist/chunks/velin-sparkline-KDA3GQAN.js +162 -0
  249. package/dist/chunks/velin-sparkline-XVTFC6GE.js +181 -0
  250. package/dist/chunks/velin-stepper-2ZIHLKGM.js +141 -0
  251. package/dist/chunks/velin-stepper-7ECRUYHD.js +148 -0
  252. package/dist/chunks/velin-stepper-FYEUEWE6.js +141 -0
  253. package/dist/chunks/velin-stepper-X54WGHDG.js +141 -0
  254. package/dist/chunks/velin-stepper-wc-QU3KRVQR.js +133 -0
  255. package/dist/chunks/velin-tabs-5UQ5ZVMV.js +138 -0
  256. package/dist/chunks/velin-theme-toggle-32J2CBNC.js +291 -0
  257. package/dist/chunks/velin-theme-toggle-BNNS7WLV.js +285 -0
  258. package/dist/chunks/velin-theme-toggle-NO7V4TWI.js +285 -0
  259. package/dist/chunks/velin-theme-toggle-QELCJM2Q.js +291 -0
  260. package/dist/chunks/velin-toast-OMMOA2DK.js +109 -0
  261. package/dist/chunks/velin-toast-RCZ4CSUC.js +109 -0
  262. package/dist/chunks/velin-toast-RSF2IHT3.js +113 -0
  263. package/dist/chunks/velin-toast-WEUBTJG6.js +109 -0
  264. package/dist/chunks/velin-tooltip-664IOHZA.js +112 -0
  265. package/dist/chunks/velin-tooltip-BDHGRTQB.js +112 -0
  266. package/dist/chunks/velin-tooltip-HVUOKNVX.js +112 -0
  267. package/dist/chunks/velin-tooltip-wc-LXDVT6DE.js +103 -0
  268. package/dist/chunks/worker-client-R6DBYPFT.js +46 -0
  269. package/dist/llms.test.txt +40 -0
  270. package/dist/llms.txt +40 -0
  271. package/dist/search-index.json +1773 -0
  272. package/dist/search-index.test.json +1773 -0
  273. package/dist/themes/brutalist.min.css +1 -1
  274. package/dist/themes/midnight.min.css +1 -1
  275. package/dist/themes/pastel.min.css +1 -1
  276. package/dist/themes/retro.min.css +1 -1
  277. package/dist/themes/sunset.min.css +1 -1
  278. package/dist/velin-agent.json +684 -0
  279. package/dist/velin-agent.test.json +684 -0
  280. package/dist/velinstyle-components.iife.js +7959 -2199
  281. package/dist/velinstyle-components.js +7982 -2174
  282. package/dist/velinstyle-components.min.js +521 -90
  283. package/dist/velinstyle.css +1290 -282
  284. package/dist/velinstyle.d.ts +54 -0
  285. package/dist/velinstyle.min.css +1 -1
  286. package/examples/search-index.schema.json +33 -0
  287. package/package.json +57 -10
  288. package/src/a11y/authentication.css +37 -0
  289. package/src/a11y/consistent-help.css +22 -0
  290. package/src/a11y/dragging-alternatives.css +30 -0
  291. package/src/a11y/focus-appearance.css +20 -0
  292. package/src/a11y/high-contrast-aaa.css +16 -26
  293. package/src/a11y/security.css +18 -0
  294. package/src/a11y/skip-link.css +3 -1
  295. package/src/a11y/sr-only.css +1 -1
  296. package/src/base/reset.css +0 -25
  297. package/src/base/wc-placeholder.css +102 -0
  298. package/src/components/attributes.css +42 -0
  299. package/src/components/highlight.css +182 -0
  300. package/src/components/input-group.css +1 -2
  301. package/src/components/nav.css +151 -151
  302. package/src/components/search.css +95 -0
  303. package/src/components/stepper.css +2 -4
  304. package/src/components/timeline.css +1 -2
  305. package/src/components/tooltip.css +2 -4
  306. package/src/layout/container.css +5 -5
  307. package/src/layout/grid.css +8 -8
  308. package/src/layout/patterns.css +5 -5
  309. package/src/themes/brutalist.css +2 -11
  310. package/src/themes/corporate.css +0 -9
  311. package/src/themes/earth.css +0 -9
  312. package/src/themes/forest.css +0 -9
  313. package/src/themes/midnight.css +1 -10
  314. package/src/themes/neon.css +0 -9
  315. package/src/themes/nordic.css +0 -9
  316. package/src/themes/ocean.css +0 -9
  317. package/src/themes/pastel.css +1 -10
  318. package/src/themes/retro.css +2 -11
  319. package/src/themes/sharp.css +0 -7
  320. package/src/themes/soft.css +0 -7
  321. package/src/themes/sunset.css +1 -10
  322. package/src/tokens/color.css +45 -46
  323. package/src/tokens/fonts.css +10 -0
  324. package/src/tokens/motion.css +7 -0
  325. package/src/tokens/typography.css +4 -5
  326. package/src/utilities/animation.css +99 -3
  327. package/src/utilities/chart-animation.css +101 -0
  328. package/src/utilities/display.css +3 -3
  329. package/src/utilities/divide.css +2 -6
  330. package/src/utilities/filter-effects.css +103 -0
  331. package/src/utilities/responsive.css +4 -4
  332. package/src/utilities/safe-area.css +39 -0
  333. package/src/utilities/scroll-animation.css +34 -0
  334. package/src/utilities/spacing.css +17 -8
  335. package/src/utilities/text.css +4 -1
  336. package/src/velinstyle.css +12 -0
@@ -85,22 +85,59 @@ export function restoreFocus(element) {
85
85
  }
86
86
  }
87
87
 
88
- let _inertSiblings = [];
88
+ /** @type {{ host: HTMLElement, siblings: Set<Element> }[]} */
89
+ const _inertStack = [];
90
+ let _overflowDepth = 0;
91
+ let _savedOverflow = '';
92
+
93
+ function activeInertElements() {
94
+ const out = new Set();
95
+ for (const layer of _inertStack) {
96
+ for (const el of layer.siblings) out.add(el);
97
+ }
98
+ return out;
99
+ }
89
100
 
90
101
  export function setBackgroundInert(except) {
91
- _inertSiblings = [];
102
+ const host = except instanceof HTMLElement ? except : null;
103
+ if (!host) return;
104
+
105
+ const siblings = new Set();
92
106
  for (const child of document.body.children) {
93
- if (child === except || child.contains(except)) continue;
107
+ if (child === host || child.contains(host)) continue;
94
108
  if (!child.hasAttribute('inert')) {
95
109
  child.setAttribute('inert', '');
96
- _inertSiblings.push(child);
110
+ siblings.add(child);
97
111
  }
98
112
  }
113
+
114
+ _inertStack.push({ host, siblings });
115
+
116
+ if (_overflowDepth === 0) {
117
+ _savedOverflow = document.body.style.overflow;
118
+ document.body.style.overflow = 'hidden';
119
+ }
120
+ _overflowDepth += 1;
99
121
  }
100
122
 
101
- export function clearBackgroundInert() {
102
- for (const el of _inertSiblings) {
103
- el.removeAttribute('inert');
123
+ export function clearBackgroundInert(except) {
124
+ let idx = -1;
125
+ if (except instanceof HTMLElement) {
126
+ idx = _inertStack.findLastIndex((layer) => layer.host === except);
127
+ } else if (_inertStack.length) {
128
+ idx = _inertStack.length - 1;
129
+ }
130
+ if (idx === -1) return;
131
+
132
+ const { siblings } = _inertStack.splice(idx, 1)[0];
133
+ const stillNeeded = activeInertElements();
134
+ for (const el of siblings) {
135
+ if (!stillNeeded.has(el)) el.removeAttribute('inert');
136
+ }
137
+
138
+ _overflowDepth = Math.max(0, _overflowDepth - 1);
139
+ if (_overflowDepth === 0) {
140
+ document.body.style.overflow = _savedOverflow;
141
+ _savedOverflow = '';
104
142
  }
105
- _inertSiblings = [];
106
143
  }
@@ -12,22 +12,51 @@ export { default as VelinScrollTop } from './velin-scroll-top.js';
12
12
  export { default as VelinCarousel } from './velin-carousel.js';
13
13
  export { default as VelinCollapse } from './velin-collapse.js';
14
14
  export { default as VelinScrollspy } from './velin-scrollspy.js';
15
- export { default as VelinTooltipWC } from './velin-tooltip-wc.js';
15
+ export { default as VelinTooltip, VelinTooltipWC } from './velin-tooltip.js';
16
16
  export { default as VelinLightbox } from './velin-lightbox.js';
17
- export { default as VelinStepperWC } from './velin-stepper-wc.js';
17
+ export { default as VelinStepper, VelinStepperWC } from './velin-stepper.js';
18
18
  export { default as VelinDialog } from './velin-dialog.js';
19
19
  export { default as VelinCountdown } from './velin-countdown.js';
20
20
  export { default as VelinProgressRing } from './velin-progress-ring.js';
21
21
  export { default as VelinPersist } from './velin-persist.js';
22
+ export { default as VelinCombobox } from './velin-combobox.js';
23
+ export { default as VelinBottomNav } from './velin-bottom-nav.js';
24
+ export { default as VelinSheet } from './velin-sheet.js';
25
+ export { default as VelinSegmentedControl } from './velin-segmented-control.js';
26
+ export { default as VelinRating } from './velin-rating.js';
27
+ export { default as VelinMenubar } from './velin-menubar.js';
28
+ export { default as VelinCommand } from './velin-command.js';
29
+ export { default as VelinAnnouncer } from './velin-announcer.js';
30
+ export { default as VelinEmail } from './velin-email.js';
31
+ export { register, lazyDefine, whenDefined, bootFromDOM } from './runtime/index.js';
32
+ export { default as VelinSparkline } from './velin-sparkline.js';
33
+ export { default as VelinCounter } from './velin-counter.js';
34
+ export { default as VelinLiveDot } from './velin-live-dot.js';
35
+ export { default as VelinCodeBlock } from './velin-code-block.js';
36
+ export { default as VelinSearch } from './velin-search.js';
37
+ export { bindDeclarativeSearch } from './velin-search.js';
38
+ export { initReveal, initMotion, velinMotion } from './velin-reveal.js';
39
+ export { initA11y, announce, getAnnouncer } from './a11y-entry.js';
40
+ export { velinSearch, createSearch, registerSearchProvider } from '../core/search/index.js';
41
+ export { bootAttributes, registerAttribute, listRegisteredAttributes } from '../core/attributes/index.js';
42
+ export {
43
+ velinSyntax,
44
+ highlightElement,
45
+ highlightAll,
46
+ initHighlight,
47
+ observeCodeBlocks,
48
+ registerLanguage,
49
+ lazyLoadLanguage,
50
+ renderTokens,
51
+ } from '../core/highlight/index.js';
52
+ export { flipReorder, filterList } from './velin-flip.js';
53
+ export { escapeHTML, escapeHTMLAttribute, sanitizeURL, stripControlChars, createSafeHTML, getTrustedPolicy } from './sanitize.js';
22
54
  export { VelinHapticObserver, vibrate, applyHaptic, PATTERNS as HapticPatterns } from './velin-haptic.js';
23
55
  export { trapFocus, rovingTabindex, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
24
56
 
25
- import { VelinHapticObserver } from './velin-haptic.js';
26
- if (typeof document !== 'undefined') {
27
- const _hapticInit = () => { new VelinHapticObserver().start(document.body); };
28
- if (document.readyState === 'loading') {
29
- document.addEventListener('DOMContentLoaded', _hapticInit);
30
- } else {
31
- _hapticInit();
32
- }
33
- }
57
+ import './velin-sparkline.js';
58
+ import './velin-counter.js';
59
+ import './velin-live-dot.js';
60
+ import './velin-reveal.js';
61
+ import './velin-flip.js';
62
+ import './velin-email.js';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * AUTO-GENERATED — run: node scripts/generate-component-loaders.mjs
3
+ */
4
+ export const COMPONENT_LOADERS = {
5
+ 'velin-accordion': () => import('../velin-accordion.js'),
6
+ 'velin-announcer': () => import('../velin-announcer.js'),
7
+ 'velin-bottom-nav': () => import('../velin-bottom-nav.js'),
8
+ 'velin-carousel': () => import('../velin-carousel.js'),
9
+ 'velin-code-block': () => import('../velin-code-block.js'),
10
+ 'velin-collapse': () => import('../velin-collapse.js'),
11
+ 'velin-combobox': () => import('../velin-combobox.js'),
12
+ 'velin-command': () => import('../velin-command.js'),
13
+ 'velin-copy': () => import('../velin-copy.js'),
14
+ 'velin-countdown': () => import('../velin-countdown.js'),
15
+ 'velin-counter': () => import('../velin-counter.js'),
16
+ 'velin-dialog': () => import('../velin-dialog.js'),
17
+ 'velin-drawer': () => import('../velin-drawer.js'),
18
+ 'velin-dropdown': () => import('../velin-dropdown.js'),
19
+ 'velin-email': () => import('../velin-email.js'),
20
+ 'velin-icon': () => import('../velin-icon.js'),
21
+ 'velin-lightbox': () => import('../velin-lightbox.js'),
22
+ 'velin-live-dot': () => import('../velin-live-dot.js'),
23
+ 'velin-menubar': () => import('../velin-menubar.js'),
24
+ 'velin-modal': () => import('../velin-modal.js'),
25
+ 'velin-persist': () => import('../velin-persist.js'),
26
+ 'velin-popover': () => import('../velin-popover.js'),
27
+ 'velin-progress-ring': () => import('../velin-progress-ring.js'),
28
+ 'velin-rating': () => import('../velin-rating.js'),
29
+ 'velin-scroll-top': () => import('../velin-scroll-top.js'),
30
+ 'velin-scrollspy': () => import('../velin-scrollspy.js'),
31
+ 'velin-search': () => import('../velin-search.js'),
32
+ 'velin-secure-field': () => import('../velin-secure-field.js'),
33
+ 'velin-segmented-control': () => import('../velin-segmented-control.js'),
34
+ 'velin-sheet': () => import('../velin-sheet.js'),
35
+ 'velin-sparkline': () => import('../velin-sparkline.js'),
36
+ 'velin-stepper': () => import('../velin-stepper.js'),
37
+ 'velin-stepper-wc': () => import('../velin-stepper.js'),
38
+ 'velin-tabs': () => import('../velin-tabs.js'),
39
+ 'velin-theme-toggle': () => import('../velin-theme-toggle.js'),
40
+ 'velin-toast': () => import('../velin-toast.js'),
41
+ 'velin-tooltip': () => import('../velin-tooltip.js'),
42
+ 'velin-tooltip-wc': () => import('../velin-tooltip.js'),
43
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * VelinStyle runtime — register only the Web Components you need.
3
+ */
4
+ import { COMPONENT_LOADERS } from './component-loaders.js';
5
+
6
+ const registry = new Map();
7
+
8
+ const VELIN_TAG_RE = /^velin-[a-z0-9-]+$/;
9
+
10
+ export function whenDefined(tagName) {
11
+ if (typeof customElements === 'undefined') {
12
+ return Promise.reject(new Error('customElements not available'));
13
+ }
14
+ return customElements.whenDefined(tagName);
15
+ }
16
+
17
+ export async function lazyDefine(tagName) {
18
+ const loader = COMPONENT_LOADERS[tagName];
19
+ if (!loader) {
20
+ throw new Error(`Unknown Velin component: ${tagName}`);
21
+ }
22
+ if (registry.has(tagName)) return registry.get(tagName);
23
+ const p = loader().then((mod) => {
24
+ const Cls = mod.default;
25
+ if (Cls && !customElements.get(tagName)) {
26
+ customElements.define(tagName, Cls);
27
+ }
28
+ return Cls;
29
+ });
30
+ registry.set(tagName, p);
31
+ return p;
32
+ }
33
+
34
+ export async function register(tagNames) {
35
+ return Promise.all(tagNames.map((t) => lazyDefine(t)));
36
+ }
37
+
38
+ function collectTagsFromDOM(root) {
39
+ const tags = new Set();
40
+ root.querySelectorAll('[data-velin-component]').forEach((el) => {
41
+ const name = el.getAttribute('data-velin-component');
42
+ if (name) tags.add(name.startsWith('velin-') ? name : `velin-${name}`);
43
+ });
44
+ for (const el of root.querySelectorAll('*')) {
45
+ const tag = el.tagName?.toLowerCase();
46
+ if (tag && VELIN_TAG_RE.test(tag) && COMPONENT_LOADERS[tag]) {
47
+ tags.add(tag);
48
+ }
49
+ }
50
+ return tags;
51
+ }
52
+
53
+ /**
54
+ * @param {ParentNode} [root]
55
+ * @param {{ attributes?: boolean, highlight?: boolean, haptic?: boolean, tags?: string[] }} [options]
56
+ */
57
+ export async function bootFromDOM(root = document, options = {}) {
58
+ const tags = options.tags?.length
59
+ ? new Set(options.tags)
60
+ : collectTagsFromDOM(root);
61
+ const wcPromise = register([...tags]);
62
+ if (options.attributes) {
63
+ const { bootAttributes } = await import('../../core/attributes/index.js');
64
+ await bootAttributes(root);
65
+ }
66
+ if (options.highlight) {
67
+ const { initHighlight } = await import('../../core/highlight/index.js');
68
+ initHighlight(root);
69
+ }
70
+ if (options.haptic) {
71
+ const { VelinHapticObserver } = await import('../velin-haptic.js');
72
+ new VelinHapticObserver().start(root instanceof Document ? root.body : root);
73
+ }
74
+ return wcPromise;
75
+ }
76
+
77
+ export { COMPONENT_LOADERS };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tree-shakeable entry — no side-effect component registrations.
3
+ */
4
+ export {
5
+ register,
6
+ lazyDefine,
7
+ whenDefined,
8
+ bootFromDOM,
9
+ COMPONENT_LOADERS,
10
+ } from './runtime/index.js';
@@ -1,28 +1,135 @@
1
- const ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
2
- const ESCAPE_RE = /[&<>"']/g;
3
-
4
- export function escapeHTML(str) {
5
- if (typeof str !== 'string') return '';
6
- return str.replace(ESCAPE_RE, (ch) => ESCAPE_MAP[ch]);
7
- }
8
-
9
- export function sanitizeURL(url) {
10
- if (typeof url !== 'string') return '';
11
- try {
12
- const parsed = new URL(url, location.href);
13
- if (['http:', 'https:', 'data:'].includes(parsed.protocol)) return url;
14
- return '';
15
- } catch { return ''; }
16
- }
17
-
18
- let _policy = null;
19
-
20
- export function getTrustedPolicy() {
21
- if (_policy) return _policy;
22
- if (typeof window !== 'undefined' && window.trustedTypes?.createPolicy) {
23
- _policy = window.trustedTypes.createPolicy('velinstyle', {
24
- createHTML: (input) => escapeHTML(input),
25
- });
26
- }
27
- return _policy;
28
- }
1
+ const ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
2
+ const ESCAPE_RE = /[&<>"']/g;
3
+ const CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
4
+
5
+ const ALLOWED_URL_PROTOCOLS = new Set(['http:', 'https:', 'data:', 'mailto:', 'tel:']);
6
+ const BLOCKED_DATA_MIME = /^data:(?:text\/html|image\/svg\+xml|application\/xml)/i;
7
+ const ALLOWED_DATA_IMAGE = /^data:image\/(png|jpeg|jpg|gif|webp|avif);base64,/i;
8
+
9
+ const SECURE_INPUT_TYPES = new Set(['text', 'password', 'email', 'search', 'tel', 'url', 'number']);
10
+
11
+ export function escapeHTML(str) {
12
+ if (typeof str !== 'string') return '';
13
+ return str.replace(ESCAPE_RE, (ch) => ESCAPE_MAP[ch]);
14
+ }
15
+
16
+ export function stripControlChars(str) {
17
+ if (typeof str !== 'string') return '';
18
+ return str.replace(CONTROL_RE, '');
19
+ }
20
+
21
+ export function escapeHTMLAttribute(str) {
22
+ return escapeHTML(stripControlChars(str));
23
+ }
24
+
25
+ /**
26
+ * @param {string} url
27
+ * @param {string} [base]
28
+ */
29
+ export function sanitizeURL(url, base) {
30
+ if (typeof url !== 'string') return '';
31
+ const trimmed = url.trim();
32
+ if (!trimmed) return '';
33
+ if (/^\s*javascript:/i.test(trimmed) || /^\s*vbscript:/i.test(trimmed)) return '';
34
+ if (BLOCKED_DATA_MIME.test(trimmed)) return '';
35
+ if (/^data:/i.test(trimmed) && !ALLOWED_DATA_IMAGE.test(trimmed)) return '';
36
+ try {
37
+ const parsed = new URL(trimmed, base || (typeof location !== 'undefined' ? location.href : 'https://example.invalid/'));
38
+ if (!ALLOWED_URL_PROTOCOLS.has(parsed.protocol)) return '';
39
+ if (parsed.protocol === 'data:' && !ALLOWED_DATA_IMAGE.test(parsed.href)) return '';
40
+ return parsed.href;
41
+ } catch {
42
+ if (trimmed.startsWith('#') || trimmed.startsWith('/')) return trimmed;
43
+ return '';
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} url
49
+ * @param {string} [base]
50
+ */
51
+ export function sanitizeSearchUrl(url, base) {
52
+ if (typeof url !== 'string') return '#';
53
+ const trimmed = url.trim();
54
+ if (!trimmed) return '#';
55
+ if (trimmed.startsWith('#')) return trimmed;
56
+ if (trimmed.startsWith('/') && !trimmed.startsWith('//')) return trimmed;
57
+ // Index paths like getting-started/foo.html — must stay relative (resolved at navigation).
58
+ if (!/^https?:\/\//i.test(trimmed)) {
59
+ if (/^\s*javascript:/i.test(trimmed) || /^\s*vbscript:/i.test(trimmed)) return '#';
60
+ return trimmed;
61
+ }
62
+ const safe = sanitizeURL(trimmed, base);
63
+ return safe || '#';
64
+ }
65
+
66
+ export function sanitizeInputType(type) {
67
+ const t = String(type || 'text').toLowerCase();
68
+ return SECURE_INPUT_TYPES.has(t) ? t : 'text';
69
+ }
70
+
71
+ let _purify = null;
72
+
73
+ async function loadPurify() {
74
+ if (_purify) return _purify;
75
+ try {
76
+ const mod = await import('isomorphic-dompurify');
77
+ _purify = mod.default || mod;
78
+ } catch {
79
+ _purify = null;
80
+ }
81
+ return _purify;
82
+ }
83
+
84
+ const SVG_ALLOWED_TAGS = ['svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'defs', 'use', 'symbol', 'title', 'desc', 'clipPath', 'mask', 'linearGradient', 'radialGradient', 'stop', 'pattern', 'text', 'tspan'];
85
+ const SVG_ALLOWED_ATTR = ['viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'd', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'width', 'height', 'x1', 'y1', 'x2', 'y2', 'points', 'transform', 'opacity', 'class', 'id', 'href', 'xlink:href', 'aria-hidden', 'aria-label', 'aria-labelledby', 'role', 'focusable', 'clip-path', 'mask', 'offset', 'stop-color', 'stop-opacity', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'fx', 'fy', 'patternUnits', 'patternContentUnits', 'preserveAspectRatio'];
86
+
87
+ /**
88
+ * @param {string} svgMarkup
89
+ */
90
+ export async function sanitizeSVG(svgMarkup) {
91
+ if (typeof svgMarkup !== 'string') return '';
92
+ const DOMPurify = await loadPurify();
93
+ if (DOMPurify?.sanitize) {
94
+ return DOMPurify.sanitize(svgMarkup, {
95
+ USE_PROFILES: { svg: true, svgFilters: true },
96
+ ADD_TAGS: SVG_ALLOWED_TAGS,
97
+ ADD_ATTR: SVG_ALLOWED_ATTR,
98
+ FORBID_TAGS: ['script', 'foreignObject', 'iframe', 'object', 'embed'],
99
+ FORBID_ATTR: ['onload', 'onerror', 'onclick', 'onmouseover'],
100
+ });
101
+ }
102
+ return svgMarkup
103
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
104
+ .replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
105
+ .replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, '');
106
+ }
107
+
108
+ /**
109
+ * @param {string} html
110
+ */
111
+ export async function sanitizeHTML(html) {
112
+ if (typeof html !== 'string') return '';
113
+ const DOMPurify = await loadPurify();
114
+ if (DOMPurify?.sanitize) return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
115
+ return escapeHTML(html);
116
+ }
117
+
118
+ let _policy = null;
119
+
120
+ export function getTrustedPolicy() {
121
+ if (_policy) return _policy;
122
+ if (typeof window !== 'undefined' && window.trustedTypes?.createPolicy) {
123
+ _policy = window.trustedTypes.createPolicy('velinstyle', {
124
+ createHTML: (input) => escapeHTML(input),
125
+ });
126
+ }
127
+ return _policy;
128
+ }
129
+
130
+ export function createSafeHTML(str) {
131
+ const policy = getTrustedPolicy();
132
+ const safe = escapeHTML(stripControlChars(str));
133
+ if (policy?.createHTML) return policy.createHTML(safe);
134
+ return safe;
135
+ }
@@ -0,0 +1,18 @@
1
+ /** Shared shadow DOM focus + target-size styles for VelinStyle web components. */
2
+ export const SHADOW_A11Y_STYLES = `
3
+ :host { display: block; }
4
+ button, [role="button"] {
5
+ min-inline-size: 2.75rem;
6
+ min-block-size: 2.75rem;
7
+ cursor: pointer;
8
+ }
9
+ button:focus-visible, [role="button"]:focus-visible {
10
+ outline: 3px solid var(--velin-color-focus, #2563eb);
11
+ outline-offset: 2px;
12
+ }
13
+ @media (forced-colors: active) {
14
+ button, [role="button"] {
15
+ border: 1px solid ButtonText;
16
+ }
17
+ }
18
+ `;
@@ -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 {
@@ -0,0 +1,35 @@
1
+ const styles = `
2
+ :host { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
3
+ `;
4
+
5
+ class VelinAnnouncer extends HTMLElement {
6
+ connectedCallback() {
7
+ if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
8
+ const live = this.getAttribute('polite') === 'false' ? 'assertive' : 'polite';
9
+ this.shadowRoot.innerHTML =
10
+ '<style>' + styles + '</style>' +
11
+ '<div role="status" aria-live="' + live + '" aria-atomic="true" part="region"></div>';
12
+ this._region = this.shadowRoot.querySelector('[role="status"]');
13
+ }
14
+
15
+ announce(message, { assertive = false } = {}) {
16
+ if (!this._region) this.connectedCallback();
17
+ this._region.setAttribute('aria-live', assertive ? 'assertive' : 'polite');
18
+ this._region.textContent = '';
19
+ requestAnimationFrame(() => {
20
+ this._region.textContent = typeof message === 'string' ? message : '';
21
+ });
22
+ }
23
+
24
+ static announceGlobal(message, options) {
25
+ let el = document.querySelector('velin-announcer');
26
+ if (!el) {
27
+ el = document.createElement('velin-announcer');
28
+ document.body.appendChild(el);
29
+ }
30
+ el.announce(message, options);
31
+ }
32
+ }
33
+
34
+ customElements.define('velin-announcer', VelinAnnouncer);
35
+ export default VelinAnnouncer;
@@ -0,0 +1,108 @@
1
+ import { escapeHTML } from './sanitize.js';
2
+
3
+ const styles = `
4
+ :host { display: block; }
5
+ nav {
6
+ display: flex;
7
+ justify-content: space-around;
8
+ align-items: center;
9
+ padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
10
+ padding-block-end: max(var(--velin-space-2, 0.5rem), env(safe-area-inset-bottom, 0px));
11
+ background: var(--velin-color-surface-bright, #fff);
12
+ border-block-start: 1px solid var(--velin-color-border, #ddd);
13
+ }
14
+ ::slotted(a), ::slotted(button) {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ gap: var(--velin-space-1, 0.25rem);
19
+ min-inline-size: 2.75rem;
20
+ min-block-size: 2.75rem;
21
+ padding: var(--velin-space-2, 0.5rem);
22
+ font-size: var(--velin-text-xs, 0.75rem);
23
+ color: var(--velin-color-text-muted, #666);
24
+ text-decoration: none;
25
+ background: none;
26
+ border: none;
27
+ cursor: pointer;
28
+ }
29
+ ::slotted([current]) {
30
+ color: var(--velin-color-primary, #2563eb);
31
+ font-weight: var(--velin-weight-semibold, 600);
32
+ }
33
+ `;
34
+
35
+ class VelinBottomNav extends HTMLElement {
36
+ constructor() {
37
+ super();
38
+ this.attachShadow({ mode: 'open' });
39
+ this._onSlot = this._onSlot.bind(this);
40
+ }
41
+
42
+ connectedCallback() {
43
+ const label = escapeHTML(this.getAttribute('aria-label') || 'Bottom navigation');
44
+ this.shadowRoot.innerHTML = `
45
+ <style>${styles}</style>
46
+ <nav role="navigation" aria-label="${label}"><slot></slot></nav>
47
+ `;
48
+ const slot = this.shadowRoot.querySelector('slot');
49
+ slot.addEventListener('slotchange', this._onSlot);
50
+ this._onSlot();
51
+ }
52
+
53
+ _onSlot() {
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
+ });
74
+ }
75
+
76
+ _syncCurrent() {
77
+ const slot = this.shadowRoot?.querySelector('slot');
78
+ if (!slot) return;
79
+ const hostKey = this.getAttribute('current');
80
+ slot.assignedElements().forEach((el) => {
81
+ const active =
82
+ el.hasAttribute('current') ||
83
+ (hostKey && (el.dataset.nav === hostKey || el.getAttribute('data-nav') === hostKey));
84
+ if (active) {
85
+ el.setAttribute('current', '');
86
+ el.setAttribute('aria-current', 'page');
87
+ } else {
88
+ el.removeAttribute('current');
89
+ el.removeAttribute('aria-current');
90
+ }
91
+ });
92
+ }
93
+
94
+ static get observedAttributes() {
95
+ return ['aria-label', 'current'];
96
+ }
97
+
98
+ attributeChangedCallback(name) {
99
+ if (name === 'aria-label') {
100
+ const nav = this.shadowRoot?.querySelector('nav');
101
+ if (nav) nav.setAttribute('aria-label', escapeHTML(this.getAttribute('aria-label') || 'Bottom navigation'));
102
+ }
103
+ if (name === 'current') this._syncCurrent();
104
+ }
105
+ }
106
+
107
+ customElements.define('velin-bottom-nav', VelinBottomNav);
108
+ export default VelinBottomNav;
@@ -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>