@leavepulse/ui 0.71.1 → 0.72.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 (297) hide show
  1. package/dist/assets/{LpCommandPalette-DgPZX0us.css → LpCommandPalette-Rtbg669l.css} +19 -19
  2. package/dist/assets/{LpNumberFlow-rZkwQwlU.css → LpNumberFlow-Chp9pEDe.css} +8 -0
  3. package/dist/assets/LpTabs-CDchzoRL.css +18 -0
  4. package/dist/chunks/{ContextMenuItems-eS7qx9gD.js → ContextMenuItems-BXumicbh.js} +1 -1
  5. package/dist/chunks/{DropzoneLinkField-DkFhdfEJ.js → DropzoneLinkField-D2Rnkcys.js} +2 -2
  6. package/dist/chunks/{FieldWrapper-YzbvvC3M.js → FieldWrapper-DnA1rxZ8.js} +1 -1
  7. package/dist/chunks/{LayoutCanvas-Ba4A9QC5.js → LayoutCanvas-CNOAUk7D.js} +1 -1
  8. package/dist/chunks/{LayoutNode-CTTTLMDM.js → LayoutNode-DF5BMN0J.js} +1 -1
  9. package/dist/chunks/{LpActionBar-B7anGlkI.js → LpActionBar-DfiBL1dE.js} +3 -3
  10. package/dist/chunks/{LpAddressInput-CD0bzrt-.js → LpAddressInput-B1bB3F6M.js} +7 -7
  11. package/dist/chunks/{LpAlert-BHi7ARqu.js → LpAlert-CpRF9eYr.js} +3 -3
  12. package/dist/chunks/{LpAppShell-1RmBp6xQ.js → LpAppShell-D8Q0Jv9d.js} +68 -18
  13. package/dist/chunks/{LpAutocomplete-DDqe2M5g.js → LpAutocomplete-DOu7FN5P.js} +2 -2
  14. package/dist/chunks/{LpAvatar-DqfKCIdr.js → LpAvatar-Cg6Az3wz.js} +1 -1
  15. package/dist/chunks/{LpBadge-C8fKEmTF.js → LpBadge-vtCpsDmv.js} +3 -3
  16. package/dist/chunks/LpBottomNav-VXJf0cKc.js +89 -0
  17. package/dist/chunks/{LpBreadcrumbs-DhcoStFa.js → LpBreadcrumbs-BdzjQcql.js} +3 -3
  18. package/dist/chunks/{LpButton-BQDAFXBd.js → LpButton-Dk7eT4ro.js} +1 -1
  19. package/dist/chunks/{LpCalendar-NmrA47J9.js → LpCalendar-CuoWks-0.js} +1 -1
  20. package/dist/chunks/{LpCard-CqKLnEhl.js → LpCard-Ce3rw8zm.js} +1 -1
  21. package/dist/chunks/{LpCheckbox-BoOuLnGB.js → LpCheckbox-DLX4amba.js} +2 -2
  22. package/dist/chunks/{LpCodeBlock-Czu-fq1M.js → LpCodeBlock-j0W5bpmF.js} +2 -2
  23. package/dist/chunks/{LpCodeDisplay-BK2FHbtM.js → LpCodeDisplay-COdiv4xu.js} +6 -6
  24. package/dist/chunks/{LpColorPicker-Ee1DZiRd.js → LpColorPicker-1y1QJHZR.js} +56 -28
  25. package/dist/chunks/{LpCommandScope-ubtWDMa0.js → LpCommandScope-BlKcgBnK.js} +1 -1
  26. package/dist/chunks/{LpConfirmDialog-i15pulMT.js → LpConfirmDialog-w1ni5nxh.js} +3 -3
  27. package/dist/chunks/{LpConfirmHost-D0oGavtd.js → LpConfirmHost-Br_-Gn-6.js} +1 -1
  28. package/dist/chunks/{LpContextMenu-DXpFJjlO.js → LpContextMenu-rx2tCjwk.js} +2 -2
  29. package/dist/chunks/{LpCopyButton-Dmq7DmFw.js → LpCopyButton-DaD1KCn6.js} +2 -2
  30. package/dist/chunks/{LpCountdown-DTdMF8DL.js → LpCountdown-Cgy0mVEq.js} +3 -3
  31. package/dist/chunks/{LpDataList-CFPtSsBB.js → LpDataList-CkcGFJgv.js} +3 -3
  32. package/dist/chunks/{LpDatePicker-cC9K-Pk8.js → LpDatePicker-CA0QTfBC.js} +4 -4
  33. package/dist/chunks/{LpDateRangePicker-BVQKPbvo.js → LpDateRangePicker-u3J51xXk.js} +2 -2
  34. package/dist/chunks/{LpDisclosure-hRnX3FWL.js → LpDisclosure-Cpa7Hc5T.js} +1 -1
  35. package/dist/chunks/{LpDrawer-u4rxnuR-.js → LpDrawer-RRamxbLV.js} +1 -1
  36. package/dist/chunks/{LpDropdownMenu-BgZ0Z_Im.js → LpDropdownMenu-Dl7Z4ZhS.js} +2 -2
  37. package/dist/chunks/{LpEditable-CjxuOVh_.js → LpEditable-D3PvD6Zm.js} +5 -5
  38. package/dist/chunks/{LpEmptyState-CKFTtQgJ.js → LpEmptyState-EQHiRYhG.js} +7 -3
  39. package/dist/chunks/{LpFieldNotice-CPI8EwtU.js → LpFieldNotice-CzQ2_5rQ.js} +1 -1
  40. package/dist/chunks/{LpFileTree-BH0gNlSJ.js → LpFileTree-D7KTaJiM.js} +2 -2
  41. package/dist/chunks/{LpFileTreeNode-gPVB1oXM.js → LpFileTreeNode-14GVqQTd.js} +5 -5
  42. package/dist/chunks/{LpFormField-gRASfyj2.js → LpFormField-MSQhRCTz.js} +2 -2
  43. package/dist/chunks/{LpGauge-BdjPGnqw.js → LpGauge-BO1PYYqD.js} +2 -2
  44. package/dist/chunks/{LpHintLayer-C2asBIfs.js → LpHintLayer-CmYm37z7.js} +2 -2
  45. package/dist/chunks/{LpHints-CNZQ76JV.js → LpHints-iLIRUkCj.js} +3 -3
  46. package/dist/chunks/{LpIcon-DkP-9mzK.js → LpIcon--5WSb3jJ.js} +1 -1
  47. package/dist/chunks/{LpInfraNode-yTh4-5qa.js → LpInfraNode-BT0VQrOa.js} +2 -2
  48. package/dist/chunks/{LpInput-2H-5ILOB.js → LpInput-1eNvtzXs.js} +2 -2
  49. package/dist/chunks/{LpLightbox-lAt5uIfx.js → LpLightbox-DWw6OSA4.js} +3 -3
  50. package/dist/chunks/{LpLoader-DjJEX8hL.js → LpLoader-B82_CcV0.js} +1 -1
  51. package/dist/chunks/{LpLogViewer-x78YfhJf.js → LpLogViewer-pzanZASC.js} +6 -6
  52. package/dist/chunks/{LpMapPicker-Dp5RSCi9.js → LpMapPicker-DNIUvF6V.js} +2 -2
  53. package/dist/chunks/{LpMediaGallery-D92oJfY_.js → LpMediaGallery-Dx8xzg5N.js} +3 -3
  54. package/dist/chunks/{LpModal-CpH0bNo7.js → LpModal-BnjZ2iem.js} +1 -1
  55. package/dist/chunks/{LpModalNav-BH4ANQGJ.js → LpModalNav-Cu04W1pV.js} +4 -4
  56. package/dist/chunks/{LpNotificationBell-BdUQqZhs.js → LpNotificationBell-PSQqFqEv.js} +8 -8
  57. package/dist/chunks/{LpNumberField-DZY3Z670.js → LpNumberField-e0IBkWU1.js} +1 -1
  58. package/dist/chunks/{LpNumberFlow-5khYxsfX.js → LpNumberFlow-CcxEGUy0.js} +10 -7
  59. package/dist/chunks/{LpPagination-DbeACavM.js → LpPagination-BIIiYHXR.js} +1 -1
  60. package/dist/chunks/{LpPaletteTheme-CqWNRwHE.js → LpPaletteTheme-D05ywM1W.js} +8 -6
  61. package/dist/chunks/{LpPasswordInput-BzAZsf5V.js → LpPasswordInput-vdcw-koG.js} +2 -2
  62. package/dist/chunks/{LpPhoneInput-BMpd-0QR.js → LpPhoneInput-CETEp5W7.js} +2 -2
  63. package/dist/chunks/{LpPie-DdFSTbGE.js → LpPie-BlWDxCtG.js} +6 -6
  64. package/dist/chunks/{LpPipeline-zjEM1Lwc.js → LpPipeline-BXjoxkq_.js} +4 -4
  65. package/dist/chunks/{LpProgress-BX33EKkN.js → LpProgress-rOlyafqp.js} +1 -1
  66. package/dist/chunks/{LpQuota-DqNv1yjs.js → LpQuota-B12yijdB.js} +3 -3
  67. package/dist/chunks/{LpRadioGroup-CsXeV9Ct.js → LpRadioGroup-ggaPL92T.js} +1 -1
  68. package/dist/chunks/{LpRangeCalendar-l-0nSFmY.js → LpRangeCalendar-CYYUYqJ7.js} +1 -1
  69. package/dist/chunks/{LpRelativeTime-UM8EkfrN.js → LpRelativeTime-DwgCb5-8.js} +1 -1
  70. package/dist/chunks/{LpRepeater-OvzByE5L.js → LpRepeater-fliVBcN7.js} +3 -3
  71. package/dist/chunks/LpRollingText-B54Pje7P.js +86 -0
  72. package/dist/chunks/{LpScramble-TDO5Kdou.js → LpScramble-Cw-af0Yh.js} +1 -1
  73. package/dist/chunks/{LpSecretField-DwQiPOko.js → LpSecretField-D_IMxEGW.js} +7 -7
  74. package/dist/chunks/{LpSection-DcQsieHG.js → LpSection-DFK_uoJE.js} +1 -1
  75. package/dist/chunks/{LpSegmented-D2VST0o3.js → LpSegmented-CeACsMMA.js} +4 -2
  76. package/dist/chunks/{LpSelect-DYX2-u5A.js → LpSelect-Cxq4yBGd.js} +7 -7
  77. package/dist/chunks/LpShift-BAp8lGoa.js +160 -0
  78. package/dist/chunks/LpSidebar-BftYsk5d.js +494 -0
  79. package/dist/chunks/LpSidebarNav-BzNWMZfc.js +357 -0
  80. package/dist/chunks/LpSidebarRow-BIReMT1f.js +302 -0
  81. package/dist/chunks/{LpStat-DIsQfbeD.js → LpStat-BFCM6fUn.js} +9 -7
  82. package/dist/chunks/{LpStatus-BQU1TUsv.js → LpStatus-BWTLVPVh.js} +3 -3
  83. package/dist/chunks/{LpStepMark-CXa7DJy_.js → LpStepMark-DkdkebLC.js} +3 -3
  84. package/dist/chunks/{LpStepper-C9RM_yTG.js → LpStepper-BgqjKrqg.js} +1 -1
  85. package/dist/chunks/{LpSwap-DzE6M3Mg.js → LpSwap-B5YOXgtx.js} +1 -1
  86. package/dist/chunks/{LpSwitch-bGZxUDSx.js → LpSwitch-DyrRes7s.js} +1 -1
  87. package/dist/chunks/LpTabFace-D6BMCbAP.js +77 -0
  88. package/dist/chunks/{LpTableFilter-CTZMqFDU.js → LpTableFilter-Dkzamzkl.js} +3 -3
  89. package/dist/chunks/{LpTagsInput-DrjmrOge.js → LpTagsInput-DAZNNehI.js} +5 -5
  90. package/dist/chunks/{LpTextarea-BbYqmG1S.js → LpTextarea-DbmVPj2X.js} +32 -7
  91. package/dist/chunks/{useTheme-BOV_-AiY.js → LpThemeProvider-8MG6yqU9.js} +66 -1
  92. package/dist/chunks/{LpThemeStudio-B2nPaG-h.js → LpThemeStudio-DWtG6OZZ.js} +140 -106
  93. package/dist/chunks/LpThemeStudioSection-B7bt8Iyg.js +32 -0
  94. package/dist/chunks/{LpThemeSwitcher-C0A5sg_E.js → LpThemeSwitcher-Dcx-VkSk.js} +9 -8
  95. package/dist/chunks/{LpTimeField-DQ8WvPr2.js → LpTimeField-BawSVsmt.js} +1 -1
  96. package/dist/chunks/{LpToaster-BTTl_wuy.js → LpToaster-50hp_B5O.js} +2 -2
  97. package/dist/chunks/{LpTopologyCanvas-B0wEvAoq.js → LpTopologyCanvas-CnUh8uzS.js} +43 -13
  98. package/dist/chunks/{LpTour-BgCfNlaW.js → LpTour-DC3LTlxg.js} +1 -1
  99. package/dist/chunks/{LpTruncate-TcqfkyJM.js → LpTruncate-Ced4gquB.js} +1 -1
  100. package/dist/chunks/{LpUptimeBar-DRadGq4Y.js → LpUptimeBar-CuB0LX8K.js} +4 -4
  101. package/dist/chunks/{MenuTitle-Dab3EGOp.js → MenuTitle-BmCNNTKH.js} +1 -1
  102. package/dist/chunks/{OptionContent-DPzNsybd.js → OptionContent-22UT8zLn.js} +1 -1
  103. package/dist/chunks/{PickerField-DwLw7j0L.js → PickerField-Bfh8id3d.js} +2 -2
  104. package/dist/chunks/{RevealToggle-BVFM_FAU.js → RevealToggle-B0ITuIX-.js} +2 -2
  105. package/dist/chunks/{TourCard--2IfgnUU.js → TourCard-CBkj1GYi.js} +5 -5
  106. package/dist/chunks/{icons-CLu1iR1j.js → icons-DMITlfw5.js} +1 -1
  107. package/dist/chunks/{palette-BiY7yQZL.js → palette-Cvxl_1tM.js} +1 -1
  108. package/dist/component-names.d.ts +1 -1
  109. package/dist/components/ContextMenuItems.vue.js +1 -1
  110. package/dist/components/DropzoneLinkField.vue.js +1 -1
  111. package/dist/components/FieldWrapper.vue.js +1 -1
  112. package/dist/components/LayoutCanvas.vue.js +1 -1
  113. package/dist/components/LayoutNode.vue.js +1 -1
  114. package/dist/components/LpActionBar.vue.js +1 -1
  115. package/dist/components/LpAddressInput.vue.js +1 -1
  116. package/dist/components/LpAlert.vue.js +1 -1
  117. package/dist/components/LpAppShell.vue.d.ts +67 -2
  118. package/dist/components/LpAppShell.vue.js +1 -1
  119. package/dist/components/LpAutocomplete.vue.js +1 -1
  120. package/dist/components/LpAvatar.vue.js +1 -1
  121. package/dist/components/LpBadge.vue.js +1 -1
  122. package/dist/components/LpBottomNav.vue.d.ts +28 -0
  123. package/dist/components/LpBottomNav.vue.js +4 -0
  124. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  125. package/dist/components/LpButton.vue.js +1 -1
  126. package/dist/components/LpCalendar.vue.js +1 -1
  127. package/dist/components/LpCard.vue.js +1 -1
  128. package/dist/components/LpChart.vue.js +26 -8
  129. package/dist/components/LpCheckbox.vue.js +1 -1
  130. package/dist/components/LpCodeBlock.vue.js +1 -1
  131. package/dist/components/LpCodeDisplay.vue.js +1 -1
  132. package/dist/components/LpColorPicker.vue.d.ts +15 -2
  133. package/dist/components/LpColorPicker.vue.js +1 -1
  134. package/dist/components/LpCommandPalette.vue.js +47 -26
  135. package/dist/components/LpCommandScope.vue.js +1 -1
  136. package/dist/components/LpConfirmDialog.vue.js +1 -1
  137. package/dist/components/LpConfirmHost.vue.js +1 -1
  138. package/dist/components/LpContextMenu.vue.js +1 -1
  139. package/dist/components/LpCopyButton.vue.js +1 -1
  140. package/dist/components/LpCountdown.vue.js +1 -1
  141. package/dist/components/LpDataList.vue.js +1 -1
  142. package/dist/components/LpDatePicker.vue.js +1 -1
  143. package/dist/components/LpDateRangePicker.vue.js +1 -1
  144. package/dist/components/LpDisclosure.vue.js +1 -1
  145. package/dist/components/LpDock.vue.js +1 -1
  146. package/dist/components/LpDrawer.vue.js +1 -1
  147. package/dist/components/LpDropdownMenu.vue.js +1 -1
  148. package/dist/components/LpDropzone.vue.js +2 -2
  149. package/dist/components/LpEditable.vue.js +1 -1
  150. package/dist/components/LpEmptyState.vue.d.ts +2 -0
  151. package/dist/components/LpEmptyState.vue.js +1 -1
  152. package/dist/components/LpFieldNotice.vue.js +1 -1
  153. package/dist/components/LpFileTree.vue.js +1 -1
  154. package/dist/components/LpFileTreeNode.vue.js +1 -1
  155. package/dist/components/LpFormField.vue.js +1 -1
  156. package/dist/components/LpGauge.vue.js +1 -1
  157. package/dist/components/LpHintLayer.vue.js +1 -1
  158. package/dist/components/LpHints.vue.js +1 -1
  159. package/dist/components/LpIcon.vue.js +1 -1
  160. package/dist/components/LpImageEditor.vue.js +2 -2
  161. package/dist/components/LpInfraNode.vue.js +1 -1
  162. package/dist/components/LpInput.vue.js +1 -1
  163. package/dist/components/LpLightbox.vue.js +1 -1
  164. package/dist/components/LpLoader.vue.js +1 -1
  165. package/dist/components/LpLogViewer.vue.d.ts +1 -1
  166. package/dist/components/LpLogViewer.vue.js +1 -1
  167. package/dist/components/LpMapPicker.vue.js +1 -1
  168. package/dist/components/LpMediaGallery.vue.js +1 -1
  169. package/dist/components/LpModal.vue.js +1 -1
  170. package/dist/components/LpModalNav.vue.js +1 -1
  171. package/dist/components/LpNotificationBell.vue.js +1 -1
  172. package/dist/components/LpNumberField.vue.js +1 -1
  173. package/dist/components/LpNumberFlow.vue.js +1 -1
  174. package/dist/components/LpPagination.vue.js +1 -1
  175. package/dist/components/LpPaletteFacts.vue.js +2 -2
  176. package/dist/components/LpPaletteHeader.vue.js +2 -2
  177. package/dist/components/LpPaletteStats.vue.js +2 -2
  178. package/dist/components/LpPaletteTheme.vue.d.ts +2 -0
  179. package/dist/components/LpPaletteTheme.vue.js +1 -1
  180. package/dist/components/LpPasswordInput.vue.js +1 -1
  181. package/dist/components/LpPhoneInput.vue.js +1 -1
  182. package/dist/components/LpPie.vue.js +1 -1
  183. package/dist/components/LpPipeline.vue.js +1 -1
  184. package/dist/components/LpProgress.vue.js +1 -1
  185. package/dist/components/LpQuota.vue.js +1 -1
  186. package/dist/components/LpRadioGroup.vue.js +1 -1
  187. package/dist/components/LpRangeCalendar.vue.js +1 -1
  188. package/dist/components/LpRelativeTime.vue.js +1 -1
  189. package/dist/components/LpRepeater.vue.js +1 -1
  190. package/dist/components/LpRollingText.vue.d.ts +2 -2
  191. package/dist/components/LpRollingText.vue.js +1 -1
  192. package/dist/components/LpScramble.vue.js +1 -1
  193. package/dist/components/LpSecretField.vue.js +1 -1
  194. package/dist/components/LpSection.vue.js +1 -1
  195. package/dist/components/LpSegmented.vue.js +1 -1
  196. package/dist/components/LpSelect.vue.js +1 -1
  197. package/dist/components/LpShift.vue.d.ts +20 -0
  198. package/dist/components/LpShift.vue.js +1 -1
  199. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  200. package/dist/components/LpSidebar.vue.d.ts +74 -23
  201. package/dist/components/LpSidebar.vue.js +1 -1
  202. package/dist/components/LpSidebarNav.vue.d.ts +30 -2
  203. package/dist/components/LpSidebarNav.vue.js +1 -1
  204. package/dist/components/LpSidebarRow.vue.d.ts +49 -0
  205. package/dist/components/LpSidebarRow.vue.js +4 -0
  206. package/dist/components/LpStat.vue.d.ts +11 -0
  207. package/dist/components/LpStat.vue.js +1 -1
  208. package/dist/components/LpStatus.vue.js +1 -1
  209. package/dist/components/LpStepMark.vue.js +1 -1
  210. package/dist/components/LpStepper.vue.js +1 -1
  211. package/dist/components/LpSwap.vue.js +1 -1
  212. package/dist/components/LpSwitch.vue.js +1 -1
  213. package/dist/components/LpTabFace.vue.d.ts +14 -0
  214. package/dist/components/LpTabFace.vue.js +4 -0
  215. package/dist/components/LpTable.vue.js +11 -11
  216. package/dist/components/LpTableFilter.vue.js +1 -1
  217. package/dist/components/LpTabs.vue.d.ts +12 -4
  218. package/dist/components/LpTabs.vue.js +22 -74
  219. package/dist/components/LpTagsInput.vue.js +1 -1
  220. package/dist/components/LpTextarea.vue.d.ts +19 -1
  221. package/dist/components/LpTextarea.vue.js +1 -1
  222. package/dist/components/LpThemeProvider.vue.js +1 -1
  223. package/dist/components/LpThemeStudio.vue.d.ts +2 -2
  224. package/dist/components/LpThemeStudio.vue.js +1 -1
  225. package/dist/components/LpThemeStudioSection.vue.d.ts +4 -1
  226. package/dist/components/LpThemeStudioSection.vue.js +1 -1
  227. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  228. package/dist/components/LpTimeField.vue.js +1 -1
  229. package/dist/components/LpToaster.vue.js +1 -1
  230. package/dist/components/LpTopologyCanvas.vue.d.ts +3 -1
  231. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  232. package/dist/components/LpTour.vue.js +1 -1
  233. package/dist/components/LpTruncate.vue.js +1 -1
  234. package/dist/components/LpUptimeBar.vue.js +1 -1
  235. package/dist/components/MenuTitle.vue.js +1 -1
  236. package/dist/components/OptionContent.vue.js +1 -1
  237. package/dist/components/PickerField.vue.js +1 -1
  238. package/dist/components/RevealToggle.vue.js +1 -1
  239. package/dist/components/TourCard.vue.js +1 -1
  240. package/dist/components/chart.d.ts +14 -0
  241. package/dist/components/chart.js +13 -0
  242. package/dist/components/format.d.ts +10 -3
  243. package/dist/components/format.js +14 -2
  244. package/dist/components/menuWhen.js +1 -1
  245. package/dist/components/rolling.d.ts +5 -0
  246. package/dist/components/rolling.js +23 -0
  247. package/dist/components/sidebar.d.ts +33 -0
  248. package/dist/components/tabs.d.ts +3 -0
  249. package/dist/components/tabs.js +6 -0
  250. package/dist/composables/usePaletteCommands.js +1 -0
  251. package/dist/composables/useReveal.js +1 -22
  252. package/dist/icons-all.js +1 -1
  253. package/dist/index.d.ts +2 -1
  254. package/dist/index.js +156 -154
  255. package/dist/theme/palette.d.ts +3 -0
  256. package/package.json +1 -1
  257. package/src/component-names.ts +1 -0
  258. package/src/components/LpAppShell.vue +75 -7
  259. package/src/components/LpBottomNav.vue +84 -0
  260. package/src/components/LpChart.vue +23 -3
  261. package/src/components/LpColorPicker.vue +32 -1
  262. package/src/components/LpCommandPalette.vue +23 -5
  263. package/src/components/LpEmptyState.vue +4 -1
  264. package/src/components/LpNumberFlow.vue +23 -9
  265. package/src/components/LpPaletteTheme.vue +3 -3
  266. package/src/components/LpRollingText.vue +27 -2
  267. package/src/components/LpSegmented.vue +3 -1
  268. package/src/components/LpSelect.vue +5 -4
  269. package/src/components/LpShift.vue +174 -2
  270. package/src/components/LpSidebar.vue +304 -18
  271. package/src/components/LpSidebarNav.vue +200 -70
  272. package/src/components/LpSidebarRow.vue +238 -0
  273. package/src/components/LpStat.vue +12 -4
  274. package/src/components/LpTabFace.vue +52 -0
  275. package/src/components/LpTabs.vue +15 -42
  276. package/src/components/LpTextarea.vue +34 -4
  277. package/src/components/LpThemeStudio.vue +92 -56
  278. package/src/components/LpThemeStudioSection.vue +15 -7
  279. package/src/components/LpThemeSwitcher.vue +4 -1
  280. package/src/components/LpTopologyCanvas.vue +35 -4
  281. package/src/components/chart.ts +26 -0
  282. package/src/components/format.ts +31 -5
  283. package/src/components/rolling.ts +28 -0
  284. package/src/components/sidebar.ts +33 -0
  285. package/src/components/tabs.ts +9 -0
  286. package/src/composables/usePaletteCommands.ts +3 -0
  287. package/src/composables/useReveal.ts +7 -30
  288. package/src/index.ts +2 -1
  289. package/src/kit-icons.ts +1 -1
  290. package/src/theme/palette.ts +4 -2
  291. package/dist/assets/LpTabs-CO3EcsNV.css +0 -18
  292. package/dist/chunks/LpRollingText-DiL717LK.js +0 -51
  293. package/dist/chunks/LpShift-DXNsauQZ.js +0 -40
  294. package/dist/chunks/LpSidebar-D44peKq2.js +0 -256
  295. package/dist/chunks/LpSidebarNav--VwkXO_a.js +0 -211
  296. package/dist/chunks/LpThemeProvider-DcbfOxsU.js +0 -69
  297. package/dist/chunks/LpThemeStudioSection-BwhqYDLf.js +0 -20
