@leavepulse/ui 0.43.4 → 0.44.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 (337) hide show
  1. package/README.md +75 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-DCP5g2ik.css} +21 -21
  3. package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
  4. package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
  5. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-ChUyU7oV.css} +7 -7
  6. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  7. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-DDQNdf86.css} +9 -9
  8. package/dist/chunks/ContextMenuItems-CrfeNPWT.js +123 -0
  9. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-D-jPOAvg.js} +26 -9
  10. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-D8Xy9N-o.js} +20 -8
  11. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-BbwCF6l5.js} +24 -10
  12. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BrOqkZqg.js} +53 -48
  13. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-DkOIjsbq.js} +15 -8
  14. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-BUKPcHco.js} +51 -24
  15. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-DxV3Y9h4.js} +50 -23
  16. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-ClBbEYZN.js} +17 -8
  17. package/dist/chunks/LpBreadcrumbs-B8IJgFIc.js +95 -0
  18. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-kqJBHIIb.js} +44 -14
  19. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-CpRByR0q.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-DpqchzH4.js} +7 -3
  21. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-DeHJPq7p.js} +48 -16
  22. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-Bhf1qxhB.js} +32 -9
  23. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-CIUOSnWo.js} +58 -16
  24. package/dist/chunks/LpConfirmHost-CDm0Wtpv.js +49 -0
  25. package/dist/chunks/LpContextMenu-DQw5dGfG.js +68 -0
  26. package/dist/chunks/LpDatePicker-Drirx1AK.js +191 -0
  27. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-bScx-KMO.js} +63 -26
  28. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CSI_5SK2.js} +23 -8
  29. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-B4MiSQX2.js} +2 -2
  30. package/dist/chunks/LpFormField-CIk5qosh.js +87 -0
  31. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-C0e8y8_d.js} +26 -11
  32. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-Mr0W_6HK.js} +147 -60
  33. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  34. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-YBOaFnc_.js} +68 -34
  35. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-NUzS9e37.js} +38 -13
  36. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-DfJmMW0I.js} +6 -3
  37. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DPwGKBPv.js} +49 -26
  38. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-DPg5H9f3.js} +9 -6
  39. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-t5yn-siX.js} +58 -24
  40. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-DWnQKktS.js} +20 -7
  41. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  42. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  43. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-DFngX7Xo.js} +30 -19
  44. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-CWVDuBJ5.js} +10 -7
  45. package/dist/chunks/{LpPhoneInput-BDSKVl_z.js → LpPhoneInput-CD-ZTeSd.js} +57 -33
  46. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BLco31Nx.js} +78 -21
  47. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-p_e_shN3.js} +4 -2
  48. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  49. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  50. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-Dejd7cIx.js} +7 -3
  51. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DrRD9I7L.js} +40 -18
  52. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-BgwUNWpJ.js} +30 -22
  53. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-Bqxz8aWT.js} +14 -9
  54. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  55. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  56. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  57. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-dXtyrdX9.js} +1 -1
  58. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-B1aLKulj.js} +40 -26
  59. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  60. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-_qMVaZJ6.js} +19 -5
  61. package/dist/chunks/LpTextarea-yP8hGedR.js +77 -0
  62. package/dist/chunks/LpThemeProvider-Wjm7Tuit.js +35 -0
  63. package/dist/chunks/LpThemeSwitcher-CKLc10KZ.js +169 -0
  64. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  65. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-C5HRgS5H.js} +13 -7
  66. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster--oT6lfhL.js} +57 -28
  67. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CBdzeKNf.js} +13 -6
  68. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  69. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-DocsLal3.js} +6 -1
  70. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-DLtxOMRy.js} +43 -14
  71. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-C8JHUlPM.js} +173 -215
  72. package/dist/component-names.d.ts +1 -1
  73. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  74. package/dist/components/ContextMenuItems.vue.js +4 -0
  75. package/dist/components/LayoutCanvas.vue.js +1 -1
  76. package/dist/components/LayoutNode.vue.d.ts +4 -0
  77. package/dist/components/LayoutNode.vue.js +1 -1
  78. package/dist/components/LpActionBar.vue.d.ts +3 -0
  79. package/dist/components/LpActionBar.vue.js +1 -1
  80. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  81. package/dist/components/LpAddressInput.vue.js +1 -1
  82. package/dist/components/LpAlert.vue.d.ts +9 -0
  83. package/dist/components/LpAlert.vue.js +1 -1
  84. package/dist/components/LpAppShell.vue.d.ts +10 -0
  85. package/dist/components/LpAppShell.vue.js +1 -1
  86. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  87. package/dist/components/LpAutocomplete.vue.js +1 -1
  88. package/dist/components/LpAvatar.vue.d.ts +3 -3
  89. package/dist/components/LpAvatar.vue.js +1 -1
  90. package/dist/components/LpBadge.vue.d.ts +2 -2
  91. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  92. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  93. package/dist/components/LpButton.vue.d.ts +35 -98
  94. package/dist/components/LpButton.vue.js +1 -1
  95. package/dist/components/LpCard.vue.js +1 -1
  96. package/dist/components/LpChart.vue.d.ts +31 -2
  97. package/dist/components/LpChart.vue.js +317 -95
  98. package/dist/components/LpCheckbox.vue.js +1 -1
  99. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  100. package/dist/components/LpCodeBlock.vue.js +1 -1
  101. package/dist/components/LpColorPicker.vue.js +1 -1
  102. package/dist/components/LpCommandPalette.vue.d.ts +8 -3
  103. package/dist/components/LpCommandPalette.vue.js +86 -72
  104. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  105. package/dist/components/LpConfirmDialog.vue.js +1 -1
  106. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  107. package/dist/components/LpConfirmHost.vue.js +4 -0
  108. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  109. package/dist/components/LpContextMenu.vue.js +1 -1
  110. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  111. package/dist/components/LpDatePicker.vue.js +1 -1
  112. package/dist/components/LpDrawer.vue.d.ts +10 -0
  113. package/dist/components/LpDrawer.vue.js +1 -1
  114. package/dist/components/LpDropzone.vue.js +22 -6
  115. package/dist/components/LpFileTree.vue.d.ts +11 -2
  116. package/dist/components/LpFileTree.vue.js +1 -1
  117. package/dist/components/LpFileTreeNode.vue.js +1 -1
  118. package/dist/components/LpFormField.vue.d.ts +9 -0
  119. package/dist/components/LpFormField.vue.js +1 -1
  120. package/dist/components/LpImageEditor.vue.js +92 -57
  121. package/dist/components/LpInput.vue.d.ts +3 -3
  122. package/dist/components/LpInput.vue.js +1 -1
  123. package/dist/components/LpLightbox.vue.d.ts +17 -0
  124. package/dist/components/LpLightbox.vue.js +1 -1
  125. package/dist/components/LpLink.vue.d.ts +11 -11
  126. package/dist/components/LpLink.vue.js +1 -1
  127. package/dist/components/LpLogViewer.vue.d.ts +20 -2
  128. package/dist/components/LpLogViewer.vue.js +1 -1
  129. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  130. package/dist/components/LpMapPicker.vue.js +1 -1
  131. package/dist/components/LpMediaGallery.vue.js +1 -1
  132. package/dist/components/LpModal.vue.d.ts +16 -6
  133. package/dist/components/LpModal.vue.js +1 -1
  134. package/dist/components/LpModalNav.vue.d.ts +0 -1
  135. package/dist/components/LpModalNav.vue.js +1 -1
  136. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  137. package/dist/components/LpNotificationBell.vue.js +1 -1
  138. package/dist/components/LpNumberField.vue.d.ts +2 -0
  139. package/dist/components/LpNumberField.vue.js +1 -1
  140. package/dist/components/LpNumberFlow.vue.js +1 -1
  141. package/dist/components/LpOtpInput.vue.js +1 -1
  142. package/dist/components/LpPagination.vue.d.ts +2 -0
  143. package/dist/components/LpPagination.vue.js +1 -1
  144. package/dist/components/LpPasswordInput.vue.js +1 -1
  145. package/dist/components/LpPhoneInput.vue.js +1 -1
  146. package/dist/components/LpPie.vue.d.ts +21 -2
  147. package/dist/components/LpPie.vue.js +1 -1
  148. package/dist/components/LpPopover.vue.d.ts +2 -0
  149. package/dist/components/LpPopover.vue.js +1 -1
  150. package/dist/components/LpProgress.vue.js +1 -1
  151. package/dist/components/LpRadioGroup.vue.js +1 -1
  152. package/dist/components/LpSection.vue.d.ts +1 -1
  153. package/dist/components/LpSegmented.vue.d.ts +5 -0
  154. package/dist/components/LpSegmented.vue.js +1 -1
  155. package/dist/components/LpSelect.vue.d.ts +1 -0
  156. package/dist/components/LpSelect.vue.js +1 -1
  157. package/dist/components/LpShortcutsDialog.vue.d.ts +1 -1
  158. package/dist/components/LpShortcutsDialog.vue.js +11 -8
  159. package/dist/components/LpSidebar.vue.d.ts +6 -2
  160. package/dist/components/LpSidebar.vue.js +1 -1
  161. package/dist/components/LpSidebarNav.vue.js +1 -1
  162. package/dist/components/LpSkeleton.vue.js +1 -1
  163. package/dist/components/LpSlider.vue.js +1 -1
  164. package/dist/components/LpSparkline.vue.d.ts +1 -0
  165. package/dist/components/LpSparkline.vue.js +1 -1
  166. package/dist/components/LpStat.vue.js +1 -1
  167. package/dist/components/LpSwitch.vue.js +1 -1
  168. package/dist/components/LpTable.vue.d.ts +23 -1
  169. package/dist/components/LpTable.vue.js +74 -36
  170. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  171. package/dist/components/LpTableOfContents.vue.js +1 -1
  172. package/dist/components/LpTabs.vue.d.ts +2 -0
  173. package/dist/components/LpTabs.vue.js +1 -1
  174. package/dist/components/LpTextarea.vue.d.ts +19 -6
  175. package/dist/components/LpTextarea.vue.js +1 -1
  176. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  177. package/dist/components/LpThemeProvider.vue.js +4 -0
  178. package/dist/components/LpThemeSwitcher.vue.d.ts +13 -0
  179. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  180. package/dist/components/LpTilt.vue.js +1 -1
  181. package/dist/components/LpTimeField.vue.js +1 -1
  182. package/dist/components/LpToaster.vue.d.ts +6 -1
  183. package/dist/components/LpToaster.vue.js +1 -1
  184. package/dist/components/LpTooltip.vue.d.ts +1 -2
  185. package/dist/components/LpTooltip.vue.js +1 -1
  186. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  187. package/dist/components/LpTooltipProvider.vue.js +4 -0
  188. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  189. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  190. package/dist/components/LpUptimeBar.vue.js +1 -1
  191. package/dist/components/chart.d.ts +32 -1
  192. package/dist/components/chart.js +39 -4
  193. package/dist/components/codeHighlight.d.ts +6 -0
  194. package/dist/components/codeHighlight.js +92 -20
  195. package/dist/components/color.d.ts +1 -1
  196. package/dist/components/color.js +20 -9
  197. package/dist/components/confirm.d.ts +25 -0
  198. package/dist/components/confirm.js +26 -0
  199. package/dist/components/countries.d.ts +6 -0
  200. package/dist/components/countries.js +7 -0
  201. package/dist/components/fileTree.js +16 -7
  202. package/dist/components/imageCrop.d.ts +42 -0
  203. package/dist/components/imageCrop.js +47 -0
  204. package/dist/components/link.d.ts +12 -0
  205. package/dist/components/link.js +19 -0
  206. package/dist/components/modalDepth.d.ts +18 -2
  207. package/dist/components/modalDepth.js +17 -5
  208. package/dist/components/otp.d.ts +5 -0
  209. package/dist/components/otp.js +10 -0
  210. package/dist/components/sidebar.d.ts +7 -1
  211. package/dist/components/tableSort.d.ts +8 -0
  212. package/dist/components/tableSort.js +31 -0
  213. package/dist/composables/useField.d.ts +34 -0
  214. package/dist/composables/useField.js +45 -0
  215. package/dist/composables/useFormDraft.d.ts +10 -3
  216. package/dist/composables/useFormDraft.js +9 -14
  217. package/dist/composables/useHotkeys.d.ts +17 -1
  218. package/dist/composables/useHotkeys.js +55 -18
  219. package/dist/composables/useInputFilter.d.ts +17 -0
  220. package/dist/composables/useInputFilter.js +47 -10
  221. package/dist/composables/useLpConfig.d.ts +24 -0
  222. package/dist/composables/useLpConfig.js +44 -0
  223. package/dist/composables/useRovingFocus.js +7 -0
  224. package/dist/composables/useTilt.d.ts +2 -2
  225. package/dist/composables/useTilt.js +5 -6
  226. package/dist/composables/useToast.d.ts +20 -0
  227. package/dist/composables/useToast.js +28 -4
  228. package/dist/composables/useZoomPan.d.ts +3 -3
  229. package/dist/composables/useZoomPan.js +4 -4
  230. package/dist/index.d.ts +32 -2
  231. package/dist/index.js +184 -137
  232. package/dist/layout/tree.d.ts +2 -0
  233. package/dist/layout/tree.js +16 -1
  234. package/dist/layout/useLayout.d.ts +15 -0
  235. package/dist/layout/useLayout.js +41 -14
  236. package/dist/theme/useTheme.d.ts +32 -9
  237. package/package.json +8 -7
  238. package/src/component-names.ts +5 -7
  239. package/src/components/ContextMenuItems.vue +88 -0
  240. package/src/components/LayoutCanvas.vue +26 -6
  241. package/src/components/LayoutNode.vue +19 -2
  242. package/src/components/LpActionBar.vue +30 -7
  243. package/src/components/LpAddressInput.vue +57 -41
  244. package/src/components/LpAlert.vue +16 -3
  245. package/src/components/LpAppShell.vue +45 -12
  246. package/src/components/LpAutocomplete.vue +46 -10
  247. package/src/components/LpAvatar.vue +19 -5
  248. package/src/components/LpBreadcrumbs.vue +65 -41
  249. package/src/components/LpButton.vue +59 -13
  250. package/src/components/LpChart.vue +272 -28
  251. package/src/components/LpCheckbox.vue +7 -3
  252. package/src/components/LpCodeBlock.vue +50 -14
  253. package/src/components/LpColorPicker.vue +39 -9
  254. package/src/components/LpCommandPalette.vue +34 -13
  255. package/src/components/LpConfirmDialog.vue +68 -17
  256. package/src/components/LpConfirmHost.vue +59 -0
  257. package/src/components/LpContextMenu.vue +14 -115
  258. package/src/components/LpDatePicker.vue +99 -68
  259. package/src/components/LpDrawer.vue +56 -19
  260. package/src/components/LpDropzone.vue +21 -4
  261. package/src/components/LpFileTree.vue +33 -7
  262. package/src/components/LpFormField.vue +30 -7
  263. package/src/components/LpImageEditor.vue +96 -57
  264. package/src/components/LpInput.vue +18 -7
  265. package/src/components/LpLightbox.vue +140 -29
  266. package/src/components/LpLink.vue +24 -13
  267. package/src/components/LpLogViewer.vue +86 -17
  268. package/src/components/LpMapPicker.vue +36 -13
  269. package/src/components/LpMediaGallery.vue +8 -2
  270. package/src/components/LpModal.vue +53 -27
  271. package/src/components/LpModalNav.vue +5 -2
  272. package/src/components/LpNotificationBell.vue +63 -17
  273. package/src/components/LpNumberField.vue +21 -6
  274. package/src/components/LpNumberFlow.vue +26 -10
  275. package/src/components/LpOtpInput.vue +43 -8
  276. package/src/components/LpPagination.vue +23 -7
  277. package/src/components/LpPasswordInput.vue +4 -1
  278. package/src/components/LpPhoneInput.vue +53 -27
  279. package/src/components/LpPie.vue +94 -15
  280. package/src/components/LpPopover.vue +3 -0
  281. package/src/components/LpProgress.vue +5 -3
  282. package/src/components/LpRadioGroup.vue +9 -2
  283. package/src/components/LpSegmented.vue +12 -2
  284. package/src/components/LpSelect.vue +0 -0
  285. package/src/components/LpShortcutsDialog.vue +8 -3
  286. package/src/components/LpSidebar.vue +14 -9
  287. package/src/components/LpSidebarNav.vue +17 -5
  288. package/src/components/LpSkeleton.vue +4 -1
  289. package/src/components/LpSlider.vue +7 -1
  290. package/src/components/LpSparkline.vue +11 -3
  291. package/src/components/LpSwitch.vue +30 -7
  292. package/src/components/LpTable.vue +91 -32
  293. package/src/components/LpTableOfContents.vue +50 -32
  294. package/src/components/LpTabs.vue +23 -6
  295. package/src/components/LpTextarea.vue +44 -6
  296. package/src/components/LpThemeProvider.vue +41 -0
  297. package/src/components/LpThemeSwitcher.vue +64 -17
  298. package/src/components/LpTilt.vue +2 -2
  299. package/src/components/LpTimeField.vue +14 -3
  300. package/src/components/LpToaster.vue +66 -26
  301. package/src/components/LpTooltip.vue +22 -5
  302. package/src/components/LpTooltipProvider.vue +24 -0
  303. package/src/components/LpTopologyCanvas.vue +12 -1
  304. package/src/components/LpUptimeBar.vue +46 -8
  305. package/src/components/chart.ts +77 -5
  306. package/src/components/codeHighlight.ts +111 -15
  307. package/src/components/color.ts +30 -9
  308. package/src/components/confirm.ts +58 -0
  309. package/src/components/countries.ts +12 -0
  310. package/src/components/fileTree.ts +19 -7
  311. package/src/components/imageCrop.ts +107 -0
  312. package/src/components/link.ts +34 -0
  313. package/src/components/modalDepth.ts +48 -6
  314. package/src/components/otp.ts +16 -0
  315. package/src/components/sidebar.ts +7 -1
  316. package/src/components/tableSort.ts +37 -0
  317. package/src/composables/useField.ts +79 -0
  318. package/src/composables/useFormDraft.ts +25 -20
  319. package/src/composables/useHotkeys.ts +89 -18
  320. package/src/composables/useInputFilter.ts +77 -14
  321. package/src/composables/useLpConfig.ts +87 -0
  322. package/src/composables/useRovingFocus.ts +11 -0
  323. package/src/composables/useTilt.ts +5 -6
  324. package/src/composables/useToast.ts +56 -4
  325. package/src/composables/useZoomPan.ts +6 -6
  326. package/src/index.ts +37 -2
  327. package/src/layout/tree.ts +30 -0
  328. package/src/layout/useLayout.ts +68 -18
  329. package/src/nuxt.ts +59 -17
  330. package/src/runtime/config.plugin.ts +14 -0
  331. package/src/runtime/theme.plugin.ts +42 -22
  332. package/src/theme/useTheme.ts +206 -100
  333. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  334. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  335. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  336. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  337. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { Motion } from "motion-v"
