@yewdiel21/motion-kit 0.10.0 → 0.14.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 (365) hide show
  1. package/README.md +155 -93
  2. package/dist/auto/index.d.ts +53 -3
  3. package/dist/auto/index.js +101 -3
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
  6. package/dist/chunk-6P4P7NU6.js.map +1 -0
  7. package/dist/chunk-CU6SXW3E.js +20334 -0
  8. package/dist/chunk-CU6SXW3E.js.map +1 -0
  9. package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
  10. package/dist/chunk-EY4BJKC4.js.map +1 -0
  11. package/dist/chunk-QP4QJOEN.js +72 -0
  12. package/dist/chunk-QP4QJOEN.js.map +1 -0
  13. package/dist/css/components/activity-feed.css +240 -0
  14. package/dist/css/components/attachment.css +266 -0
  15. package/dist/css/components/breadcrumb.css +110 -0
  16. package/dist/css/components/chart-tooltip.css +168 -0
  17. package/dist/css/components/chart.css +296 -0
  18. package/dist/css/components/code-block.css +264 -0
  19. package/dist/css/components/color-area.css +82 -0
  20. package/dist/css/components/color-field.css +67 -0
  21. package/dist/css/components/color-picker.css +131 -0
  22. package/dist/css/components/color-slider.css +113 -0
  23. package/dist/css/components/command-search.css +252 -0
  24. package/dist/css/components/context-menu.css +23 -0
  25. package/dist/css/components/credit-card.css +288 -0
  26. package/dist/css/components/data-table.css +591 -0
  27. package/dist/css/components/date-range-picker.css +168 -0
  28. package/dist/css/components/date-time-picker.css +178 -0
  29. package/dist/css/components/drawer.css +58 -0
  30. package/dist/css/components/editable-table.css +201 -0
  31. package/dist/css/components/empty-state.css +61 -0
  32. package/dist/css/components/file-explorer.css +633 -0
  33. package/dist/css/components/filter-builder.css +232 -0
  34. package/dist/css/components/hold-confirm.css +87 -0
  35. package/dist/css/components/hover-card.css +13 -7
  36. package/dist/css/components/image-preview.css +195 -0
  37. package/dist/css/components/json-viewer.css +272 -0
  38. package/dist/css/components/like-button.css +78 -0
  39. package/dist/css/components/link-preview.css +275 -0
  40. package/dist/css/components/loading-button.css +77 -0
  41. package/dist/css/components/multi-select.css +267 -0
  42. package/dist/css/components/notification-bell.css +127 -0
  43. package/dist/css/components/password-strength.css +183 -0
  44. package/dist/css/components/popover.css +25 -0
  45. package/dist/css/components/progress.css +140 -0
  46. package/dist/css/components/questionnaire.css +217 -0
  47. package/dist/css/components/range-calendar.css +186 -0
  48. package/dist/css/components/range-slider.css +192 -0
  49. package/dist/css/components/resizable-panel.css +243 -0
  50. package/dist/css/components/rich-text-editor.css +284 -0
  51. package/dist/css/components/scratch-card.css +75 -0
  52. package/dist/css/components/skeleton.css +108 -0
  53. package/dist/css/components/split-view.css +19 -0
  54. package/dist/css/components/star-rating.css +80 -0
  55. package/dist/css/components/stat-card.css +164 -0
  56. package/dist/css/components/swipe-actions.css +105 -0
  57. package/dist/css/components/tag-input.css +244 -0
  58. package/dist/css/components/time-field.css +88 -0
  59. package/dist/css/components/timeline.css +219 -0
  60. package/dist/css/components/tooltip.css +79 -0
  61. package/dist/css/components/tree-view.css +210 -0
  62. package/dist/css/components/virtual-list.css +195 -0
  63. package/dist/css/tokens.css +185 -181
  64. package/dist/elements/index.d.ts +674 -5
  65. package/dist/elements/index.js +1361 -8
  66. package/dist/elements/index.js.map +1 -1
  67. package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
  68. package/dist/engine-QECQnGz1.d.ts +33 -0
  69. package/dist/index.css +9104 -189
  70. package/dist/index.d.ts +89 -5
  71. package/dist/index.js +243 -9
  72. package/dist/index.js.map +1 -1
  73. package/dist/react/index.d.ts +579 -23
  74. package/dist/react/index.js +2042 -7
  75. package/dist/react/index.js.map +1 -1
  76. package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
  77. package/dist/reveal-DEvp6U6T.d.ts +2381 -0
  78. package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
  79. package/package.json +1 -1
  80. package/registry.json +406 -1
  81. package/src/auto/auto-init.ts +98 -0
  82. package/src/components/activity-feed/README.md +56 -0
  83. package/src/components/activity-feed/activity-feed.css +239 -0
  84. package/src/components/activity-feed/element.ts +36 -0
  85. package/src/components/activity-feed/engine.ts +351 -0
  86. package/src/components/activity-feed/react.tsx +32 -0
  87. package/src/components/attachment/README.md +27 -0
  88. package/src/components/attachment/attachment.css +265 -0
  89. package/src/components/attachment/element.ts +50 -0
  90. package/src/components/attachment/engine.ts +469 -0
  91. package/src/components/attachment/react.tsx +65 -0
  92. package/src/components/breadcrumb/README.md +38 -0
  93. package/src/components/breadcrumb/breadcrumb.css +109 -0
  94. package/src/components/breadcrumb/element.ts +27 -0
  95. package/src/components/breadcrumb/engine.ts +249 -0
  96. package/src/components/breadcrumb/react.tsx +30 -0
  97. package/src/components/chart/README.md +24 -0
  98. package/src/components/chart/chart.css +295 -0
  99. package/src/components/chart/element.ts +41 -0
  100. package/src/components/chart/engine.ts +811 -0
  101. package/src/components/chart/react.tsx +50 -0
  102. package/src/components/chart-tooltip/README.md +53 -0
  103. package/src/components/chart-tooltip/chart-tooltip.css +167 -0
  104. package/src/components/chart-tooltip/element.ts +37 -0
  105. package/src/components/chart-tooltip/engine.ts +442 -0
  106. package/src/components/chart-tooltip/react.tsx +47 -0
  107. package/src/components/code-block/README.md +19 -0
  108. package/src/components/code-block/code-block.css +263 -0
  109. package/src/components/code-block/element.ts +34 -0
  110. package/src/components/code-block/engine.ts +254 -0
  111. package/src/components/code-block/highlight.ts +244 -0
  112. package/src/components/code-block/react.tsx +94 -0
  113. package/src/components/color-area/README.md +50 -0
  114. package/src/components/color-area/color-area.css +81 -0
  115. package/src/components/color-area/element.ts +26 -0
  116. package/src/components/color-area/engine.ts +246 -0
  117. package/src/components/color-area/react.tsx +42 -0
  118. package/src/components/color-field/README.md +43 -0
  119. package/src/components/color-field/color-field.css +66 -0
  120. package/src/components/color-field/element.ts +26 -0
  121. package/src/components/color-field/engine.ts +174 -0
  122. package/src/components/color-field/react.tsx +42 -0
  123. package/src/components/color-picker/README.md +45 -0
  124. package/src/components/color-picker/color-picker.css +130 -0
  125. package/src/components/color-picker/element.ts +45 -0
  126. package/src/components/color-picker/engine.ts +314 -0
  127. package/src/components/color-picker/react.tsx +47 -0
  128. package/src/components/color-slider/README.md +55 -0
  129. package/src/components/color-slider/color-slider.css +112 -0
  130. package/src/components/color-slider/element.ts +26 -0
  131. package/src/components/color-slider/engine.ts +234 -0
  132. package/src/components/color-slider/react.tsx +45 -0
  133. package/src/components/command-search/README.md +15 -0
  134. package/src/components/command-search/command-search.css +251 -0
  135. package/src/components/command-search/element.ts +36 -0
  136. package/src/components/command-search/engine.ts +356 -0
  137. package/src/components/command-search/match.ts +53 -0
  138. package/src/components/command-search/react.tsx +66 -0
  139. package/src/components/context-menu/README.md +31 -0
  140. package/src/components/context-menu/context-menu.css +22 -0
  141. package/src/components/context-menu/element.ts +40 -0
  142. package/src/components/context-menu/engine.ts +153 -0
  143. package/src/components/context-menu/react.tsx +57 -0
  144. package/src/components/credit-card/README.md +20 -0
  145. package/src/components/credit-card/card.ts +77 -0
  146. package/src/components/credit-card/credit-card.css +287 -0
  147. package/src/components/credit-card/element.ts +33 -0
  148. package/src/components/credit-card/engine.ts +410 -0
  149. package/src/components/credit-card/react.tsx +36 -0
  150. package/src/components/data-table/README.md +83 -0
  151. package/src/components/data-table/data-table.css +590 -0
  152. package/src/components/data-table/element.ts +54 -0
  153. package/src/components/data-table/engine.ts +1059 -0
  154. package/src/components/data-table/react.tsx +94 -0
  155. package/src/components/date-range-picker/README.md +51 -0
  156. package/src/components/date-range-picker/date-range-picker.css +167 -0
  157. package/src/components/date-range-picker/element.ts +52 -0
  158. package/src/components/date-range-picker/engine.ts +352 -0
  159. package/src/components/date-range-picker/react.tsx +57 -0
  160. package/src/components/date-time-picker/README.md +48 -0
  161. package/src/components/date-time-picker/date-time-picker.css +177 -0
  162. package/src/components/date-time-picker/element.ts +47 -0
  163. package/src/components/date-time-picker/engine.ts +350 -0
  164. package/src/components/date-time-picker/react.tsx +54 -0
  165. package/src/components/dialog/README.md +103 -102
  166. package/src/components/dialog/element.ts +123 -118
  167. package/src/components/dialog/engine.ts +449 -432
  168. package/src/components/drawer/README.md +28 -0
  169. package/src/components/drawer/drawer.css +57 -0
  170. package/src/components/drawer/element.ts +21 -0
  171. package/src/components/drawer/engine.ts +56 -0
  172. package/src/components/drawer/react.tsx +85 -0
  173. package/src/components/editable-table/README.md +67 -0
  174. package/src/components/editable-table/editable-table.css +200 -0
  175. package/src/components/editable-table/element.ts +45 -0
  176. package/src/components/editable-table/engine.ts +775 -0
  177. package/src/components/editable-table/react.tsx +79 -0
  178. package/src/components/empty-state/README.md +41 -0
  179. package/src/components/empty-state/element.ts +23 -0
  180. package/src/components/empty-state/empty-state.css +60 -0
  181. package/src/components/empty-state/engine.ts +232 -0
  182. package/src/components/empty-state/react.tsx +34 -0
  183. package/src/components/file-explorer/README.md +67 -0
  184. package/src/components/file-explorer/element.ts +39 -0
  185. package/src/components/file-explorer/engine.ts +1307 -0
  186. package/src/components/file-explorer/file-explorer.css +632 -0
  187. package/src/components/file-explorer/react.tsx +77 -0
  188. package/src/components/filter-builder/README.md +47 -0
  189. package/src/components/filter-builder/element.ts +34 -0
  190. package/src/components/filter-builder/engine.ts +365 -0
  191. package/src/components/filter-builder/filter-builder.css +231 -0
  192. package/src/components/filter-builder/react.tsx +36 -0
  193. package/src/components/hold-confirm/README.md +13 -0
  194. package/src/components/hold-confirm/element.ts +24 -0
  195. package/src/components/hold-confirm/engine.ts +216 -0
  196. package/src/components/hold-confirm/hold-confirm.css +86 -0
  197. package/src/components/hold-confirm/react.tsx +28 -0
  198. package/src/components/hover-card/README.md +6 -2
  199. package/src/components/hover-card/engine.ts +68 -6
  200. package/src/components/hover-card/hover-card.css +13 -7
  201. package/src/components/image-preview/README.md +16 -0
  202. package/src/components/image-preview/element.ts +26 -0
  203. package/src/components/image-preview/engine.ts +325 -0
  204. package/src/components/image-preview/image-preview.css +194 -0
  205. package/src/components/image-preview/react.tsx +43 -0
  206. package/src/components/json-viewer/README.md +56 -0
  207. package/src/components/json-viewer/element.ts +37 -0
  208. package/src/components/json-viewer/engine.ts +440 -0
  209. package/src/components/json-viewer/json-viewer.css +271 -0
  210. package/src/components/json-viewer/react.tsx +56 -0
  211. package/src/components/like-button/README.md +13 -0
  212. package/src/components/like-button/element.ts +47 -0
  213. package/src/components/like-button/engine.ts +145 -0
  214. package/src/components/like-button/like-button.css +77 -0
  215. package/src/components/like-button/react.tsx +39 -0
  216. package/src/components/link-preview/README.md +19 -0
  217. package/src/components/link-preview/element.ts +29 -0
  218. package/src/components/link-preview/engine.ts +192 -0
  219. package/src/components/link-preview/link-preview.css +274 -0
  220. package/src/components/link-preview/react.tsx +101 -0
  221. package/src/components/loading-button/README.md +45 -0
  222. package/src/components/loading-button/element.ts +34 -0
  223. package/src/components/loading-button/engine.ts +201 -0
  224. package/src/components/loading-button/loading-button.css +76 -0
  225. package/src/components/loading-button/react.tsx +43 -0
  226. package/src/components/multi-select/README.md +48 -0
  227. package/src/components/multi-select/element.ts +28 -0
  228. package/src/components/multi-select/engine.ts +294 -0
  229. package/src/components/multi-select/multi-select.css +266 -0
  230. package/src/components/multi-select/react.tsx +39 -0
  231. package/src/components/notification-bell/README.md +14 -0
  232. package/src/components/notification-bell/element.ts +48 -0
  233. package/src/components/notification-bell/engine.ts +202 -0
  234. package/src/components/notification-bell/notification-bell.css +126 -0
  235. package/src/components/notification-bell/react.tsx +57 -0
  236. package/src/components/password-strength/README.md +17 -0
  237. package/src/components/password-strength/element.ts +22 -0
  238. package/src/components/password-strength/engine.ts +212 -0
  239. package/src/components/password-strength/password-strength.css +182 -0
  240. package/src/components/password-strength/react.tsx +36 -0
  241. package/src/components/password-strength/score.ts +62 -0
  242. package/src/components/popover/README.md +44 -0
  243. package/src/components/popover/element.ts +31 -0
  244. package/src/components/popover/engine.ts +114 -0
  245. package/src/components/popover/popover.css +24 -0
  246. package/src/components/popover/react.tsx +50 -0
  247. package/src/components/progress/README.md +17 -0
  248. package/src/components/progress/element.ts +30 -0
  249. package/src/components/progress/engine.ts +173 -0
  250. package/src/components/progress/progress.css +139 -0
  251. package/src/components/progress/react.tsx +35 -0
  252. package/src/components/questionnaire/README.md +32 -0
  253. package/src/components/questionnaire/element.ts +118 -0
  254. package/src/components/questionnaire/engine.ts +255 -0
  255. package/src/components/questionnaire/questionnaire.css +216 -0
  256. package/src/components/questionnaire/react.tsx +140 -0
  257. package/src/components/range-calendar/README.md +58 -0
  258. package/src/components/range-calendar/element.ts +42 -0
  259. package/src/components/range-calendar/engine.ts +390 -0
  260. package/src/components/range-calendar/range-calendar.css +185 -0
  261. package/src/components/range-calendar/react.tsx +56 -0
  262. package/src/components/range-slider/README.md +23 -0
  263. package/src/components/range-slider/element.ts +53 -0
  264. package/src/components/range-slider/engine.ts +316 -0
  265. package/src/components/range-slider/range-slider.css +191 -0
  266. package/src/components/range-slider/react.tsx +66 -0
  267. package/src/components/resizable-panel/README.md +61 -0
  268. package/src/components/resizable-panel/element.ts +36 -0
  269. package/src/components/resizable-panel/engine.ts +551 -0
  270. package/src/components/resizable-panel/react.tsx +74 -0
  271. package/src/components/resizable-panel/resizable-panel.css +242 -0
  272. package/src/components/rich-text-editor/README.md +57 -0
  273. package/src/components/rich-text-editor/element.ts +34 -0
  274. package/src/components/rich-text-editor/engine.ts +673 -0
  275. package/src/components/rich-text-editor/react.tsx +60 -0
  276. package/src/components/rich-text-editor/rich-text-editor.css +283 -0
  277. package/src/components/scratch-card/README.md +15 -0
  278. package/src/components/scratch-card/element.ts +30 -0
  279. package/src/components/scratch-card/engine.ts +257 -0
  280. package/src/components/scratch-card/react.tsx +40 -0
  281. package/src/components/scratch-card/scratch-card.css +74 -0
  282. package/src/components/skeleton/README.md +15 -0
  283. package/src/components/skeleton/element.ts +32 -0
  284. package/src/components/skeleton/engine.ts +104 -0
  285. package/src/components/skeleton/react.tsx +41 -0
  286. package/src/components/skeleton/skeleton.css +107 -0
  287. package/src/components/split-view/README.md +50 -0
  288. package/src/components/split-view/element.ts +34 -0
  289. package/src/components/split-view/engine.ts +114 -0
  290. package/src/components/split-view/react.tsx +63 -0
  291. package/src/components/split-view/split-view.css +18 -0
  292. package/src/components/star-rating/README.md +14 -0
  293. package/src/components/star-rating/element.ts +26 -0
  294. package/src/components/star-rating/engine.ts +217 -0
  295. package/src/components/star-rating/react.tsx +33 -0
  296. package/src/components/star-rating/star-rating.css +79 -0
  297. package/src/components/stat-card/README.md +42 -0
  298. package/src/components/stat-card/element.ts +40 -0
  299. package/src/components/stat-card/engine.ts +283 -0
  300. package/src/components/stat-card/react.tsx +36 -0
  301. package/src/components/stat-card/stat-card.css +163 -0
  302. package/src/components/swipe-actions/README.md +19 -0
  303. package/src/components/swipe-actions/element.ts +26 -0
  304. package/src/components/swipe-actions/engine.ts +260 -0
  305. package/src/components/swipe-actions/react.tsx +80 -0
  306. package/src/components/swipe-actions/swipe-actions.css +104 -0
  307. package/src/components/tag-input/README.md +42 -0
  308. package/src/components/tag-input/element.ts +39 -0
  309. package/src/components/tag-input/engine.ts +313 -0
  310. package/src/components/tag-input/react.tsx +54 -0
  311. package/src/components/tag-input/tag-input.css +243 -0
  312. package/src/components/time-field/README.md +54 -0
  313. package/src/components/time-field/element.ts +38 -0
  314. package/src/components/time-field/engine.ts +110 -0
  315. package/src/components/time-field/react.tsx +45 -0
  316. package/src/components/time-field/time-field.css +87 -0
  317. package/src/components/timeline/README.md +57 -0
  318. package/src/components/timeline/element.ts +31 -0
  319. package/src/components/timeline/engine.ts +300 -0
  320. package/src/components/timeline/react.tsx +59 -0
  321. package/src/components/timeline/timeline.css +218 -0
  322. package/src/components/tooltip/README.md +46 -0
  323. package/src/components/tooltip/element.ts +22 -0
  324. package/src/components/tooltip/engine.ts +288 -0
  325. package/src/components/tooltip/react.tsx +30 -0
  326. package/src/components/tooltip/tooltip.css +78 -0
  327. package/src/components/tree-view/README.md +61 -0
  328. package/src/components/tree-view/element.ts +32 -0
  329. package/src/components/tree-view/engine.ts +569 -0
  330. package/src/components/tree-view/react.tsx +78 -0
  331. package/src/components/tree-view/tree-view.css +209 -0
  332. package/src/components/virtual-list/README.md +81 -0
  333. package/src/components/virtual-list/element.ts +42 -0
  334. package/src/components/virtual-list/engine.ts +678 -0
  335. package/src/components/virtual-list/react.tsx +101 -0
  336. package/src/components/virtual-list/virtual-list.css +194 -0
  337. package/src/core/chips.ts +196 -0
  338. package/src/core/color.ts +304 -0
  339. package/src/core/date-range.ts +67 -0
  340. package/src/core/dates.ts +11 -1
  341. package/src/core/files.ts +107 -0
  342. package/src/core/filter.ts +229 -0
  343. package/src/core/gsap.ts +80 -65
  344. package/src/core/listbox.ts +332 -0
  345. package/src/core/mark.ts +29 -0
  346. package/src/core/position.ts +89 -0
  347. package/src/core/relative-time.ts +77 -0
  348. package/src/core/segment-field.ts +373 -0
  349. package/src/core/segments.ts +153 -0
  350. package/src/core/tree.ts +116 -0
  351. package/src/core/virtual.ts +79 -0
  352. package/src/css/tokens.css +186 -182
  353. package/src/elements/index.ts +147 -0
  354. package/src/index.ts +72 -0
  355. package/src/motion/micro.ts +248 -0
  356. package/src/motion/sheet.ts +88 -65
  357. package/src/motion/surface.ts +9 -0
  358. package/src/react/index.ts +57 -0
  359. package/dist/chunk-IKQ5FODN.js.map +0 -1
  360. package/dist/chunk-J6OCFNGU.js +0 -177
  361. package/dist/chunk-J6OCFNGU.js.map +0 -1
  362. package/dist/chunk-TFNRIZHU.js +0 -3977
  363. package/dist/chunk-TFNRIZHU.js.map +0 -1
  364. package/dist/chunk-XAKC2QY7.js.map +0 -1
  365. package/dist/reveal-jsX-uwUz.d.ts +0 -625