@@ -33,6 +33,7 @@ export default { inheritAttrs: false }
33
33
  */
34
34
  import { computed, ref, useId, watch } from "vue"
35
35
  import { useLabels } from "../composables/useLpConfig"
36
+ import LpBottomNav from "./LpBottomNav.vue"
36
37
  import LpButton from "./LpButton.vue"
37
38
  import LpIcon from "./LpIcon.vue"
38
39
  import LpScrollArea from "./LpScrollArea.vue"
@@ -82,11 +83,37 @@ const props = withDefaults(
82
83
  loading?: boolean
83
84
  /** Text overrides: the burger's name and the skip link. */
84
85
  labels?: Partial<{ openMenu: string; skipToContent: string }>
86
+ /** Icon-only rail (v-model:compact). Forwarded to LpSidebar. */
87
+ compact?: boolean
88
+ /** The person sizes the rail and folds it to icons. Forwarded to LpSidebar. */
89
+ resizable?: boolean
90
+ /** Rows can be pinned to the top (v-model:pinned). Forwarded to LpSidebar. */
91
+ pinnable?: boolean
92
+ pinned?: string[]
93
+ /** Remember folded groups, width, compact and pins under this key. Forwarded to LpSidebar. */
94
+ persist?: string
95
+ /** Rest time before a row's `preview` card opens. Forwarded to LpSidebar. */
96
+ previewDelay?: number
97
+ /** The key that folds the rail; false turns it off. Forwarded to LpSidebar. */
98
+ compactHotkey?: string | false
99
+ /** Marks a non-production copy along the top of the rail. Forwarded to LpSidebar. */
100
+ environment?: string | { label: string; tone?: "warning" | "danger" | "action" | "brand" }
101
+ /** The app's hint system; rows with a `hintPage` show unread news as a dot. Forwarded to LpSidebar. */
102
+ hints?: { unreadNews: (page?: string) => readonly unknown[] }
103
+ /**
104
+ * On phones, the main pages along the bottom instead of the burger, with
105
+ * "More" for the rest (it opens the drawer). `true` takes the first four
106
+ * pages, a number that many, a list of ids those pages.
107
+ */
108
+ bottomNav?: boolean | number | string[]
85
109
  }>(),