13
13
  import { computed, onBeforeUnmount, onMounted, ref, useId, watch } from "vue"
14
+ import { useLabels } from "../composables/useLpConfig"
14
15
  import { usePillTransition } from "../composables/usePillTransition"
15
16
 
16
17
  export interface TocLink {
@@ -23,14 +24,29 @@ export interface TocLink {
23
24
  const props = withDefaults(
24
25
  defineProps<{
25
26
  links: TocLink[]
26
- /** Heading shown above the list. */
27
+ /** Heading shown above the list (defaults to the `title` label). */
27
28
  title?: string
28
29
  /** px offset from the top used as the scroll-spy trigger line. */
29
30
  offset?: number
31
+ labels?: Partial<{ title: string }>
30
32
  }>(),
31
- { title: "On this page", offset: 96 },
33
+ { offset: 96 },
32
34
  )
33
35
 
36
+ const text = useLabels("toc", { title: "On this page" }, () => props.labels)
37
+ const heading = computed(() => props.title ?? text.value.title)
38
+
39
+ // Every level of the tree as one ordered list; depth drives the indent, so
40
+ // links nested deeper than two levels render instead of vanishing.
41
+ interface TocRow {
42
+ link: TocLink
43
+ depth: number
44
+ }
45
+ function flatten(links: TocLink[], depth = 0): TocRow[] {
46
+ return links.flatMap((link) => [{ link, depth }, ...flatten(link.children ?? [], depth + 1)])
47
+ }
48
+ const rows = computed(() => flatten(props.links))
49
+
34
50
  const activeId = ref<string>("")
35
51
  // The id the pointer is hovering (null when not hovering). The indicator bar
36
52
  // follows the hovered link, falling back to the scroll-spy active one — same
@@ -97,13 +113,28 @@ function unfreeze() {
97
113
  safetyTimer = null
98
114
  }
99
115
 
100
- function suppressSpyUntilScrollEnds() {
116
+ /*
117
+ * The element that actually scrolls to the heading. Inside an app shell that is
118
+ * a scroll region, not the window, and watching window.scrollY there saw no
119
+ * movement and unfroze on the first frame.
120
+ */
121
+ function scrollerOf(el: HTMLElement): HTMLElement | null {
122
+ for (let node = el.parentElement; node; node = node.parentElement) {
123
+ const { overflowY } = getComputedStyle(node)
124
+ if (/(auto|scroll|overlay)/.test(overflowY) && node.scrollHeight > node.clientHeight) return node
125
+ }
126
+ return null
127
+ }
128
+
129
+ function suppressSpyUntilScrollEnds(target: HTMLElement) {
101
130
  spySuppressed = true
102
131
  if (rafId != null) cancelAnimationFrame(rafId)
103
132
  if (safetyTimer != null) clearTimeout(safetyTimer)
104
- let last = window.scrollY
133
+ const scroller = scrollerOf(target)
134
+ const position = () => (scroller ? scroller.scrollTop : window.scrollY)
135
+ let last = position()
105
136
  const tick = () => {
106
- const y = window.scrollY
137
+ const y = position()
107
138
  if (y === last) {
108
139
  unfreeze() // position held for a frame → scrolling has come to rest
109
140
  return
@@ -122,9 +153,11 @@ function onClick(id: string, ev: MouseEvent) {
122
153
  if (!el) return
123
154
  ev.preventDefault()
124
155
  activeId.value = id // mark immediately so the bar jumps to the clicked item
125
- suppressSpyUntilScrollEnds()
156
+ suppressSpyUntilScrollEnds(el)
126
157
  el.scrollIntoView({ behavior: "smooth", block: "start" })
127
- history.replaceState(null, "", `#${id}`)
158
+ // Keep the router's own state: replacing it with null broke scroll
159
+ // restoration and back/forward bookkeeping in vue-router apps.
160
+ history.replaceState(history.state, "", `#${id}`)
128
161
  }
129
162
 
130
163
  onMounted(setup)
@@ -136,16 +169,21 @@ onBeforeUnmount(() => {
136
169
  </script>
137
170
 
138
171
  <template>
139
- <nav :aria-label="title" class="text-sm">
172
+ <nav :aria-label="heading" class="text-sm">
140
173
  <p class="mb-2.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted">
141
- {{ title }}
174
+ {{ heading }}
142
175
  </p>
143
176
  <ul class="relative flex flex-col gap-0.5 border-l border-line" @mouseleave="hoveredId = null">
144
- <li v-for="link in links" :key="link.id" class="flex flex-col">
177
+ <li v-for="{ link, depth } in rows" :key="link.id" class="flex flex-col">
145
178
  <a
146
179
  :href="`#${link.id}`"
147
- class="relative -ml-px block py-1 pl-3.5 leading-snug transition-colors duration-[var(--duration-fast)]"
148
- :class="litId === link.id ? 'text-brand' : 'text-muted hover:text-ink'"
180
+ class="relative -ml-px block py-1 leading-snug transition-colors duration-[var(--duration-fast)]"
181
+ :class="[
182
+ depth > 0 ? 'text-[13px]' : '',
183
+ litId === link.id ? 'text-brand' : 'text-muted hover:text-ink',
184
+ ]"
185
+ :style="{ paddingLeft: `${0.875 + depth * 0.875}rem` }"
186
+ :aria-current="activeId === link.id ? 'location' : undefined"
149
187
  @click="onClick(link.id, $event)"
150
188
  @mouseenter="hoveredId = link.id"
151
189
  >
@@ -157,26 +195,6 @@ onBeforeUnmount(() => {
157
195
  />
158
196
  {{ link.text }}
159
197
  </a>
160
-
161
- <ul v-if="link.children?.length" class="flex flex-col gap-0.5">
162
- <li v-for="child in link.children" :key="child.id">
163
- <a
164
- :href="`#${child.id}`"
165
- class="relative -ml-px block py-1 pl-7 text-[13px] leading-snug transition-colors duration-[var(--duration-fast)]"
166
- :class="litId === child.id ? 'text-brand' : 'text-muted hover:text-ink'"
167
- @click="onClick(child.id, $event)"
168
- @mouseenter="hoveredId = child.id"
169
- >
170
- <Motion
171
- v-if="litId === child.id"
172
- :layout-id="pillId"
173
- :transition="pillTransition"
174
- class="absolute -left-px top-0 h-full w-0.5 rounded-full bg-brand"
175
- />
176
- {{ child.text }}
177
- </a>
178
- </li>
179
- </ul>
180
198
  </li>
181
199
  </ul>
182
200
  </nav>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { Motion, useReducedMotion } from "motion-v"
3
3
  import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "reka-ui"
4
- import { computed, ref, useId } from "vue"
4
+ import { computed, ref, useId, watch } from "vue"
5
5
  import { usePillTransition } from "../composables/usePillTransition"
6
6
  import LpIcon from "./LpIcon.vue"
7
7
 
@@ -12,9 +12,11 @@ export interface TabItem {
12
12
  icon?: string
13
13
  }
14
14
 
15
- withDefaults(
15
+ const props = withDefaults(
16
16
  defineProps<{
17
17
  modelValue?: string
18
+ /** Initially active tab when `v-model` is not bound (default: the first). */
19
+ defaultValue?: string
18
20
  items: TabItem[]
19
21
  /**
20
22
  * "contained" (default) wraps the triggers in a bordered, filled bar.
@@ -31,7 +33,22 @@ withDefaults(
31
33
  }>(),
32
34
  { variant: "contained", accent: false, block: false },
33
35
  )
34
- defineEmits<{ (e: "update:modelValue", value: string): void }>()
36
+ const emit = defineEmits<{ (e: "update:modelValue", value: string): void }>()
37
+
38
+ // Held here rather than left to reka, so the active marker follows the active
39
+ // tab in uncontrolled use too: reading the prop alone left it unmarked.
40
+ const current = ref(props.modelValue ?? props.defaultValue ?? props.items[0]?.value)
41
+ watch(
42
+ () => props.modelValue,
43
+ (value) => {
44
+ if (value !== undefined) current.value = value
45
+ },
46
+ )
47
+
48
+ function select(value: string) {
49
+ if (props.modelValue === undefined) current.value = value
50
+ emit("update:modelValue", value)
51
+ }
35
52
 
36
53
  // The pill sits under the hovered tab, falling back to the active one
37
54
  // (UAProject header behaviour). motion-v shared layoutId animates the move.
@@ -80,8 +97,8 @@ function releaseFocusAfterPointer(event: PointerEvent) {
80
97
 
81
98
  <template>
82
99
  <TabsRoot
83
- :model-value="modelValue"
84
- @update:model-value="(v) => $emit('update:modelValue', v as string)"
100
+ :model-value="current"
101
+ @update:model-value="(v) => select(v as string)"
85
102
  >
86
103
  <TabsList
87
104
  class="gap-1"
@@ -109,7 +126,7 @@ function releaseFocusAfterPointer(event: PointerEvent) {
109
126
  <!-- Active marker: a bottom bar in "underline", a filled pill otherwise.
110
127
  Both slide between tabs via the shared motion layoutId. -->
111
128
  <Motion
112
- v-if="pillUnder(item.value, modelValue)"
129
+ v-if="pillUnder(item.value, current)"
113
130
  :layout-id="pillId"
114
131
  :transition="pillTransition"
115
132
  :class="variant === 'underline'
@@ -1,5 +1,11 @@
1
+ <script lang="ts">
2
+ export default { inheritAttrs: false }
3
+ </script>
4
+
1
5
  <script setup lang="ts">
2
- import { computed } from "vue"
6
+ import { computed, useAttrs } from "vue"
7
+ import { useFieldControl } from "../composables/useField"
8
+ import { mergeClass } from "../composables/useMergedClass"
3
9
  import LpFormField from "./LpFormField.vue"
4
10
 
5
11
  const props = defineProps<{
@@ -21,25 +27,57 @@ const props = defineProps<{
21
27
  defineEmits<{ (e: "update:modelValue", value: string): void }>()
22
28
 
23
29
  const hasField = computed(() => !!props.label || !!props.hint || !!props.error)
30
+
31
+ // class/style lay out the field when there is one, and style the <textarea>
32
+ // when it stands alone; every other attr (name, maxlength, autofocus, @blur…)
33
+ // belongs to the <textarea> itself.
34
+ const attrs = useAttrs()
35
+ const textareaAttrs = computed(() => {
36
+ const { class: _c, style: _s, ...rest } = attrs
37
+ return rest
38
+ })
39
+
40
+ const field = useFieldControl(() => ({
41
+ wrapped: hasField.value,
42
+ hasMessage: !!props.error || !!props.hint,
43
+ id: typeof attrs.id === "string" ? attrs.id : undefined,
44
+ invalid: props.invalid || !!props.error,
45
+ required: props.required,
46
+ disabled: props.disabled,
47
+ }))
48
+
49
+ const textareaClass = computed(() =>
50
+ mergeClass(
51
+ "w-full resize-y rounded-control border bg-surface-soft px-3 py-2 text-sm text-ink outline-none transition-colors duration-[var(--duration-fast)] placeholder:text-muted focus:border-brand focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-55",
52
+ field.value.invalid ? "border-danger focus:border-danger focus:ring-danger-soft" : "border-line",
53
+ hasField.value ? "" : (attrs.class as string),
54
+ ),
55
+ )
24
56
  </script>
25
57
 
26
58
  <template>
27
59
  <component
28
60
  :is="hasField ? LpFormField : 'div'"
61
+ :id="hasField ? field.id : undefined"
29
62
  :label="hasField ? label : undefined"
30
63
  :hint="hasField ? hint : undefined"
31
64
  :error="hasField ? error : undefined"
32
65
  :required="hasField ? required : undefined"
33
- :class="hasField ? undefined : 'contents'"
66
+ :class="hasField ? attrs.class : 'contents'"
67
+ :style="hasField ? attrs.style : undefined"
34
68
  >
35
69
  <textarea
70
+ v-bind="textareaAttrs"
71
+ :id="field.id"
36
72
  :value="modelValue"
37
73
  :placeholder="placeholder"
38
74
  :rows="rows ?? 4"
39
- :disabled="disabled"
40
- :required="required || undefined"
41
- class="w-full resize-y rounded-control border bg-surface-soft px-3 py-2 text-sm text-ink outline-none transition-colors duration-[var(--duration-fast)] placeholder:text-muted focus:border-brand focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-55"
42
- :class="(invalid || error) ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line'"
75
+ :disabled="field.disabled"
76
+ :required="field.required || undefined"
77
+ :aria-invalid="field.invalid || undefined"
78
+ :aria-describedby="field.describedBy"
79
+ :class="textareaClass"
80
+ :style="hasField ? undefined : (attrs.style as never)"
43
81
  @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
44
82
  />
45
83
  </component>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ export default { inheritAttrs: false }
3
+ </script>
4
+
5
+ <script setup lang="ts">
6
+ /*
7
+ * Applies a theme to a subtree instead of the whole page: a dark sidebar in a
8
+ * light app, a live preview of a theme being edited, an embedded widget that
9
+ * keeps its own look. Every token is declared on this element, derived ones
10
+ * included, so kit components inside re-skin while the page around them keeps
11
+ * the global theme. Nothing is persisted; this is presentation only.
12
+ */
13
+ import { computed, type Component } from "vue"
14
+ import type { TokenSet } from "../theme/tokens"
15
+ import { themeEntries } from "../theme/useTheme"
16
+ import { useMergedAttrs } from "../composables/useMergedClass"
17
+
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ theme: TokenSet
21
+ /** Root element. A wrapper that should not affect layout can use `class="contents"`. */
22
+ as?: string | Component
23
+ }>(),
24
+ { as: "div" },
25
+ )
26
+
27
+ // Paints its own surface and text so the scoped theme reads as one; a class
28
+ // like `bg-transparent` overrides that.
29
+ const { class: rootClass, attrs: rest } = useMergedAttrs("bg-surface text-ink")
30
+
31
+ const style = computed(() => ({
32
+ ...Object.fromEntries(themeEntries(props.theme)),
33
+ colorScheme: props.theme.mode,
34
+ }))
35
+ </script>
36
+
37
+ <template>
38
+ <component :is="as" :style="style" :data-theme-mode="theme.mode" :class="rootClass" v-bind="rest">
39
+ <slot />
40
+ </component>
41
+ </template>
@@ -12,6 +12,7 @@
12
12
  * fetches; it just drives the theme engine.
13
13
  */
14
14
  import { computed, onMounted, ref } from "vue"
15
+ import { useLabels } from "../composables/useLpConfig"
15
16
  import { presets } from "../theme/presets"
16
17
  import type { TokenSet } from "../theme/tokens"
17
18
  import { useTheme } from "../theme/useTheme"
@@ -51,6 +52,13 @@ const props = withDefaults(
51
52
  transition?: boolean
52
53
  size?: "sm" | "md" | "lg"
53
54
  "aria-label"?: string
55
+ /**
56
+ * Offer a "System" entry in the menu that follows the OS colour scheme,
57
+ * switching between these two themes (names from `themes`, or TokenSets).
58
+ */
59
+ system?: { light: string | TokenSet; dark: string | TokenSet }
60
+ /** Text overrides. `trigger` may contain `{name}`. */
61
+ labels?: Partial<{ trigger: string; system: string }>
54
62
  }>(),
55
63
  {
56
64
  variant: "swatch",
@@ -66,6 +74,12 @@ const emit = defineEmits<{
66
74
 
67
75
  const theme = useTheme()
68
76
 
77
+ const text = useLabels(
78
+ "themeSwitcher",
79
+ { trigger: "Theme: {name}. Click to switch, right-click to choose.", system: "System" },
80
+ () => props.labels,
81
+ )
82
+
69
83
  const list = computed<TokenSet[]>(() =>
70
84
  props.themes && props.themes.length ? props.themes : Object.values(presets),
71
85
  )
@@ -75,9 +89,6 @@ const list = computed<TokenSet[]>(() =>
75
89
  const activeName = computed(
76
90
  () => props.modelValue ?? (theme.currentName.value || list.value[0]?.name || ""),
77
91
  )
78
- const active = computed<TokenSet | undefined>(
79
- () => list.value.find((t) => t.name === activeName.value) ?? list.value[0],
80
- )
81
92
 
82
93
  // Apply the consumer's default on mount when nothing is cached yet. init()
83
94
  // hydrates from the cache first (a saved choice wins), so this only takes effect
@@ -140,18 +151,49 @@ function cycle(ev: MouseEvent) {
140
151
  // Right-click menu: every theme, the active one checked. A circular-reveal from
141
152
  // the (saved) click point isn't available here, so the menu picks reveal from
142
153
  // the trigger centre via applyWithTransition's default origin.
143
- const menu = computed<ContextMenuItemDef[]>(() =>
144
- list.value.map((t) => ({
154
+ function resolve(t: string | TokenSet): TokenSet | undefined {
155
+ return typeof t === "string" ? list.value.find((x) => x.name === t) : t
156
+ }
157
+
158
+ const systemPair = computed(() => {
159
+ if (!props.system) return null
160
+ const light = resolve(props.system.light)
161
+ const dark = resolve(props.system.dark)
162
+ return light && dark ? { light, dark } : null
163
+ })
164
+
165
+ function followSystem() {
166
+ const pair = systemPair.value
167
+ if (!pair) return
168
+ theme.setSystem(pair)
169
+ const next = theme.current.value as TokenSet | null
170
+ if (next) {
171
+ emit("update:modelValue", next.name)
172
+ emit("change", next)
173
+ }
174
+ }
175
+
176
+ const menu = computed<ContextMenuItemDef[]>(() => {
177
+ const following = theme.system.value
178
+ const items: ContextMenuItemDef[] = list.value.map((t) => ({
145
179
  label: t.name,
146
180
  icon:
147
- t.name === activeName.value
181
+ !following && t.name === activeName.value
148
182
  ? "lucide:check"
149
183
  : t.mode === "dark"
150
184
  ? "lucide:moon"
151
185
  : "lucide:sun",
152
186
  onSelect: () => applyTheme(t),
153
- })),
154
- )
187
+ }))
188
+ if (systemPair.value) {
189
+ items.unshift({
190
+ label: text.value.system,
191
+ icon: following ? "lucide:check" : "lucide:monitor",
192
+ onSelect: followSystem,
193
+ })
194
+ }
195
+ return items
196
+ })
155
197
 
156
198
  /*
157
199
  * The theme the SERVER rendered is not necessarily the one the client restores:
@@ -176,7 +218,7 @@ const labelName = computed(() =>
176
218
  )
177
219
 
178
220
  const triggerLabel = computed(
179
- () => props["aria-label"] ?? `Theme: ${labelName.value}. Click to switch, right-click to choose.`,
221
+ () => props["aria-label"] ?? text.value.trigger.replace("{name}", labelName.value),
180
222
  )
181
223
  </script>
182
224
 
@@ -197,23 +239,28 @@ const triggerLabel = computed(
197
239
  "
198
240
  @click="cycle"
199
241
  >
200
- <!-- swatch: brand over surface, hinting the theme at a glance -->
242
+ <!-- swatch: brand over surface, read from the applied variables rather
243
+ than the theme object, so the server's render (painted from the
244
+ cookie) and the client's agree without a hydration patch -->
201
245
  <span
202
246
  v-if="variant !== 'icon'"
203
247
  class="grid shrink-0 place-items-center rounded-control border border-line/60"
204
248
  :class="SIZES[size]"
205
- :style="{ background: active?.colors.surface }"
249
+ style="background: var(--color-surface)"
206
250
  >
207
- <span
208
- class="size-3 rounded-full"
209
- :style="{ background: active?.colors.brand }"
210
- />
251
+ <span class="size-3 rounded-full" style="background: var(--color-brand)" />
211
252
  </span>
212
- <!-- icon: sun/moon by mode -->
253
+ <!-- icon: sun/moon by mode, chosen by CSS from the applied theme's mode -->
213
254
  <span v-else class="grid shrink-0 place-items-center" :class="SIZES[size]">
214
255
  <LpIcon
215
- :name="active?.mode === 'dark' ? 'lucide:moon' : 'lucide:sun'"
256
+ name="lucide:moon"
257
+ :size="ICON_SIZE[size]"
258
+ class="in-data-[theme-mode=light]:hidden"
259
+ />
260
+ <LpIcon
261
+ name="lucide:sun"
216
262
  :size="ICON_SIZE[size]"
263
+ class="hidden in-data-[theme-mode=light]:block"
217
264
  />
218
265
  </span>
219
266
 
@@ -31,12 +31,12 @@ const props = withDefaults(
31
31
  { max: 10, lift: 8, scale: 1.03, glare: false, perspective: 900 },
32
32
  )
33
33
 
34
- const tilt = useTilt({
34
+ const tilt = useTilt(() => ({
35
35
  max: props.max,
36
36
  lift: props.lift,
37
37
  scale: props.scale,
38
38
  glare: props.glare,
39
- })
39
+ }))
40
40
 
41
41
  const stageStyle = computed(() => ({ perspective: `${props.perspective}px` }))
42
42
  </script>
@@ -13,6 +13,7 @@
13
13
  import { Time } from "@internationalized/date"
14
14
  import { TimeFieldInput, TimeFieldRoot } from "reka-ui"
15
15
  import { computed } from "vue"
16
+ import { useFieldControl } from "../composables/useField"
16
17
  import LpIcon from "./LpIcon.vue"
17
18
 
18
19
  const props = withDefaults(
@@ -50,6 +51,12 @@ function onUpdate(next: unknown) {
50
51
  emit("update:modelValue", `${pad(next.hour)}:${pad(next.minute)}`)
51
52
  }
52
53
 
54
+ const field = useFieldControl(() => ({
55
+ wrapped: false,
56
+ invalid: props.invalid,
57
+ disabled: props.disabled,
58
+ }))
59
+
53
60
  const shellSize = {
54
61
  sm: "h-(--size-control-sm) text-xs",
55
62
  md: "h-(--size-control-md) text-sm",
@@ -64,16 +71,20 @@ const shellSize = {
64
71
  :hour-cycle="hourCycle"
65
72
  :locale="locale"
66
73
  granularity="minute"
67
- :disabled="disabled"
74
+ :disabled="field.disabled"
75
+ :aria-invalid="field.invalid || undefined"
76
+ :aria-describedby="field.describedBy"
68
77
  class="flex w-full items-center gap-0.5 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55"
69
78
  :class="[
70
79
  shellSize[size],
71
- invalid ? 'border-danger focus-within:ring-danger-soft' : 'border-line focus-within:border-brand',
80
+ field.invalid ? 'border-danger focus-within:ring-danger-soft' : 'border-line focus-within:border-brand',
72
81
  ]"
73
82
  @update:model-value="onUpdate"
74
83
  >
75
84
  <LpIcon v-if="icon" name="lucide:clock" :size="15" class="mr-1.5 shrink-0 text-muted" />
76
- <template v-for="item in segments" :key="item.part">
85
+ <!-- Indexed keys: in 12-hour mode the segments hold two "literal" parts
86
+ (the ":" and the space before AM/PM), which a key by part repeats. -->
87
+ <template v-for="(item, i) in segments" :key="i">
77
88
  <TimeFieldInput v-if="item.part === 'literal'" :part="item.part" class="text-muted">
78
89
  {{ item.value }}
79
90
  </TimeFieldInput>