@@ -0,0 +1,313 @@
1
+ import { registerMotion } from '../../core/gsap';
2
+ import { createScope } from '../../core/scope';
3
+ import { inert, type Instance } from '../../core/types';
4
+ import { createChips } from '../../core/chips';
5
+ import { createListbox, filterOptions } from '../../core/listbox';
6
+ import { shake } from '../../motion/micro';
7
+
8
+ /*
9
+ * Tag input: free tags as chips, with suggestions. Enter or a comma adds what
10
+ * was typed (or the highlighted suggestion); pasting "a, b; c" adds each, one
11
+ * after another. New chips pop in and removed ones fold away
12
+ * (core/chips.ts); the suggestions open out of the field with a gliding
13
+ * active pill and the typed letters marked (core/listbox.ts). A duplicate
14
+ * rings the chip that already has it; a tag that `validate`, `restrict` or
15
+ * `max` refuses shakes the field and says why under it (aria-invalid, the
16
+ * message described by the input). Backspace in an empty input arms the last
17
+ * chip, then removes it; ← reaches the chips.
18
+ */
19
+
20
+ export interface TagInputOptions {
21
+ value?: string[];
22
+ /** Offered while typing (fuzzy, accents ignored). */
23
+ suggestions?: string[];
24
+ placeholder?: string;
25
+ /** Accessible name (default: the authored <label>, else 'Etiquetas'). */
26
+ label?: string;
27
+ /** Hidden inputs name[] for forms. */
28
+ name?: string;
29
+ max?: number;
30
+ /** Only tags from the suggestions. */
31
+ restrict?: boolean;
32
+ allowDuplicates?: boolean;
33
+ /** Return a message to refuse a tag. */
34
+ validate?: (tag: string, tags: string[]) => string | null | undefined | void;
35
+ /** Clean a tag before adding it (default: trim and collapse spaces). */
36
+ transform?: (tag: string) => string;
37
+ disabled?: boolean;
38
+ onChange?: (tags: string[]) => void;
39
+ }
40
+
41
+ export interface TagInputInstance extends Instance {
42
+ setValue(tags: string[], notify?: boolean): void;
43
+ /** Add a tag as if typed; false when refused. */
44
+ add(tag: string): boolean;
45
+ remove(tag: string): void;
46
+ setSuggestions(suggestions: string[]): void;
47
+ readonly value: string[];
48
+ }
49
+
50
+ const fold = (text: string) => text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
51
+ const SEPARATORS = /[,;\n]+/;
52
+ let sequence = 0;
53
+
54
+ export function createTagInput(root: HTMLElement | null, settings: TagInputOptions = {}): TagInputInstance {
55
+ if (!root) return inert({ setValue() {}, add: () => false, remove() {}, setSuggestions() {}, value: [] });
56
+ registerMotion();
57
+ const scope = createScope();
58
+ const uid = `mk-tags-${++sequence}`;
59
+ const max = Number.isFinite(settings.max) && settings.max! > 0 ? Math.floor(settings.max!) : Infinity;
60
+ const clean = settings.transform ?? ((tag: string) => tag.replace(/\s+/g, ' ').trim());
61
+ let suggestions = [...(settings.suggestions ?? [])];
62
+ let tags: string[] = [];
63
+ const labelText = settings.label ?? root.querySelector('label')?.textContent?.trim() ?? 'Etiquetas';
64
+
65
+ const field = document.createElement('div');
66
+ field.dataset.mkTagField = '';
67
+ const chips = createChips({ disabled: settings.disabled, onRemove: (tag) => remove(tag, true), onExit: () => input.focus() });
68
+ scope.add(() => chips.destroy());
69
+ const input = document.createElement('input');
70
+ input.type = 'text';
71
+ input.dataset.mkTagInput = '';
72
+ input.setAttribute('role', 'combobox');
73
+ input.setAttribute('aria-autocomplete', 'list');
74
+ input.setAttribute('aria-label', labelText);
75
+ input.setAttribute('aria-describedby', `${uid}-error`);
76
+ input.autocomplete = 'off';
77
+ input.spellcheck = false;
78
+ input.enterKeyHint = 'enter';
79
+ input.disabled = Boolean(settings.disabled);
80
+ field.append(chips.element, input);
81
+ const error = document.createElement('p');
82
+ error.id = `${uid}-error`;
83
+ error.dataset.mkTagError = '';
84
+ error.hidden = true;
85
+ const status = document.createElement('span');
86
+ status.className = 'mk-sr-only';
87
+ status.setAttribute('role', 'status');
88
+ const hiddenHost = document.createElement('span');
89
+ hiddenHost.hidden = true;
90
+ root.append(field, error, status, hiddenHost);
91
+ scope.add(() => {
92
+ field.remove();
93
+ error.remove();
94
+ status.remove();
95
+ hiddenHost.remove();
96
+ });
97
+ scope.attr(root, 'data-mk-ready', '');
98
+
99
+ const listbox = createListbox({
100
+ input,
101
+ anchor: field,
102
+ host: root,
103
+ label: 'Sugerencias',
104
+ emptyLabel: 'Sin sugerencias',
105
+ onPick: (option) => {
106
+ if (add(option.value, true)) {
107
+ input.value = '';
108
+ listbox.hide();
109
+ }
110
+ },
111
+ });
112
+ scope.add(() => listbox.destroy());
113
+
114
+ const placeholder = () => {
115
+ input.placeholder = tags.length >= max ? '' : (settings.placeholder ?? 'Añadir etiqueta…');
116
+ };
117
+ const showError = (message: string | null) => {
118
+ error.textContent = message ?? '';
119
+ error.hidden = !message;
120
+ if (message) input.setAttribute('aria-invalid', 'true');
121
+ else input.removeAttribute('aria-invalid');
122
+ };
123
+ const sync = (animate: boolean) => {
124
+ chips.set(tags.map((tag) => ({ value: tag, label: tag })), animate);
125
+ placeholder();
126
+ if (settings.name) {
127
+ hiddenHost.replaceChildren(
128
+ ...tags.map((tag) => {
129
+ const hidden = document.createElement('input');
130
+ hidden.type = 'hidden';
131
+ hidden.name = `${settings.name}[]`;
132
+ hidden.value = tag;
133
+ return hidden;
134
+ }),
135
+ );
136
+ }
137
+ };
138
+ const emit = () => {
139
+ settings.onChange?.([...tags]);
140
+ root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value: [...tags] } }));
141
+ };
142
+
143
+ /** The suggestions not chosen yet that match the typed text (all of them for an empty query). */
144
+ const available = (query: string) => {
145
+ const chosen = new Set(tags.map(fold));
146
+ return filterOptions(
147
+ suggestions.filter((tag) => !chosen.has(fold(tag))).map((tag) => ({ value: tag, label: tag })),
148
+ query,
149
+ ).slice(0, 50);
150
+ };
151
+ /** While typing: matching suggestions, or none (a new tag needs no empty list). */
152
+ const offer = () => {
153
+ const query = input.value;
154
+ const matches = query.trim() && suggestions.length ? available(query) : [];
155
+ if (!matches.length) return listbox.hide();
156
+ listbox.show(matches, query);
157
+ };
158
+
159
+ /** Add one tag; false (and why, shaken) when refused. */
160
+ const add = (raw: string, animate: boolean, notify = true) => {
161
+ const tag = clean(raw);
162
+ if (!tag || settings.disabled) return false;
163
+ const refuse = (message: string) => {
164
+ showError(message);
165
+ shake(field);
166
+ return false;
167
+ };
168
+ const twin = tags.find((existing) => fold(existing) === fold(tag));
169
+ if (twin && !settings.allowDuplicates) {
170
+ chips.flash(twin);
171
+ status.textContent = `Ya existe «${twin}»`;
172
+ return false;
173
+ }
174
+ if (tags.length >= max) return refuse(`Máximo ${max} ${max === 1 ? 'etiqueta' : 'etiquetas'}`);
175
+ const known = suggestions.find((suggestion) => fold(suggestion) === fold(tag));
176
+ if (settings.restrict && !known) return refuse(`«${tag}» no está en la lista`);
177
+ const message = settings.validate?.(known ?? tag, [...tags]);
178
+ if (message) return refuse(message);
179
+ tags = [...tags, known ?? tag];
180
+ showError(null);
181
+ chips.arm(null);
182
+ sync(animate);
183
+ status.textContent = `${known ?? tag} añadida`;
184
+ if (notify) emit();
185
+ return true;
186
+ };
187
+
188
+ const remove = (tag: string, notify: boolean) => {
189
+ if (!tags.includes(tag) || settings.disabled) return;
190
+ tags = tags.filter((existing) => existing !== tag);
191
+ chips.arm(null);
192
+ sync(true);
193
+ status.textContent = `${tag} quitada`;
194
+ if (notify) emit();
195
+ };
196
+
197
+ const commit = () => {
198
+ if (!input.value.trim()) return false;
199
+ if (add(input.value, true)) {
200
+ input.value = '';
201
+ listbox.hide();
202
+ return true;
203
+ }
204
+ return false;
205
+ };
206
+
207
+ scope.on(field, 'pointerdown', (event) => {
208
+ const target = event.target as Element | null;
209
+ if (target?.closest('[data-mk-chip-remove]')) return;
210
+ if (target !== input) event.preventDefault();
211
+ input.focus();
212
+ });
213
+ scope.on(input, 'input', () => {
214
+ showError(null);
215
+ chips.arm(null);
216
+ // A typed separator (mobile keyboards have no key events for it) commits what came before.
217
+ if (SEPARATORS.test(input.value)) {
218
+ const parts = input.value.split(SEPARATORS);
219
+ const rest = parts.pop() ?? '';
220
+ for (const part of parts) add(part, true);
221
+ input.value = rest;
222
+ }
223
+ offer();
224
+ });
225
+ scope.on(input, 'paste', (event) => {
226
+ const text = (event as ClipboardEvent).clipboardData?.getData('text') ?? '';
227
+ if (!SEPARATORS.test(text)) return;
228
+ event.preventDefault();
229
+ const parts = text.split(SEPARATORS).map(clean).filter(Boolean);
230
+ let added = 0;
231
+ for (const part of parts) if (add(part, true, false)) added++;
232
+ if (added) emit();
233
+ });
234
+ scope.on(input, 'keydown', (event) => {
235
+ const key = event as KeyboardEvent;
236
+ switch (key.key) {
237
+ case 'Enter':
238
+ if (listbox.isOpen && listbox.active) {
239
+ key.preventDefault();
240
+ listbox.pick();
241
+ } else if (input.value.trim()) {
242
+ key.preventDefault();
243
+ commit();
244
+ }
245
+ return;
246
+ case ',':
247
+ case ';':
248
+ key.preventDefault();
249
+ commit();
250
+ return;
251
+ case 'ArrowDown':
252
+ case 'ArrowUp':
253
+ if (!suggestions.length) return;
254
+ key.preventDefault();
255
+ if (!listbox.isOpen) listbox.show(available(input.value), input.value);
256
+ listbox.move(key.key === 'ArrowDown' ? 1 : -1);
257
+ return;
258
+ case 'Escape':
259
+ if (listbox.isOpen) {
260
+ key.preventDefault();
261
+ listbox.hide();
262
+ }
263
+ return;
264
+ case 'Backspace':
265
+ if (input.value || !tags.length) return;
266
+ key.preventDefault();
267
+ if (chips.armed) remove(chips.armed, true);
268
+ else chips.arm(tags[tags.length - 1]!);
269
+ return;
270
+ case 'ArrowLeft':
271
+ if (input.selectionStart === 0 && input.selectionEnd === 0 && chips.focusLast()) {
272
+ key.preventDefault();
273
+ listbox.hide();
274
+ }
275
+ return;
276
+ case 'Tab':
277
+ listbox.hide();
278
+ return;
279
+ }
280
+ });
281
+ scope.on(input, 'blur', () => {
282
+ chips.arm(null);
283
+ listbox.hide();
284
+ });
285
+
286
+ for (const tag of settings.value ?? []) add(tag, false, false);
287
+ sync(false);
288
+
289
+ return {
290
+ setValue(next, notify = false) {
291
+ if (scope.disposed) return;
292
+ tags = [];
293
+ for (const tag of next) {
294
+ const cleaned = clean(tag);
295
+ if (cleaned && (settings.allowDuplicates || !tags.some((existing) => fold(existing) === fold(cleaned)))) tags.push(cleaned);
296
+ }
297
+ tags = tags.slice(0, max);
298
+ showError(null);
299
+ sync(true);
300
+ if (notify) emit();
301
+ },
302
+ add: (tag) => add(tag, true),
303
+ remove: (tag) => remove(tag, true),
304
+ setSuggestions(next) {
305
+ suggestions = [...next];
306
+ if (listbox.isOpen) offer();
307
+ },
308
+ get value() {
309
+ return [...tags];
310
+ },
311
+ destroy: () => scope.dispose(),
312
+ };
313
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createTagInput, type TagInputInstance, type TagInputOptions } from './engine';
5
+
6
+ export interface TagInputProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>, TagInputOptions {
7
+ /** Controlled tags; without it the field keeps its own (defaultValue). */
8
+ value?: string[];
9
+ defaultValue?: string[];
10
+ }
11
+
12
+ /** <TagInput value={tags} onChange={setTags} suggestions={known} max={8} />. */
13
+ export function TagInput({ value, defaultValue, suggestions, placeholder, label, name, max, restrict, allowDuplicates, validate, transform, disabled, onChange, className, ...props }: TagInputProps) {
14
+ const ref = useRef<HTMLDivElement>(null);
15
+ const instance = useRef<TagInputInstance | null>(null);
16
+ const change = useLatest(onChange);
17
+ const check = useLatest(validate);
18
+ const shape = useLatest(transform);
19
+ const start = useRef(value ?? defaultValue ?? []);
20
+ useIsomorphicLayoutEffect(() => {
21
+ const created = createTagInput(ref.current, {
22
+ value: value ?? start.current,
23
+ suggestions,
24
+ placeholder,
25
+ label,
26
+ name,
27
+ max,
28
+ restrict,
29
+ allowDuplicates,
30
+ disabled,
31
+ validate: validate ? (tag, tags) => check.current?.(tag, tags) : undefined,
32
+ transform: transform ? (tag) => shape.current?.(tag) ?? tag : undefined,
33
+ onChange: (next) => change.current?.(next),
34
+ });
35
+ instance.current = created;
36
+ return () => {
37
+ start.current = created.value;
38
+ created.destroy();
39
+ instance.current = null;
40
+ };
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [placeholder, label, name, max, restrict, allowDuplicates, disabled, Boolean(validate), Boolean(transform)]);
43
+ const suggestionsKey = JSON.stringify(suggestions ?? null);
44
+ useIsomorphicLayoutEffect(() => {
45
+ if (suggestions) instance.current?.setSuggestions(suggestions);
46
+ // eslint-disable-next-line react-hooks/exhaustive-deps
47
+ }, [suggestionsKey]);
48
+ const valueKey = JSON.stringify(value ?? null);
49
+ useIsomorphicLayoutEffect(() => {
50
+ if (value && instance.current && JSON.stringify(instance.current.value) !== valueKey) instance.current.setValue(value);
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [valueKey]);
53
+ return <div {...props} ref={ref} className={['mk-tag-input', className].filter(Boolean).join(' ')} />;
54
+ }
@@ -0,0 +1,243 @@
1
+ /* tag-input — a 12 px field on surface-3 with 8 px chips and the input; the reason a tag was refused sits under it in
2
+ the error colour. The chips and the listbox rules below are shared with multi-select (each stylesheet works alone). */
3
+ .mk-tag-input,
4
+ mk-tag-input {
5
+ display: block;
6
+ min-width: 0;
7
+ }
8
+
9
+ [data-mk-tag-field] {
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ align-items: center;
13
+ gap: 0.35rem;
14
+ box-sizing: border-box;
15
+ min-height: 2.75rem;
16
+ padding: 0.35rem 0.5rem;
17
+ border-radius: 12px;
18
+ background: var(--mk-surface-3);
19
+ color: var(--mk-text);
20
+ cursor: text;
21
+ transition: box-shadow 0.2s ease;
22
+ }
23
+
24
+ [data-mk-tag-field]:focus-within {
25
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--mk-accent) 35%, transparent);
26
+ }
27
+
28
+ [data-mk-tag-field]:has([aria-invalid='true']) {
29
+ box-shadow: 0 0 0 2px var(--mk-error, #c62828);
30
+ }
31
+
32
+ [data-mk-tag-input] {
33
+ flex: 1;
34
+ min-width: 8ch;
35
+ height: 1.75rem;
36
+ padding: 0 0.25rem;
37
+ border: 0;
38
+ background: transparent;
39
+ color: inherit;
40
+ font: inherit;
41
+ outline: none;
42
+ }
43
+
44
+ [data-mk-tag-input]::placeholder {
45
+ color: var(--mk-muted);
46
+ }
47
+
48
+ [data-mk-tag-error] {
49
+ margin: 0.4rem 0 0;
50
+ color: var(--mk-error, #c62828);
51
+ font-size: 0.8125rem;
52
+ }
53
+
54
+ [data-mk-tag-error][hidden] {
55
+ display: none;
56
+ }
57
+
58
+ @media (prefers-reduced-motion: reduce) {
59
+ [data-mk-tag-field] {
60
+ transition: none;
61
+ }
62
+ }
63
+
64
+ /* Chips (core/chips.ts): 8 px, accent-soft, a round remove button; the armed one (next Backspace) gets a ring. */
65
+ [data-mk-chips] {
66
+ display: contents;
67
+ margin: 0;
68
+ padding: 0;
69
+ list-style: none;
70
+ }
71
+
72
+ [data-mk-chip] {
73
+ display: inline-flex;
74
+ flex: none;
75
+ align-items: center;
76
+ gap: 0.15rem;
77
+ box-sizing: border-box;
78
+ max-width: 100%;
79
+ height: 1.75rem;
80
+ padding: 0 0.2rem 0 0.6rem;
81
+ overflow: hidden;
82
+ border-radius: 8px;
83
+ background: var(--mk-accent-soft);
84
+ color: var(--mk-on-accent-soft, var(--mk-text));
85
+ font-size: 0.8125rem;
86
+ font-weight: 500;
87
+ white-space: nowrap;
88
+ outline: none;
89
+ transition: box-shadow 0.2s ease;
90
+ }
91
+
92
+ [data-mk-chip-label] {
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ }
96
+
97
+ [data-mk-chip]:focus-visible,
98
+ [data-mk-chip][data-mk-armed] {
99
+ box-shadow: 0 0 0 2px var(--mk-accent);
100
+ }
101
+
102
+ [data-mk-chip-remove] {
103
+ display: grid;
104
+ flex: none;
105
+ place-items: center;
106
+ width: 1.25rem;
107
+ height: 1.25rem;
108
+ padding: 0;
109
+ border: 0;
110
+ border-radius: 50%;
111
+ background: transparent;
112
+ color: inherit;
113
+ cursor: pointer;
114
+ opacity: 0.7;
115
+ transition: background-color 0.15s ease, opacity 0.15s ease;
116
+ }
117
+
118
+ [data-mk-chip-remove]:hover {
119
+ background: color-mix(in srgb, currentColor 14%, transparent);
120
+ opacity: 1;
121
+ }
122
+
123
+ /* Listbox (core/listbox.ts): a 16 px panel in the top layer under the field; the active option sits on a pill. */
124
+ [data-mk-listbox] {
125
+ position: fixed;
126
+ inset: auto;
127
+ z-index: 2147482000;
128
+ box-sizing: border-box;
129
+ max-height: min(18rem, 50vh);
130
+ margin: 0;
131
+ padding: 0.35rem;
132
+ overflow: auto;
133
+ border: 1px solid var(--mk-outline-soft);
134
+ border-radius: 16px;
135
+ background: var(--mk-surface-1);
136
+ color: var(--mk-text);
137
+ box-shadow: 0 16px 40px -14px color-mix(in srgb, var(--mk-shadow-color, #000) 35%, transparent);
138
+ font: 0.9375rem/1.3 var(--mk-font, system-ui, sans-serif);
139
+ overscroll-behavior: contain;
140
+ }
141
+
142
+ [data-mk-listbox][hidden] {
143
+ display: none;
144
+ }
145
+
146
+ [data-mk-listbox-items] {
147
+ position: relative;
148
+ display: grid;
149
+ gap: 2px;
150
+ }
151
+
152
+ [data-mk-listbox-pill] {
153
+ position: absolute;
154
+ top: 0.35rem;
155
+ right: 0.35rem;
156
+ left: 0.35rem;
157
+ border-radius: 10px;
158
+ background: var(--mk-accent-soft);
159
+ opacity: 0;
160
+ pointer-events: none;
161
+ }
162
+
163
+ [data-mk-option] {
164
+ position: relative;
165
+ z-index: 1;
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 0.6rem;
169
+ min-height: 2.5rem;
170
+ padding: 0 0.75rem;
171
+ border-radius: 10px;
172
+ cursor: pointer;
173
+ user-select: none;
174
+ }
175
+
176
+ [data-mk-option][aria-disabled='true'] {
177
+ color: var(--mk-muted);
178
+ cursor: not-allowed;
179
+ opacity: 0.55;
180
+ }
181
+
182
+ [data-mk-option-label] {
183
+ flex: 1;
184
+ min-width: 0;
185
+ overflow-wrap: anywhere;
186
+ }
187
+
188
+ [data-mk-option-label] mark {
189
+ background: transparent;
190
+ color: inherit;
191
+ font-weight: 700;
192
+ text-decoration: underline;
193
+ text-decoration-color: var(--mk-accent);
194
+ text-decoration-thickness: 2px;
195
+ text-underline-offset: 3px;
196
+ }
197
+
198
+ [data-mk-option-hint] {
199
+ color: var(--mk-muted);
200
+ font-size: 0.8125rem;
201
+ }
202
+
203
+ [data-mk-option-check] {
204
+ display: grid;
205
+ flex: none;
206
+ place-items: center;
207
+ width: 1.15rem;
208
+ height: 1.15rem;
209
+ border: 1.5px solid var(--mk-outline);
210
+ border-radius: 5px;
211
+ color: transparent;
212
+ transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
213
+ }
214
+
215
+ [data-mk-option][aria-selected='true'] [data-mk-option-check] {
216
+ border-color: var(--mk-accent);
217
+ background: var(--mk-accent);
218
+ color: var(--mk-on-accent);
219
+ }
220
+
221
+ [data-mk-listbox-group] {
222
+ padding: 0.6rem 0.75rem 0.25rem;
223
+ color: var(--mk-muted);
224
+ font-size: 0.75rem;
225
+ font-weight: 600;
226
+ letter-spacing: 0.02em;
227
+ text-transform: uppercase;
228
+ }
229
+
230
+ [data-mk-listbox-empty] {
231
+ padding: 0.75rem;
232
+ color: var(--mk-muted);
233
+ font-size: 0.875rem;
234
+ text-align: center;
235
+ }
236
+
237
+ @media (prefers-reduced-motion: reduce) {
238
+ [data-mk-chip],
239
+ [data-mk-chip-remove],
240
+ [data-mk-option-check] {
241
+ transition: none;
242
+ }
243
+ }
@@ -0,0 +1,54 @@
1
+ # time-field
2
+
3
+ Hora por segmentos: hora, minutos (y segundos) en el orden del idioma, con a. m./p. m. donde el idioma escribe 12 horas (`es`: 24 h; `en-US`: 12 h). El valor siempre es texto de 24 h («13:05»).
4
+
5
+ Cada segmento es un `role="spinbutton"`. Las flechas cambian el valor con vuelta y los dígitos ruedan en la dirección del cambio; al teclear se rellena el segmento y se pasa al siguiente; una píldora se desliza tras el segmento con foco (curva swing). En el móvil se abre el teclado numérico (`inputmode="numeric"`), pero el componente escribe siempre el texto: no se cuela nada raro. `min` y `max` marcan el grupo como no válido (una sacudida al pasar a no válido), aunque el valor se sigue informando, como en un input nativo.
6
+
7
+ ## HTML / Astro
8
+
9
+ ```html
10
+ <mk-time-field class="mk-time-field" value="09:30" minute-step="15" min="09:00" max="20:00" name="hora"></mk-time-field>
11
+ <mk-time-field class="mk-time-field" locale="en-US" seconds></mk-time-field>
12
+ ```
13
+
14
+ ## React
15
+
16
+ ```tsx
17
+ const [time, setTime] = useState<string | null>('09:30');
18
+ <TimeField value={time} onChange={setTime} minuteStep={15} />
19
+ ```
20
+
21
+ ## Opciones
22
+
23
+ | Opción | Atributo | Por defecto | Qué hace |
24
+ |---|---|---|---|
25
+ | `value` | `value` | vacío | `'HH:MM'` o `'HH:MM:SS'` (24 h). |
26
+ | `locale` | `locale` | `lang` de la página, o `es` | Orden de los segmentos y formato de 12/24 h. |
27
+ | `hourCycle` | `hour-cycle` | el del idioma | `12` o `24`. |
28
+ | `seconds` | `seconds` | `false` | Añade los segundos. |
29
+ | `minuteStep` | `minute-step` | `1` | Paso de las flechas en minutos (y segundos). |
30
+ | `min`, `max` | `min`, `max` | — | `'HH:MM'`; fuera de rango marca `aria-invalid`. |
31
+ | `label` | `label` | `'Hora'` | Nombre accesible del grupo. |
32
+ | `name` | `name` | — | Input oculto con el texto de 24 h. |
33
+ | `disabled`, `readOnly` | `disabled`, `read-only` | `false` | Estados. |
34
+ | `onChange(value)` | — | — | Cada cambio; `null` mientras falte algún segmento. |
35
+
36
+ ## Teclado
37
+
38
+ | Tecla | Acción |
39
+ |---|---|
40
+ | ↑ / ↓ | Cambia el segmento (con vuelta: 23 → 00) |
41
+ | RePág / AvPág | Saltos grandes (3 horas, 15 minutos) |
42
+ | Inicio / Fin | Mínimo / máximo del segmento |
43
+ | ← / → | Segmento anterior / siguiente |
44
+ | Dígitos | Rellenan el segmento y pasan al siguiente |
45
+ | Retroceso / Supr | Vacía el segmento; si ya está vacío, vuelve al anterior |
46
+ | «a» / «p» | a. m. / p. m. |
47
+
48
+ ## API y eventos
49
+
50
+ `setValue(value | null, animate = true)`, `focus()`, `value`, `destroy()`. Evento `mk:change` con `{ value }`.
51
+
52
+ ## Estilos
53
+
54
+ Campo de 12 px sobre `--mk-surface-3`, cifras tabulares, píldora de foco de 6 px en `--mk-accent-soft`; los segmentos vacíos se ven atenuados; no válido dibuja un anillo de `--mk-error`. Con movimiento reducido no hay rodillo ni píldora: el segmento con foco toma el color directamente.
@@ -0,0 +1,38 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createTimeField, type TimeFieldInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-time-field class="mk-time-field" value="09:30" minute-step="15" name="hora"></mk-time-field>
6
+ * Emits mk:change ({value}: 24 h text, null while incomplete).
7
+ */
8
+ export class MkTimeFieldElement extends MkElement<TimeFieldInstance> {
9
+ static options: Record<string, OptionType> = {
10
+ value: 'string',
11
+ locale: 'string',
12
+ hourCycle: 'number',
13
+ seconds: 'boolean',
14
+ minuteStep: 'number',
15
+ min: 'string',
16
+ max: 'string',
17
+ label: 'string',
18
+ name: 'string',
19
+ disabled: 'boolean',
20
+ readOnly: 'boolean',
21
+ };
22
+
23
+ protected create(options: Record<string, unknown>) {
24
+ return createTimeField(this, options);
25
+ }
26
+
27
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
28
+ if (previous === next || !this.instance) return;
29
+ if (name === 'value') this.instance.setValue(next);
30
+ else super.attributeChangedCallback(name, previous, next);
31
+ }
32
+
33
+ get value() {
34
+ return this.instance?.value ?? null;
35
+ }
36
+ }
37
+
38
+ export const defineTimeField = () => defineElement('mk-time-field', MkTimeFieldElement);