86
110
  {
87
111
  mobileBreakpoint: "lg",
88
112
  divider: true,
89
113
  contentClass: "mx-auto w-full max-w-6xl",
114
+ // Unset, not false: an absent boolean would hand the sidebar "expanded"
115
+ // and override the state it remembers.
116
+ compact: undefined,
90
117
  },
91
118
  )
92
119
 
@@ -94,11 +121,13 @@ const emit = defineEmits<{
94
121
  (e: "update:modelValue", id: string): void
95
122
  (e: "update:collapsed", keys: string[]): void
96
123
  (e: "select", item: SidebarItem): void
124
+ (e: "update:compact", value: boolean): void
125
+ (e: "update:pinned", ids: string[]): void
97
126
  }>()
98
127
 
99
128
  // Normalise items/sections to a flat list for the title computation.
100
129
  const flatItems = computed<SidebarItem[]>(() =>
101
- props.sections ? props.sections.flatMap((s) => s.items) : (props.items ?? []),
130
+ (props.sections ? props.sections.flatMap((s) => s.items) : (props.items ?? [])).flatMap((i) => [i, ...(i.children ?? [])]),
102
131
  )
103
132
 
104
133
  // Default active test: exact match for "/", prefix match (on a segment
@@ -179,7 +208,23 @@ const burgerHidden: Record<NonNullable<typeof props.mobileBreakpoint>, string> =
179
208
  lg: "lg:hidden",
