@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
@@ -0,0 +1,83 @@
1
+ import { rovingTabindex } from './focus-manager.js';
2
+ import { escapeHTML } from './sanitize.js';
3
+ import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
4
+
5
+ const styles = `
6
+ ${SHADOW_A11Y_STYLES}
7
+ :host { display: block; }
8
+ .menubar {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ gap: var(--velin-space-1, 0.25rem);
12
+ padding: var(--velin-space-2, 0.5rem);
13
+ background: var(--velin-color-surface-bright, #fff);
14
+ border-bottom: 1px solid var(--velin-color-border, #ddd);
15
+ }
16
+ ::slotted([role="menuitem"]) {
17
+ min-inline-size: 2.75rem;
18
+ min-block-size: 2.75rem;
19
+ padding: var(--velin-space-2, 0.5rem) var(--velin-space-4, 1rem);
20
+ background: none;
21
+ border: none;
22
+ border-radius: var(--velin-radius-sm, 0.25rem);
23
+ cursor: pointer;
24
+ font-size: var(--velin-text-base, 1rem);
25
+ color: var(--velin-color-text, #111);
26
+ }
27
+ ::slotted([role="menuitem"]:hover) {
28
+ background: var(--velin-color-surface-dim, #eee);
29
+ }
30
+ `;
31
+
32
+ class VelinMenubar extends HTMLElement {
33
+ constructor() {
34
+ super();
35
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
36
+ this._onKey = this._onKey.bind(this);
37
+ }
38
+
39
+ connectedCallback() {
40
+ const label = escapeHTML(this.getAttribute('aria-label') || 'Menu bar');
41
+ this.shadowRoot.innerHTML = `
42
+ <style>${styles}</style>
43
+ <div class="menubar" role="menubar" aria-label="${label}"><slot></slot></div>
44
+ `;
45
+ this.addEventListener('keydown', this._onKey);
46
+ this.shadowRoot.querySelector('slot')?.addEventListener('slotchange', () => this._init());
47
+ this._init();
48
+ }
49
+
50
+ _getItems() {
51
+ const slot = this.shadowRoot.querySelector('slot');
52
+ return slot ? slot.assignedElements().filter((el) => !el.hasAttribute('disabled')) : [];
53
+ }
54
+
55
+ _init() {
56
+ const items = this._getItems();
57
+ items.forEach((el, i) => {
58
+ if (!el.hasAttribute('role')) el.setAttribute('role', 'menuitem');
59
+ el.setAttribute('tabindex', i === 0 ? '0' : '-1');
60
+ });
61
+ }
62
+
63
+ _onKey(e) {
64
+ const items = this._getItems();
65
+ if (items.includes(e.target)) rovingTabindex(this, items, e);
66
+ }
67
+
68
+ static get observedAttributes() { return ['aria-label']; }
69
+
70
+ attributeChangedCallback(name) {
71
+ if (name === 'aria-label') {
72
+ const bar = this.shadowRoot?.querySelector('.menubar');
73
+ if (bar) bar.setAttribute('aria-label', escapeHTML(this.getAttribute('aria-label') || 'Menu bar'));
74
+ }
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ this.removeEventListener('keydown', this._onKey);
79
+ }
80
+ }
81
+
82
+ customElements.define('velin-menubar', VelinMenubar);
83
+ export default VelinMenubar;
@@ -98,14 +98,17 @@ class VelinModal extends HTMLElement {
98
98
  }
99
99
 