180
209
  xl: "xl:hidden",
181
210
  }
182
- const burgerClass = computed(() => burgerHidden[props.mobileBreakpoint])
211
+ const burgerClass = computed(() => (props.bottomNav ? "hidden" : burgerHidden[props.mobileBreakpoint]))
212
+
213
+ const topItems = computed<SidebarItem[]>(() => (props.sections ? props.sections.flatMap((s) => s.items) : (props.items ?? [])))
214
+ const bottomItems = computed<SidebarItem[]>(() => {
215
+ const want = props.bottomNav
216
+ if (!want) return []
217
+ if (Array.isArray(want)) return want.map((id) => topItems.value.find((i) => i.id === id)).filter((i): i is SidebarItem => !!i)
218
+ return topItems.value.slice(0, want === true ? 4 : want)
219
+ })
220
+ const moreActive = computed(() => {
221
+ const shown = new Set(bottomItems.value.map((i) => i.id))
222
+ return flatItems.value.some((i) => !shown.has(i.id) && activeTest.value(i)) && !bottomItems.value.some((i) => activeTest.value(i))
223
+ })
224
+ function onBottom(item: SidebarItem) {
225
+ onModel(item.id)
226
+ onSelect(item)
227
+ }
183
228
 
184
229
  // Expose the burger trigger so a host can omit its own header entirely.
185
230
  const _openDrawer = () => (drawerOpen.value = true)
@@ -215,20 +260,31 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
215
260
  :loading="loading"
216
261
  :collapsed="collapsed"
217
262
  :auto-collapse-after="autoCollapseAfter"
263
+ :compact="compact"
264
+ :resizable="resizable"
265
+ :pinnable="pinnable"
266
+ :pinned="pinned"
267
+ :persist="persist"
268
+ :preview-delay="previewDelay"
269
+ :compact-hotkey="compactHotkey"
270
+ :environment="environment"
271
+ :hints="hints"
218
272
  class="shrink-0"
219
273
  @update:model-value="onModel"
274
+ @update:compact="(v) => emit('update:compact', v)"
275
+ @update:pinned="(v) => emit('update:pinned', v)"
220
276
  @update:open="(v) => (drawerOpen = v)"
221
277
  @update:collapsed="(v) => emit('update:collapsed', v)"
222
278
  @select="onSelect"
223
279
  >
224
- <template v-if="$slots.logo" #header>
225
- <slot name="logo" />
280
+ <template v-if="$slots.logo" #header="{ compact: folded }">
281
+ <slot name="logo" :compact="folded" />
226
282
  </template>
227
283
  <template v-if="$slots['sidebar-actions']" #actions>
228
284
  <slot name="sidebar-actions" />
229
285
  </template>
230
- <template v-if="$slots['sidebar-footer']" #footer>
231
- <slot name="sidebar-footer" />
286
+ <template v-if="$slots['sidebar-footer']" #footer="{ compact: folded }">
287
+ <slot name="sidebar-footer" :compact="folded" />
232
288
  </template>
233
289
  <template v-if="$slots['sidebar-item']" #item="slotProps">
234
290
  <slot name="sidebar-item" v-bind="slotProps" />
@@ -261,7 +317,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
261
317
  translation that wraps a long title onto a second line, otherwise
262
318
  re-lays the whole bar in one frame. Width is left free so the
263
319
  truncation still resolves against the space actually available. -->
264
- <LpShift axis="height" class="flex min-w-0 flex-1 items-center">
320
+ <LpShift axis="height" presence class="flex min-w-0 flex-1 items-center">
265
321
  <slot name="header-title">
266
322
  <h1 class="truncate text-lg font-semibold">{{ pageTitle }}</h1>
267
323
  </slot>
@@ -283,6 +339,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
283
339
  the title keeps a third and every button stays reachable. -->
284
340
  <LpShift
285
341
  axis="width"
342
+ anchor="end"
343
+ presence
286
344
  class="ml-auto flex max-w-[66%] shrink items-center gap-2 overflow-x-auto lp-scrollbar-none md:max-w-none md:shrink-0 md:overflow-visible"
287
345
  >
288
346
  <slot name="header-actions" />
@@ -304,6 +362,16 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
304
362
  <slot />
305
363
  </LpScrollArea>
306
364
  </main>
365
+ <LpBottomNav
366
+ v-if="bottomNav"
367
+ :class="burgerHidden[mobileBreakpoint]"
368
+ :items="bottomItems"
369
+ :is-active="activeTest"
370
+ more
371
+ :more-active="moreActive"
372
+ @select="onBottom"
373
+ @more="drawerOpen = true"
374
+ />
307
375
  </div>
308
376
 