100
100
  connectedCallback() {
101
- const title = this.getAttribute('title') || '';
101
+ const title = (this.getAttribute('title') || '').trim();
102
102
  const safeTitle = escapeHTML(title);
103
+ const dialogLabel = title
104
+ ? 'aria-labelledby="velin-modal-title"'
105
+ : `aria-label="${escapeHTML(this.getAttribute('aria-label') || 'Dialog')}"`;
103
106
  this.shadowRoot.innerHTML = `
104
107
  <style>${styles}</style>
105
108
  <div class="overlay" part="overlay">
106
- <div class="dialog" role="dialog" aria-modal="true" aria-labelledby="velin-modal-title" part="dialog">
109
+ <div class="dialog" role="dialog" aria-modal="true" ${dialogLabel} part="dialog">
107
110
  <div class="header" part="header">
108
- <h2 class="title" id="velin-modal-title">${safeTitle}</h2>
111
+ <h2 class="title" id="velin-modal-title"${title ? '' : ' class="velin-sr-only"'}>${safeTitle || 'Dialog'}</h2>
109
112
  <button class="close-btn" aria-label="Close" part="close">&#215;</button>
110
113
  </div>
111
114
  <div class="body" part="body"><slot></slot></div>
@@ -50,24 +50,52 @@ class VelinPopover extends HTMLElement {
50
50
  const role = triggerType === 'hover' ? 'tooltip' : 'dialog';
51
51
  this._isDialog = role === 'dialog';
52
52
 
53
+ const titleId = title ? `${this._popoverId}-title` : '';
53
54
  this.shadowRoot.innerHTML = `
54
55
  <style>${styles}</style>
55
56
  <slot name="trigger"></slot>
56
- <div class="popover popover--${placement}" id="${this._popoverId}" role="${role}" part="popover">
57
- ${title ? `<div class="popover__title" part="title">${escapeHTML(title)}</div>` : ''}
57
+ <div class="popover popover--${placement}" id="${this._popoverId}" role="${role}" part="popover"${titleId ? ` aria-labelledby="${titleId}"` : ''}>
58
+ ${title ? `<div class="popover__title" id="${titleId}" part="title">${escapeHTML(title)}</div>` : ''}
58
59
  <slot></slot>
59
60
  </div>
60
61
  `;
62
+ const popoverEl = this.shadowRoot.querySelector('.popover');
63
+ if (!title && popoverEl) {
64
+ const fallback =
65
+ this.getAttribute('aria-label') ||
66
+ (role === 'tooltip' ? 'Tooltip' : 'Popover');
67
+ popoverEl.setAttribute('aria-label', fallback);
68
+ }
61
69
 
62
70
  const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
63
71
  triggerSlot.addEventListener('slotchange', () => this._wireTrigger(triggerType));
64
72
  this._wireTrigger(triggerType);
65
73
  }
66
74
 
75
+ _ensureTriggerInteractive(trigger) {
76
+ const tag = trigger.tagName;
77
+ if (tag !== 'BUTTON' && tag !== 'A' && trigger.getAttribute('role') !== 'button') {
78
+ trigger.setAttribute('role', 'button');
79
+ if (!trigger.hasAttribute('tabindex')) trigger.setAttribute('tabindex', '0');
80
+ }
81
+ }
82
+
83
+ _onTriggerKey(e) {
84
+ if (e.key === 'Escape') {
85
+ this.close();
86
+ return;
87
+ }
88
+ if (e.key === 'Enter' || e.key === ' ') {
89
+ e.preventDefault();
90
+ this.toggle();
91
+ }
92
+ }
93
+
67
94
  _wireTrigger(triggerType) {
68
95
  const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
69
96
  if (!trigger) return;
70
97
 
98
+ this._ensureTriggerInteractive(trigger);
71
99
  const isHover = triggerType === 'hover';
72
100
  trigger.setAttribute('aria-haspopup', isHover ? 'true' : 'dialog');
73
101
  trigger.setAttribute('aria-expanded', this.hasAttribute('open') ? 'true' : 'false');
@@ -75,6 +103,10 @@ class VelinPopover extends HTMLElement {
75
103
  trigger.setAttribute('aria-controls', this._popoverId);
76
104
  }
77
105
 
106
+ trigger.removeEventListener('keydown', this._onTriggerKeyBound);
107
+ this._onTriggerKeyBound = this._onTriggerKeyBound || this._onTriggerKey.bind(this);
108
+ trigger.addEventListener('keydown', this._onTriggerKeyBound);
109
+
78
110
  if (triggerType === 'click') {
79
111
  trigger.onclick = () => this.toggle();
80
112
  } else if (triggerType === 'hover') {
@@ -0,0 +1,91 @@
1
+ import { rovingTabindex } from './focus-manager.js';
2
+ import { escapeHTML } from './sanitize.js';
3
+ import { SHADOW_A11Y_STYLES } from './shadow-a11y-styles.js';
4
+
5
+ const styles = `
6
+ ${SHADOW_A11Y_STYLES}
7
+ :host { display: inline-block; }
8
+ .stars { display: inline-flex; gap: var(--velin-space-1, 0.25rem); }
9
+ button {
10
+ background: none; border: none; padding: var(--velin-space-1, 0.25rem);
11
+ font-size: 1.5rem; line-height: 1; cursor: pointer;
12
+ color: var(--velin-color-border, #ccc);
13
+ }
14
+ button[aria-checked="true"] { color: var(--velin-color-warning, #f59e0b); }
15
+ `;
16
+
17
+ const MAX = 5;
18
+
19
+ class VelinRating extends HTMLElement {
20
+ static get observedAttributes() { return ['value']; }
21
+
22
+ constructor() {
23
+ super();
24
+ this.attachShadow({ mode: 'open', delegatesFocus: true });
25
+ this._onClick = this._onClick.bind(this);
26
+ this._onKey = this._onKey.bind(this);
27
+ }
28
+
29
+ connectedCallback() {
30
+ this.shadowRoot.innerHTML = `<style>${styles}</style><div class="stars" role="radiogroup"></div>`;
31
+ this._render();
32
+ this.shadowRoot.querySelector('.stars').addEventListener('click', this._onClick);
33
+ this.shadowRoot.querySelector('.stars').addEventListener('keydown', this._onKey);
34
+ }
35
+
36
+ _value() {
37
+ const v = parseInt(this.getAttribute('value') || '0', 10);
38
+ return Math.min(MAX, Math.max(0, Number.isNaN(v) ? 0 : v));
39
+ }
40
+
41
+ _render() {
42
+ const group = this.shadowRoot.querySelector('.stars');
43
+ const val = this._value();
44
+ const label = escapeHTML(this.getAttribute('aria-label') || 'Rating');
45
+ group.setAttribute('aria-label', label);
46
+ group.innerHTML = '';
47
+ for (let i = 1; i <= MAX; i++) {
48
+ const btn = document.createElement('button');
49
+ btn.type = 'button';
50
+ btn.setAttribute('role', 'radio');
51
+ btn.setAttribute('aria-checked', i <= val ? 'true' : 'false');
52
+ btn.setAttribute('aria-label', escapeHTML(`${i} star${i > 1 ? 's' : ''}`));
53
+ btn.setAttribute('tabindex', i === (val || 1) ? '0' : '-1');
54
+ btn.dataset.value = String(i);
55
+ btn.textContent = i <= val ? '\u2605' : '\u2606';
56
+ group.appendChild(btn);
57
+ }
58
+ }
59
+
60
+ _getButtons() {
61
+ return [...this.shadowRoot.querySelectorAll('button[role="radio"]')];
62
+ }
63
+
64
+ _onClick(e) {
65
+ const btn = e.target.closest('button');
66
+ if (!btn) return;
67
+ this._setValue(parseInt(btn.dataset.value, 10));
68
+ }
69
+
70
+ _onKey(e) {
71
+ const buttons = this._getButtons();
72
+ if (!buttons.includes(e.target)) return;
73
+ rovingTabindex(this, buttons, e);
74
+ if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
75
+ const focused = buttons.find((b) => b.getAttribute('tabindex') === '0');
76
+ if (focused) this._setValue(parseInt(focused.dataset.value, 10));
77
+ }
78
+ }
79
+
80
+ _setValue(n) {
81
+ this.setAttribute('value', String(n));
82
+ this.dispatchEvent(new CustomEvent('velin-change', { bubbles: true, detail: { value: n } }));
83
+ }
84
+
85
+ attributeChangedCallback(name) {
86
+ if (name === 'value' && this.shadowRoot?.querySelector('.stars')) this._render();
87
+ }
88
+ }
89
+
90
+ customElements.define('velin-rating', VelinRating);
91
+ export default VelinRating;
@@ -0,0 +1,22 @@
1
+ /*
2
+ * velin-reveal.js — scroll-into-view reveal (delegates to core/motion).
3
+ */
4
+ import { initMotion, initReveal as motionInitReveal, velinMotion } from '../core/motion/index.js';
5
+
6
+ export { initMotion, velinMotion };
7
+ export const initReveal = motionInitReveal;
8
+
9
+ if (typeof document !== 'undefined') {
10
+ const autoInit = () => {
11
+ if (document.documentElement?.hasAttribute('data-velin-reveal-auto')) {
12
+ initMotion();
13
+ }
14
+ };
15
+ if (document.readyState === 'loading') {
16
+ document.addEventListener('DOMContentLoaded', autoInit, { once: true });
17
+ } else {
18
+ autoInit();
19
+ }
20
+ }
21
+
22
+ export default { initReveal, initMotion };
@@ -30,7 +30,7 @@ class VelinScrollTop extends HTMLElement {
30
30
  this.shadowRoot.innerHTML = `
31
31
  <style>${styles}</style>
32
32
  <button part="button" aria-label="Scroll to top">
33
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
33
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
34
34
  <line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>
35
35
  </svg>
36
36
  </button>
@@ -0,0 +1,267 @@
1
+ import { createSearch, highlightHtml, SEARCH_CATEGORIES, resolveDocsSearchUrl } from '../core/search/index.js';
2
+ import { escapeHTMLAttribute } from './sanitize.js';
3
+
4
+ const CATEGORY_LABELS = {
5
+ docs: 'Documentation',
6
+ components: 'Components',
7
+ api: 'API',
8
+ examples: 'Examples',
9
+ };
10
+
11
+ class VelinSearch extends HTMLElement {
12
+ static get observedAttributes() {
13
+ return ['index', 'categories', 'min-chars', 'fuzzy', 'placeholder', 'debounce'];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this._search = createSearch();
19
+ this._debounceTimer = null;
20
+ this._activeIndex = -1;
21
+ this._flatResults = [];
22
+ this._indexLoaded = false;
23
+ this._listId = `velin-search-listbox-${Math.random().toString(36).slice(2, 9)}`;
24
+ this._mounted = false;
25
+ }
26
+
27
+ connectedCallback() {
28
+ if (this._mounted) return;
29
+ this._mounted = true;
30
+ this._ensureMarkup();
31
+ this._loadIndex();
32
+ this._bindEvents();
33
+ }
34
+
35
+ disconnectedCallback() {
36
+ this._teardown?.();
37
+ this._mounted = false;
38
+ }
39
+
40
+ get indexUrl() {
41
+ return this.getAttribute('index') || '/search-index.json';
42
+ }
43
+
44
+ get minChars() {
45
+ return parseInt(this.getAttribute('min-chars') || '2', 10);
46
+ }
47
+
48
+ get fuzzy() {
49
+ const v = this.getAttribute('fuzzy');
50
+ return v === null ? 0.2 : parseFloat(v) || 0;
51
+ }
52
+
53
+ get categories() {
54
+ const raw = this.getAttribute('categories');
55
+ if (!raw) return null;
56
+ return raw.split(',').map((c) => c.trim()).filter((c) => SEARCH_CATEGORIES.includes(c));
57
+ }
58
+
59
+ get debounceMs() {
60
+ return parseInt(this.getAttribute('debounce') || '120', 10);
61
+ }
62
+
63
+ _ensureMarkup() {
64
+ if (this.querySelector('[data-velin-search-input]')) return;
65
+ const ph = escapeHTMLAttribute(this.getAttribute('placeholder') || 'Search…');
66
+ const label = escapeHTMLAttribute(this.getAttribute('aria-label') || 'Search');
67
+ this.innerHTML = `
68
+ <div class="velin-search">
69
+ <div class="velin-search__field">
70
+ <input type="search" class="velin-search__input" data-velin-search-input
71
+ placeholder="${ph}" autocomplete="off" role="combobox" aria-expanded="false"
72
+ aria-label="${label}" aria-controls="${this._listId}" aria-autocomplete="list" />
73
+ </div>
74
+ <div class="velin-search__status velin-visually-hidden" data-velin-search-status aria-live="polite" aria-atomic="true"></div>
75
+ <div class="velin-search__results" data-velin-search-results role="listbox"
76
+ id="${this._listId}" hidden></div>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ _inputEl() {
82
+ return this.querySelector('[data-velin-search-input]');
83
+ }
84
+
85
+ _resultsEl() {
86
+ return this.querySelector('[data-velin-search-results]');
87
+ }
88
+
89
+ async _loadIndex() {
90
+ try {
91
+ await this._search.loadIndex(this.indexUrl);
92
+ this._indexLoaded = true;
93
+ } catch {
94
+ this._indexLoaded = false;
95
+ }
96
+ }
97
+
98
+ _bindEvents() {
99
+ const input = this._inputEl();
100
+ const panel = this._resultsEl();
101
+ if (!input || !panel) return;
102
+
103
+ input.addEventListener('input', () => {
104
+ clearTimeout(this._debounceTimer);
105
+ this._debounceTimer = setTimeout(() => this._runQuery(input.value), this.debounceMs);
106
+ });
107
+ input.addEventListener('focus', () => {
108
+ if (input.value.trim().length >= this.minChars) this._runQuery(input.value);
109
+ });
110
+ input.addEventListener('keydown', (e) => this._onKeydown(e, input, panel));
111
+
112
+ const onDocClick = (e) => {
113
+ if (!this.contains(e.target)) this._hide(panel, input);
114
+ };
115
+ document.addEventListener('click', onDocClick);
116
+ this._teardown = () => {
117
+ document.removeEventListener('click', onDocClick);
118
+ clearTimeout(this._debounceTimer);
119
+ };
120
+ }
121
+
122
+ async _runQuery(raw) {
123
+ const input = this._inputEl();
124
+ const panel = this._resultsEl();
125
+ if (!input || !panel) return;
126
+ const q = raw.trim();
127
+ if (q.length < this.minChars) {
128
+ this._hide(panel, input);
129
+ return;
130
+ }
131
+ if (!this._indexLoaded) await this._loadIndex();
132
+ const { groups } = this._search.query(q, {
133
+ minChars: this.minChars,
134
+ fuzzy: this.fuzzy,
135
+ categories: this.categories || undefined,
136
+ limit: 12,
137
+ });
138
+ this._activeIndex = -1;
139
+ this._renderResults(panel, input, q, groups);
140
+ if (this._flatResults.length) {
141
+ panel.hidden = false;
142
+ panel.classList.add('velin-search__results--open');
143
+ input.setAttribute('aria-expanded', 'true');
144
+ } else {
145
+ this._hide(panel, input);
146
+ }
147
+ }
148
+
149
+ _renderResults(panel, input, q, groups) {
150
+ panel.innerHTML = '';
151
+ const order = this.categories || [...SEARCH_CATEGORIES];
152
+ this._flatResults = [];
153
+ let globalIdx = 0;
154
+ for (const cat of order) {
155
+ const items = groups[cat];
156
+ if (!items?.length) continue;
157
+ const groupId = `${this._listId}-group-${cat}`;
158
+ const group = document.createElement('div');
159
+ group.setAttribute('role', 'group');
160
+ group.setAttribute('aria-labelledby', groupId);
161
+ const heading = document.createElement('div');
162
+ heading.className = 'velin-search__group-label';
163
+ heading.id = groupId;
164
+ heading.textContent = CATEGORY_LABELS[cat] || cat;
165
+ group.appendChild(heading);
166
+ for (const item of items) {
167
+ const a = document.createElement('a');
168
+ const navHref = resolveDocsSearchUrl(item.url);
169
+ if (navHref) a.href = navHref;
170
+ a.className = 'velin-search__item';
171
+ a.setAttribute('role', 'option');
172
+ a.id = `velin-search-opt-${globalIdx}`;
173
+ a.innerHTML =
174
+ `<span class="velin-search__title">${highlightHtml(item.title, q)}</span>` +
175
+ `<span class="velin-search__excerpt">${highlightHtml(item.excerpt || '', q)}</span>`;
176
+ a.addEventListener(
177
+ 'click',
178
+ (e) => {
179
+ e.preventDefault();
180
+ if (navHref) window.location.assign(navHref);
181
+ },
182
+ true,
183
+ );
184
+ a.addEventListener('mouseenter', () => this._setActive(globalIdx, panel, input));
185
+ group.appendChild(a);
186
+ this._flatResults.push(item);
187
+ globalIdx++;
188
+ }
189
+ panel.appendChild(group);
190
+ }
191
+ const status = this._statusEl();
192
+ if (status) {
193
+ status.textContent = this._flatResults.length
194
+ ? `${this._flatResults.length} results`
195
+ : 'No results';
196
+ }
197
+ }
198
+
199
+ _statusEl() {
200
+ return this.querySelector('[data-velin-search-status]');
201
+ }
202
+
203
+ _setActive(idx, panel, input) {
204
+ this._activeIndex = idx;
205
+ panel.querySelectorAll('.velin-search__item').forEach((el, i) => {
206
+ el.classList.toggle('velin-search__item--active', i === idx);
207
+ if (i === idx) {
208
+ input.setAttribute('aria-activedescendant', el.id);
209
+ el.scrollIntoView({ block: 'nearest' });
210
+ }
211
+ });
212
+ }
213
+
214
+ _onKeydown(e, input, panel) {
215
+ if (e.key === 'Escape') {
216
+ this._hide(panel, input);
217
+ return;
218
+ }
219
+ if (!this._flatResults.length) return;
220
+ if (e.key === 'ArrowDown') {
221
+ e.preventDefault();
222
+ const next = this._activeIndex < this._flatResults.length - 1 ? this._activeIndex + 1 : 0;
223
+ this._setActive(next, panel, input);
224
+ } else if (e.key === 'ArrowUp') {
225
+ e.preventDefault();
226
+ const prev = this._activeIndex > 0 ? this._activeIndex - 1 : this._flatResults.length - 1;
227
+ this._setActive(prev, panel, input);
228
+ } else if (e.key === 'Enter') {
229
+ const target = this._activeIndex >= 0 ? this._flatResults[this._activeIndex] : this._flatResults[0];
230
+ const navHref = target?.url ? resolveDocsSearchUrl(target.url) : '';
231
+ if (navHref) {
232
+ e.preventDefault();
233
+ window.location.assign(navHref);
234
+ }
235
+ }
236
+ }
237
+
238
+ _hide(panel, input) {
239
+ panel.hidden = true;
240
+ panel.classList.remove('velin-search__results--open');
241
+ input.setAttribute('aria-expanded', 'false');
242
+ input.removeAttribute('aria-activedescendant');
243
+ this._activeIndex = -1;
244
+ }
245
+ }
246
+
247
+ export default VelinSearch;
248
+
249
+ if (typeof HTMLElement !== 'undefined' && typeof customElements !== 'undefined' && !customElements.get('velin-search')) {
250
+ customElements.define('velin-search', VelinSearch);
251
+ }
252
+
253
+ export function bindDeclarativeSearch(root = document) {
254
+ root.querySelectorAll('[velin-search-input]').forEach((input) => {
255
+ if (input.dataset.velinSearchBound) return;
256
+ input.dataset.velinSearchBound = '1';
257
+ let host = input.closest('velin-search');
258
+ if (!host) {
259
+ host = document.createElement('velin-search');
260
+ host.setAttribute('index', input.getAttribute('data-search-index') || '/search-index.json');
261
+ const results = input.parentElement?.querySelector('[velin-search-results]');
262
+ input.before(host);
263
+ host.appendChild(input);
264
+ if (results) host.appendChild(results);
265
+ }
266
+ });
267
+ }
@@ -0,0 +1,99 @@
1
+ import { escapeHTML, escapeHTMLAttribute, sanitizeInputType } from './sanitize.js';
2
+
3
+ const styles = `
4
+ :host { display: block; }
5
+ label { display: block; font-size: var(--velin-text-sm); margin-block-end: var(--velin-space-1); }
6
+ input {
7
+ width: 100%;
8
+ min-height: 2.75rem;
9
+ padding: var(--velin-space-2) var(--velin-space-3);
10
+ font: inherit;
11
+ border: 1px solid var(--velin-color-border);
12
+ border-radius: var(--velin-radius-md);
13
+ }
14
+ input:focus-visible {
15
+ outline: 3px solid var(--velin-color-focus);
16
+ outline-offset: 2px;
17
+ }
18
+ .velin-field-help {
19
+ font-size: var(--velin-text-xs);
20
+ color: var(--velin-color-text-muted);
21
+ margin-block-start: var(--velin-space-1);
22
+ }
23
+ `;
24
+
25
+ async function encryptValue(plain, mode) {
26
+ if (mode !== 'aes-gcm' || !globalThis.crypto?.subtle) {
27
+ return JSON.stringify({ encoding: 'base64', payload: btoa(unescape(encodeURIComponent(plain))) });
28
+ }
29
+ const iv = crypto.getRandomValues(new Uint8Array(12));
30
+ const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt']);
31
+ const encoded = new TextEncoder().encode(plain);
32
+ const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
33
+ return JSON.stringify({
34
+ encoding: 'aes-gcm',
35
+ iv: Array.from(iv),
36
+ cipher: Array.from(new Uint8Array(cipher)),
37
+ });
38
+ }
39
+
40
+ class VelinSecureField extends HTMLElement {
41
+ static get observedAttributes() {
42
+ return ['type', 'name', 'label', 'mode', 'autocomplete'];
43
+ }
44
+
45
+ constructor() {
46
+ super();
47
+ this.attachShadow({ mode: 'open' });
48
+ this._onChange = null;
49
+ }
50
+
51
+ connectedCallback() {
52
+ if (!this._onChange) this.render();
53
+ }
54
+
55
+ disconnectedCallback() {
56
+ const input = this.shadowRoot?.querySelector('input');
57
+ if (input && this._onChange) input.removeEventListener('change', this._onChange);
58
+ this._onChange = null;
59
+ }
60
+
61
+ attributeChangedCallback() {
62
+ if (this.isConnected) this.render();
63
+ }
64
+
65
+ render() {
66
+ const type = sanitizeInputType(this.getAttribute('type'));
67
+ const name = this.getAttribute('name') || 'secure';
68
+ const label = this.getAttribute('label') || 'Secure field';
69
+ const autocomplete = escapeHTMLAttribute(this.getAttribute('autocomplete') || 'off');
70
+ const id = `velin-secure-${Math.random().toString(36).slice(2, 9)}`;
71
+
72
+ const input = this.shadowRoot?.querySelector('input');
73
+ if (input && this._onChange) input.removeEventListener('change', this._onChange);
74
+
75
+ this.shadowRoot.innerHTML = `
76
+ <style>${styles}</style>
77
+ <label for="${id}">${escapeHTML(label)}</label>
78
+ <input id="${id}" type="${type}" autocomplete="${autocomplete}" part="input" />
79
+ <p class="velin-field-help">Demo-only client encoding — use HTTPS and server-side crypto for real secrets.</p>
80
+ `;
81
+
82
+ const newInput = this.shadowRoot.querySelector('input');
83
+ this._onChange = async () => {
84
+ const mode = this.getAttribute('mode') || 'aes-gcm';
85
+ const payload = await encryptValue(newInput.value, mode);
86
+ this.dispatchEvent(new CustomEvent('velin-secure-submit', {
87
+ bubbles: true,
88
+ detail: { name, payload },
89
+ }));
90
+ };
91
+ newInput.addEventListener('change', this._onChange);
92
+ }
93
+ }
94
+
95
+ if (!customElements.get('velin-secure-field')) {
96
+ customElements.define('velin-secure-field', VelinSecureField);
97
+ }
98
+
99
+ export default VelinSecureField;