309
377
  <slot name="overlays" />
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * A phone app's bottom navigation: the main pages along the bottom edge, each
4
+ * a real link (or a button that emits `select`), the current one marked by a
5
+ * bar that slides between them, and "More" last for everything else.
6
+ *
7
+ * It looks exactly like LpTabs' bottom variant, sharing its bar and its tab
8
+ * face, but it is navigation between pages, not tabs within one: links that a
9
+ * screen reader announces as links, that open in a new tab when asked to, and
10
+ * that need no router call from the app. LpAppShell shows one on phones with
11
+ * `bottomNav`; an app with a shell of its own uses it directly.
12
+ */
13
+ import { Motion } from "motion-v"
14
+ import { useId } from "vue"
15
+ import { usePillTransition } from "../composables/usePillTransition"
16
+ import { useLabels, useLinkComponent } from "../composables/useLpConfig"
17
+ import { resolveLink, type ResolvedLink } from "./link"
18
+ import LpTabFace from "./LpTabFace.vue"
19
+ import type { SidebarItem } from "./sidebar"
20
+ import { BOTTOM_BAR, BOTTOM_ITEM, BOTTOM_MARKER } from "./tabs"
21
+
22
+ const props = withDefaults(
23
+ defineProps<{
24
+ items: SidebarItem[]
25
+ /** Active page id. Ignored when `isActive` is given. */
26
+ modelValue?: string
27
+ isActive?: (item: SidebarItem) => boolean
28
+ /** A last "More" entry that emits `more` (the shell opens its drawer). */
29
+ more?: boolean
30
+ /** Marks "More" current: the page shown is one of those behind it. */
31
+ moreActive?: boolean
32
+ label?: string
33
+ labels?: Partial<{ nav: string; more: string }>
34
+ }>(),
35
+ { more: false, moreActive: false },
36
+ )
37
+
38
+ const emit = defineEmits<{
39
+ (e: "select", item: SidebarItem): void
40
+ (e: "more"): void
41
+ }>()
42
+
43
+ const text = useLabels("bottomNav", { nav: "Main", more: "More" }, () => props.labels)
44
+ const linkComponent = useLinkComponent()
45
+ const pillTransition = usePillTransition()
46
+ const markerId = `lp-bottom-nav-${useId()}`
47
+
48
+ function active(item: SidebarItem): boolean {
49
+ return props.isActive ? props.isActive(item) : item.id === props.modelValue
50
+ }
51
+
52
+ function rowOf(item: SidebarItem): ResolvedLink {
53
+ const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href })
54
+ return link ?? { is: "button", props: { type: "button", disabled: item.disabled || undefined } }
55
+ }
56
+
57
+ const ROW = "relative flex items-center justify-center outline-none transition-[color,opacity] duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
58
+ </script>
59
+
60
+ <template>
61
+ <nav :aria-label="label ?? text.nav" :class="BOTTOM_BAR">
62
+ <component
63
+ :is="rowOf(item).is"
64
+ v-for="item in items"
65
+ :key="item.id"
66
+ v-bind="rowOf(item).props"
67
+ :aria-current="active(item) ? 'page' : undefined"
68
+ :class="[ROW, BOTTOM_ITEM, item.disabled ? 'cursor-not-allowed opacity-45' : active(item) ? 'text-brand' : 'text-muted']"
69
+ @click="emit('select', item)"
70
+ >
71
+ <Motion v-if="active(item)" :layout-id="markerId" :transition="pillTransition" :class="[BOTTOM_MARKER, 'bg-brand']" />
72
+ <LpTabFace :item="item" bottom :active="active(item)" />
73
+ </component>
74
+ <button
75
+ v-if="more"
76
+ type="button"
77
+ :class="[ROW, BOTTOM_ITEM, moreActive ? 'text-brand' : 'text-muted']"
78
+ @click="emit('more')"
79
+ >
80
+ <Motion v-if="moreActive" :layout-id="markerId" :transition="pillTransition" :class="[BOTTOM_MARKER, 'bg-brand']" />
81
+ <LpTabFace :item="{ label: text.more, icon: 'lucide:menu' }" bottom :active="moreActive" />
82
+ </button>
83
+ </nav>
84
+ </template>
@@ -41,6 +41,7 @@ import {
41
41
  pointCount,
42
42
  resolveLabels,
43
43
  xAt,
44
+ xLabelIndices,
44
45
  yAt,
45
46
  type Box,
46
47
  type ChartMarker,
@@ -255,6 +256,23 @@ const allSeries = computed(() =>
255
256
  )
256
257
  const visible = computed(() => allSeries.value.filter((s) => !hidden.value.has(s.id)))
257
258
 
259
+ // A series asked to start hidden is hidden once, when it first appears: the
260
+ // legend owns it from then on, and a data refresh must not switch it back off.
261
+ const seeded = new Set<string>()
262
+ watch(
263
+ () => props.series.map((s, i) => [`${i}:${s.name}`, !!s.hidden] as const),
264
+ (entries) => {
265
+ let next: Set<string> | null = null
266
+ for (const [id, off] of entries) {
267
+ if (seeded.has(id)) continue
268
+ seeded.add(id)
269
+ if (off) (next ??= new Set(hidden.value)).add(id)
270
+ }
271
+ if (next && next.size < entries.length) hidden.value = next
272
+ },
273
+ { immediate: true },
274
+ )
275
+
258
276
  /*
259
277
  * How present each series is, 1 to 0, driven per frame rather than by CSS.
260
278
  *
@@ -664,9 +682,11 @@ const xLabelStride = computed(() => {
664
682
  })
665
683
 
666
684
  const xLabels = computed(() =>
667
- labels.value
668
- .map((label, i) => ({ label, i, x: xAt(i, count.value, box.value, banded.value) }))
669
- .filter(({ i }) => i % xLabelStride.value === 0 || i === count.value - 1),
685
+ xLabelIndices(labels.value.length, xLabelStride.value).map((i) => ({
686
+ label: labels.value[i],
687
+ i,
688
+ x: xAt(i, count.value, box.value, banded.value),
689
+ })),
670
690
  )
671
691
 
672
692
  // ── marks ──────────────────────────────────────
@@ -104,6 +104,17 @@ const props = withDefaults(
104
104
  size?: "sm" | "md" | "lg"
105
105
  /** Trigger text beside the swatch. Defaults to the colour's own value. */
106
106
  label?: string
107
+ /**
108
+ * `swatch` makes the trigger a round chip of the colour alone, for a row of
109
+ * presets that ends in "any colour" or a toolbar with no room for a field.
110
+ * The panel is the same.
111
+ */
112
+ variant?: "field" | "swatch"
113
+ /**
114
+ * The swatch is the current choice. False draws it as "any colour" (a
115
+ * colour wheel), for when the value came from presets beside it.
116
+ */
117
+ selected?: boolean
107
118
  /** Accessible name for the trigger when it carries no visible text. */
108
119
  ariaLabel?: string
109
120
  /** Popover side, as LpPopover. */
@@ -156,6 +167,8 @@ const props = withDefaults(
156
167
  recent: false,
157
168
  recentKey: "lp.color.recent",
158
169
  recentLimit: 8,
170
+ variant: "field",
171
+ selected: true,
159
172
  },
160
173
  )
161
174
 
@@ -372,6 +385,9 @@ function tickClass(value: string) {
372
385
  return rgb && isLight(rgbToHsv(rgb)) ? "text-black" : "text-white"
373
386
  }
374
387
 
388
+ const SWATCH_SIZE = { sm: "size-6", md: "size-7", lg: "size-8" }
389
+ const ANY_COLOUR = "conic-gradient(from 90deg, #ff5c5c, #ffd23f, #39e58c, #2dd4bf, #00bcff, #7c5cff, #ff5ca8, #ff5c5c)"
390
+
375
391
  const triggerSize = {
376
392
  sm: "h-(--size-control-sm) text-xs",
377
393
  md: "h-(--size-control-md) text-sm",
@@ -634,10 +650,25 @@ const guardOutside = useOutsideGuard()
634
650
  menu's trigger and the popover's trigger both. LpContextMenu renders
635
651
  just its slot when `items` is empty, so `menu="none"` with no items of
636
652
  your own leaves the native browser menu in place. -->
637
- <PopoverAnchor class="block w-full">
653
+ <PopoverAnchor :class="variant === 'swatch' ? 'inline-block' : 'block w-full'">
638
654
  <LpContextMenu :items="menuItems">
639
655
  <PopoverTrigger as-child>
640
656
  <button
657
+ v-if="variant === 'swatch'"
658
+ type="button"
659
+ :id="field.id"
660
+ :disabled="field.disabled"
661
+ :aria-label="ariaLabel ?? current"
662
+ :title="current"
663
+ class="relative shrink-0 overflow-hidden rounded-full outline-none ring-2 ring-offset-2 ring-offset-surface-raised transition-[box-shadow,scale] duration-(--duration-fast) hover:scale-110 focus-visible:ring-ring data-[state=open]:ring-brand disabled:cursor-not-allowed disabled:opacity-55"
664
+ :class="[SWATCH_SIZE[size], selected ? 'ring-ink' : 'ring-line']"
665
+ :style="{ background: CHECKERBOARD }"
666
+ >
667
+ <span class="block size-full" :style="{ background: selected ? previewCss : ANY_COLOUR }" />
668
+ <LpIcon v-if="!selected" name="lucide:pipette" :size="12" class="absolute inset-0 m-auto text-white drop-shadow" />
669
+ </button>
670
+ <button
671
+ v-else
641
672
  type="button"
642
673
  :id="field.id"
643
674
  :disabled="field.disabled"
@@ -1519,7 +1519,10 @@ const guardOutside = useOutsideGuard()
1519
1519
  <div class="flex gap-2"><LpButton :loading="executing || validating" @click="submitPrompt">{{ text.submit }}</LpButton><LpButton variant="ghost" @click="promptCommand = undefined; inputRef?.focus()">{{ text.backToResults }}</LpButton></div>
1520
1520
  </div>
1521
1521
  <p v-if="actionError" role="alert" class="px-4 py-2 text-sm text-danger">{{ actionError }}</p>
1522
- <div v-if="active && (canPin || selectedActions.length) && !promptCommand" class="flex items-center justify-end gap-2 border-b border-line px-3 py-1">
1522
+ <!-- Touch only: there the footer with its keys is hidden, and these are
1523
+ the only way to the row's actions and pin. On a pointer the same
1524
+ buttons live in the footer, so the list never shifts under this strip. -->
1525
+ <div v-if="active && (canPin || selectedActions.length) && !promptCommand" class="hidden items-center justify-end gap-2 border-b border-line px-3 py-1 [@media(hover:none)_and_(pointer:coarse)]:flex">
1523
1526
  <LpButton v-if="selectedActions.length" size="xs" variant="ghost" @click="openActions(active)">{{ text.actions }} <span class="text-muted">Tab</span></LpButton>
1524
1527
  <LpButton v-if="canPin" size="xs" variant="ghost" :aria-label="isPinned(active) ? text.unpin : text.pin" :aria-pressed="isPinned(active)" @click="togglePin(active)"><LpIcon name="lucide:star" :size="14" />{{ isPinned(active) ? text.unpin : text.pin }} <span class="text-muted">Alt+P</span></LpButton>
1525
1528
  </div>
@@ -1727,17 +1730,32 @@ const guardOutside = useOutsideGuard()
1727
1730
  </span>
1728
1731
  <!-- Only inside a mode: at the root Backspace just edits the query,
1729
1732
  and advertising it there would be a lie. -->
1730
- <span
1731
- v-if="selectedActions.length"
1732
- class="flex items-center gap-1 transition-colors"
1733
+ <button
1734
+ v-if="selectedActions.length && active"
1735
+ type="button"
1736
+ class="flex items-center gap-1 rounded-small outline-none transition-colors hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
1733
1737
  :class="showActionsKey && 'font-medium text-brand'"
1738
+ :aria-label="text.actions"
1739
+ @click="openActions(active)"
1734
1740
  >
1735
1741
  <kbd
1736
1742
  class="rounded-small border px-1 text-[10px] transition-colors"
1737
1743
  :class="showActionsKey ? 'border-brand' : 'border-line'"
1738
1744
  >{{ text.tab }}</kbd>
1739
1745
  {{ text.actions }}
1740
- </span>
1746
+ </button>
1747
+ <button
1748
+ v-if="canPin && active && !promptCommand"
1749
+ type="button"
1750
+ class="flex items-center gap-1 rounded-small outline-none transition-colors hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
1751
+ :aria-label="isPinned(active) ? text.unpin : text.pin"
1752
+ :aria-pressed="isPinned(active)"
1753
+ @click="togglePin(active)"
1754
+ >
1755
+ <kbd class="rounded-small border border-line px-1 text-[10px]">Alt+P</kbd>
1756
+ <LpIcon name="lucide:star" :size="11" :class="isPinned(active) && 'fill-current text-brand'" />
1757
+ {{ isPinned(active) ? text.unpin : text.pin }}
1758
+ </button>
1741
1759
  <span v-if="trail.length" class="flex items-center gap-1">
1742
1760
  <kbd class="rounded-small border border-line px-1 text-[10px]">⌫</kbd>
1743
1761
  {{ text.back }}
@@ -45,7 +45,10 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
45
45
  </div>
46
46
  <div class="flex flex-col gap-1">
47
47
  <p class="font-semibold text-ink" :class="compact ? 'text-sm' : ''">{{ title }}</p>
48
- <p v-if="description" class="max-w-sm text-sm text-muted">{{ description }}</p>
48
+ <!-- The slot is for a line a string cannot carry: a command, a link. -->
49
+ <p v-if="description || $slots.description" class="max-w-sm text-sm text-muted">
50
+ <slot name="description">{{ description }}</slot>
51
+ </p>
49
52
  </div>
50
53
  <div v-if="$slots.default" class="mt-1"><slot /></div>
51
54
  </div>
@@ -229,8 +229,8 @@ onBeforeUnmount(stop)
229
229
  * own, the same rule the dates follow: a Russian app read "4,594" otherwise.
230
230
  */
231
231
  const locale = useLocale(() => props.locale)
232
- // Only the grouped form is written in the locale; the others always use ".".
233
- const fractionMark = computed(() => (props.grouped && !props.format && !props.compact ? decimalMark(locale.value) : "."))
232
+ // The grouped and compact forms are written in the locale; the plain one always uses ".".
233
+ const fractionMark = computed(() => ((props.grouped || props.compact) && !props.format ? decimalMark(locale.value) : "."))
234
234
 
235
235
  const formatted = computed(() => formatAt(round(displayed.value), !settled.value))
236
236
 
@@ -249,10 +249,10 @@ function formatAt(v: number, padded: boolean): string {
249
249
  *
250
250
  * The decimal count is what stays fixed: one place whenever a unit is in
251
251
  * play, so the row's shape only changes at a rollover, not on every frame.
252
- * No locale here: the cells below find the fraction by its ".", and a
253
- * locale's "," would turn the fraction's columns into integer ones.
252
+ * Written in the app's locale: the cells find the fraction by that
253
+ * locale's own mark (fractionMark), so "1,2 тыс." keeps its columns.
254
254
  */
255
- return compactNumber(v, { plainDecimals: 0, fixed: true })
255
+ return compactNumber(v, { plainDecimals: 0, fixed: true, locale: locale.value, words: true })
256
256
  }
257
257
  const pad = padded ? Math.max(1, padTo.value) : 1
258
258
  if (props.grouped)
@@ -280,6 +280,8 @@ interface Cell {
280
280
  char: string
281
281
  /** A leading zero held open for a column the value hasn't reached yet. */
282
282
  isPad: boolean
283
+ /** A space between two digits: the locale's thousands separator. */
284
+ isGap: boolean
283
285
  }
284
286
 
285
287
  /*
@@ -338,7 +340,8 @@ const cells = computed<Cell[]>(() => {
338
340
  const digit = isDigit(char) ? char.charCodeAt(0) - 48 : null
339
341
  const isPad = leading && digit === 0 && i < intEnd - 1
340
342
  if (digit !== null && digit !== 0) leading = false
341
- return { key: keys[i], digit, char, isPad }
343
+ const isGap = digit === null && /\s/.test(char) && isDigit(chars[i - 1] ?? "") && isDigit(chars[i + 1] ?? "")
344
+ return { key: keys[i], digit, char, isPad, isGap }
342
345
  })
343
346
  })
344
347
 
@@ -372,7 +375,9 @@ const label = computed(
372
375
  class="lp-nf inline-flex items-baseline tabular-nums"
373
376
  >
374
377
  <span class="sr-only">{{ label }}</span>
375
- <span v-if="prefix" aria-hidden="true">{{ prefix }}</span>
378
+ <!-- `pre`: a flex item drops its leading and trailing spaces, so " ms"
379
+ as a suffix printed "12.8ms". -->
380
+ <span v-if="prefix" aria-hidden="true" class="whitespace-pre">{{ prefix }}</span>
376
381
 
377
382
  <span class="lp-nf__track inline-flex items-baseline" aria-hidden="true">
378
383
  <TransitionGroup name="lp-nf-cell">
@@ -380,7 +385,7 @@ const label = computed(
380
385
  v-for="cell in cells"
381
386
  :key="cell.key"
382
387
  class="lp-nf__cell"
383
- :class="cell.isPad ? 'lp-nf__cell--pad' : ''"
388
+ :class="{ 'lp-nf__cell--pad': cell.isPad, 'lp-nf__cell--gap': cell.isGap }"
384
389
  >
385
390
  <!-- A digit is a window onto its strip; a separator is just a glyph. -->
386
391
  <template v-if="cell.digit !== null">
@@ -399,7 +404,7 @@ const label = computed(
399
404
  </TransitionGroup>
400
405
  </span>
401
406
 
402
- <span v-if="suffix" aria-hidden="true">{{ suffix }}</span>
407
+ <span v-if="suffix" aria-hidden="true" class="whitespace-pre">{{ suffix }}</span>
403
408
  </span>
404
409
  </template>
405
410
 
@@ -486,6 +491,15 @@ const label = computed(
486
491
 
487
492
  /* Faded, so a held-open column reads as an empty wheel rather than a zero the
488
493
  number actually has. */
494
+ /*
495
+ * A space separator (ru, fr, …) takes a whole column in a monospaced or
496
+ * tabular face, so "7 271" read as "7 271". It is drawn as the narrow gap
497
+ * the locale means.
498
+ */
499
+ .lp-nf__cell--gap {
500
+ width: 0.25em;
501
+ }
502
+
489
503
  .lp-nf__cell--pad {
490
504
  opacity: 0.18;
491
505
  transition: opacity var(--duration-fast, 160ms) linear;
@@ -3,12 +3,12 @@ import { computed, ref } from "vue"
3
3
  import type { StudioState, PreviewableThemeStudio } from "../theme/studio"
4
4
  import { useLabels } from "../composables/useLpConfig"
5
5
  import { suppressPaletteDiscovery } from "../composables/usePaletteCommands"
6
- import LpThemeStudio from "./LpThemeStudio.vue"
6
+ import LpThemeStudio, { type ThemeStudioPart } from "./LpThemeStudio.vue"
7
7
  import LpThemeProvider from "./LpThemeProvider.vue"
8
8
  import LpButton from "./LpButton.vue"
9
9
  import LpInput from "./LpInput.vue"
10
10
 
11
- const props = defineProps<{ studio: PreviewableThemeStudio }>()
11
+ const props = defineProps<{ studio: PreviewableThemeStudio; hide?: ThemeStudioPart[] }>()
12
12
  suppressPaletteDiscovery()
13
13
  const text = useLabels("paletteTheme", { title: "Theme", apply: "Apply theme", revert: "Revert draft", sample: "Preview", action: "Continue", placeholder: "A field in this theme" })
14
14
  const snapshot = ref({ ...props.studio.state.value })
@@ -43,6 +43,6 @@ function apply() {
43
43
  <LpInput :placeholder="text.placeholder" readonly :tabindex="-1" />
44
44
  <LpButton :tabindex="-1">{{ text.action }}</LpButton>
45
45
  </LpThemeProvider>
46
- <LpThemeStudio :studio="draft" :labels="{ reset: text.revert }" />
46
+ <LpThemeStudio :studio="draft" :hide="hide" :labels="{ reset: text.revert }" />
47
47
  </div>
48
48
  </template>
@@ -20,7 +20,8 @@
20
20
  * <LpRollingText :value="label" />
21
21
  * <LpRollingText :value="status" mode="flip" />
22
22
  */
23
- import { computed } from "vue"
23
+ import { computed, ref, useSlots, watch } from "vue"
24
+ import { splitChange } from "./rolling"
24
25
 
25
26
  const props = withDefaults(
26
27
  defineProps<{
@@ -45,6 +46,21 @@ const props = withDefaults(
45
46
  text patch — without it there is nothing to transition between. */
46
47
  const key = computed(() => String(props.value))
47
48
 
49
+ /*
50
+ * Only what changed rolls. "Valid for 2:54" turning into "2:53" rolled every
51
+ * word of the line once a second (a countdown badge), so the whole badge
52
+ * flickered; now "Valid for 2:5" stays and the "4" rolls to "3". With a slot
53
+ * of its own, or wrapping text, the whole value rolls as before.
54
+ */
55
+ const slots = useSlots()
56
+ const partial = computed(() => !slots.default && !props.wrap)
57
+ const parts = ref({ head: "", mid: String(props.value), tail: "" })
58
+ watch(
59
+ () => String(props.value),
60
+ (next, was) => (parts.value = splitChange(was, next)),
61
+ )
62
+ const midKey = computed(() => `${parts.value.head.length}:${parts.value.mid}`)
63
+
48
64
  const name = computed(() =>
49
65
  props.mode === "flip" ? "lp-roll-flip" : `lp-roll-${props.direction}`,
50
66
  )
@@ -68,7 +84,16 @@ const style = computed(() => ({
68
84
  :class="[justify, wrap && 'lp-roll--wrap']"
69
85
  :style="style"
70
86
  >
71
- <Transition :name="name">
87
+ <template v-if="partial">
88
+ <span v-if="parts.head" class="whitespace-pre">{{ parts.head }}</span>
89
+ <span class="relative inline-flex overflow-hidden">
90
+ <Transition :name="name">
91
+ <span :key="midKey" class="lp-roll__item whitespace-pre">{{ parts.mid }}</span>
92
+ </Transition>
93
+ </span>
94
+ <span v-if="parts.tail" class="whitespace-pre">{{ parts.tail }}</span>
95
+ </template>
96
+ <Transition v-else :name="name">
72
97
  <span :key="key" class="lp-roll__item">
73
98
  <slot :value="value">{{ value }}</slot>
74
99
  </span>
@@ -79,7 +79,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
79
79
  [
80
80
  "inline-flex items-center gap-1 rounded-control border border-line bg-surface-soft p-1",
81
81
  sizeCls[props.size],
82
- props.block ? "flex w-full" : "",
82
+ // `w-fit`, not left to the parent: in a column a flex item stretches, and
83
+ // the options sat on the left of an empty bar (the theme studio met it).
84
+ props.block ? "flex w-full" : "w-fit",
83
85
  ].join(" "),
84
86
  )
85
87
 
@@ -104,10 +104,11 @@ const props = withDefaults(
104
104
  },
105
105
  )
106
106
 
107
+ /* The text steps with the height, as in every other kit field: a small select beside a small input read a size larger. */
107
108
  const SIZE_HEIGHT = {
108
- sm: "h-(--size-control-sm)",
109
- md: "h-(--size-control-md)",
110
- lg: "h-(--size-control-lg)",
109
+ sm: "h-(--size-control-sm) text-xs",
110
+ md: "h-(--size-control-md) text-sm",
111
+ lg: "h-(--size-control-lg) text-sm",
111
112
  } as const
112
113
  const anchorHeight = computed(() => SIZE_HEIGHT[props.size])
113
114
 
@@ -431,7 +432,7 @@ usePaletteControl({ disabled: () => field.value.disabled, invalid: () => field.v
431
432
  @focusout="onFocusOut"
432
433
  :class="[
433
434
  anchorHeight,
434
- 'group flex w-full items-center gap-2 rounded-control border bg-surface-soft px-3 text-sm text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2',
435
+ 'group flex w-full items-center gap-2 rounded-control border bg-surface-soft px-3 text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2',
435
436
  field.disabled && 'cursor-not-allowed opacity-55',
436
437
  field.invalid
437
438
  ? 'border-danger focus-within:border-danger focus-within:ring-danger-soft'