@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
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * What a tab shows: its icon with a dot or a count on the corner, its label,
4
+ * and a trailing dot or count where there is no icon to carry them. Shared by
5
+ * LpTabs and LpBottomNav, so a section tab and a page in the bottom bar read
6
+ * the same. Not exported on its own.
7
+ */
8
+ import AttentionDot from "./AttentionDot.vue"
9
+ import LpBadge from "./LpBadge.vue"
10
+ import LpIcon from "./LpIcon.vue"
11
+ import LpRollingText from "./LpRollingText.vue"
12
+
13
+ defineProps<{
14
+ item: { label: string; icon?: string; dot?: boolean; badge?: number | string }
15
+ bottom?: boolean
16
+ compact?: boolean
17
+ active?: boolean
18
+ }>()
19
+ </script>
20
+
21
+ <template>
22
+ <span v-if="item.icon" class="relative z-10 inline-flex">
23
+ <LpIcon :name="item.icon" :size="bottom ? 22 : 14" />
24
+ <AttentionDot v-if="item.dot" corner />
25
+ <!-- On a bottom bar the count rides on the icon's corner, as phone
26
+ navs draw it: the label under the icon has no room beside it. -->
27
+ <Transition name="lp-tab-badge">
28
+ <LpBadge
29
+ v-if="bottom && item.badge != null"
30
+ size="sm"
31
+ :tone="active ? 'brand' : 'neutral'"
32
+ class="absolute -right-3 -top-1.5 min-w-[18px] justify-center"
33
+ >
34
+ <LpRollingText :value="item.badge" />
35
+ </LpBadge>
36
+ </Transition>
37
+ </span>
38
+ <span class="relative z-10" :class="bottom ? 'max-w-full truncate' : compact && item.icon ? 'sr-only' : ''">{{ item.label }}</span>
39
+ <AttentionDot v-if="item.dot && !item.icon" />
40
+ <!-- The count rolls when it changes, and the chip scales in and out
41
+ rather than popping. -->
42
+ <Transition name="lp-tab-badge">
43
+ <LpBadge
44
+ v-if="!(bottom && item.icon) && item.badge != null"
45
+ size="sm"
46
+ :tone="active ? 'brand' : 'neutral'"
47
+ class="relative z-10 min-w-[18px] justify-center"
48
+ >
49
+ <LpRollingText :value="item.badge" />
50
+ </LpBadge>
51
+ </Transition>
52
+ </template>
@@ -5,13 +5,10 @@ import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "reka-ui"
5
5
  import { computed, nextTick, ref, useId, watch } from "vue"
6
6
  import { usePillTransition } from "../composables/usePillTransition"
7
7
  import { prefersReducedMotion } from "../composables/easing"
8
- import AttentionDot from "./AttentionDot.vue"
9
- import LpBadge from "./LpBadge.vue"
10
8
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
11
- import LpIcon from "./LpIcon.vue"
12
- import LpRollingText from "./LpRollingText.vue"
13
9
  import LpSkeleton from "./LpSkeleton.vue"
14
- import { pickTab } from "./tabs"
10
+ import LpTabFace from "./LpTabFace.vue"
11
+ import { BOTTOM_BAR, BOTTOM_ITEM, BOTTOM_MARKER, pickTab } from "./tabs"
15
12
 
16
13
  export interface TabItem {
17
14
  value: string
@@ -61,8 +58,15 @@ const props = withDefaults(
61
58
  * where the pointer crosses the bar on its way elsewhere.
62
59
  */
63
60
  hoverFollow?: boolean
61
+ /**
62
+ * Icons only, the label kept for screen readers and shown on hover. For a
63
+ * header that cannot fit every label: a tab cut off at the edge, or hidden
64
+ * under the actions beside it, cannot be reached at all. Tabs without an
65
+ * icon keep their label.
66
+ */
67
+ compact?: boolean
64
68
  }>(),
65
- { variant: "contained", accent: false, block: false, hoverFollow: true },
69
+ { variant: "contained", accent: false, block: false, hoverFollow: true, compact: false },
66
70
  )
67
71
 
68
72
  const bottom = computed(() => props.variant === "bottom")
@@ -170,7 +174,7 @@ usePaletteCommands(() => props.items.map(item => ({
170
174
  ref="list"
171
175
  :aria-label="label"
172
176
  :class="bottom
173
- ? 'grid w-full auto-cols-fr grid-flow-col border-t border-line bg-surface pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]'
177
+ ? BOTTOM_BAR
174
178
  : [
175
179
  'max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
176
180
  block ? 'flex w-full' : 'inline-flex',
@@ -190,10 +194,11 @@ usePaletteCommands(() => props.items.map(item => ({
190
194
  <TabsTrigger
191
195
  :value="item.value"
192
196
  :disabled="item.disabled"
197
+ :title="compact && item.icon && !bottom ? item.label : undefined"
193
198
  class="relative inline-flex items-center justify-center outline-none transition-[color,opacity] duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45"
194
199
  :class="[
195
200
  bottom
196
- ? 'min-h-14 min-w-0 flex-col gap-0.5 px-1 py-1.5 text-[11px] font-medium focus-visible:ring-inset'
201
+ ? BOTTOM_ITEM
197
202
  : 'shrink-0 gap-1.5 whitespace-nowrap text-sm',
198
203
  block && !bottom ? 'flex-1' : '',
199
204
  variant === 'underline' ? 'rounded-t-inner px-3 pt-1.5 pb-2' : '',
@@ -217,10 +222,7 @@ usePaletteCommands(() => props.items.map(item => ({
217
222
  accent ? 'bg-brand' : 'bg-ink',
218
223
  ]
219
224
  : bottom
220
- ? [
221
- 'absolute inset-x-[22%] top-0 z-0 h-0.5 rounded-b-full',
222
- accent ? 'bg-brand' : 'bg-ink',
223
- ]
225
+ ? [BOTTOM_MARKER, accent ? 'bg-brand' : 'bg-ink']
224
226
  : [
225
227
  'absolute inset-0 z-0 rounded-inner shadow-sm',
226
228
  accent
@@ -228,36 +230,7 @@ usePaletteCommands(() => props.items.map(item => ({
228
230
  : 'border border-line bg-surface-raised',
229
231
  ]"
230
232
  />
231
- <span v-if="item.icon" class="relative z-10 inline-flex">
232
- <LpIcon :name="item.icon" :size="bottom ? 22 : 14" />
233
- <AttentionDot v-if="item.dot" corner />
234
- <!-- On a bottom bar the count rides on the icon's corner, as phone
235
- navs draw it: the label under the icon has no room beside it. -->
236
- <Transition name="lp-tab-badge">
237
- <LpBadge
238
- v-if="bottom && item.badge != null"
239
- size="sm"
240
- :tone="current === item.value ? 'brand' : 'neutral'"
241
- class="absolute -right-3 -top-1.5 min-w-[18px] justify-center"
242
- >
243
- <LpRollingText :value="item.badge" />
244
- </LpBadge>
245
- </Transition>
246
- </span>
247
- <span class="relative z-10" :class="bottom ? 'max-w-full truncate' : ''">{{ item.label }}</span>
248
- <AttentionDot v-if="item.dot && !item.icon" />
249
- <!-- The count rolls when it changes, and the chip scales in and out
250
- rather than popping. -->
251
- <Transition name="lp-tab-badge">
252
- <LpBadge
253
- v-if="!(bottom && item.icon) && item.badge != null"
254
- size="sm"
255
- :tone="current === item.value ? 'brand' : 'neutral'"
256
- class="relative z-10 min-w-[18px] justify-center"
257
- >
258
- <LpRollingText :value="item.badge" />
259
- </LpBadge>
260
- </Transition>
233
+ <LpTabFace :item="item" :bottom="bottom" :compact="compact" :active="current === item.value" />
261
234
  </TabsTrigger>
262
235
  </LpContextMenu>
263
236
  <!-- Items that arrive later (counts from a server, tabs from a route)
@@ -4,7 +4,7 @@ export default { inheritAttrs: false }
4
4
 
5
5
  <script setup lang="ts">
6
6
  import { usePaletteControl, type PaletteControlOptions } from "../composables/usePaletteCommands"
7
- import { computed, useAttrs } from "vue"
7
+ import { computed, nextTick, onMounted, ref, useAttrs, watch } from "vue"
8
8
  import { useFieldControl } from "../composables/useField"
9
9
  import { mergeClass } from "../composables/useMergedClass"
10
10
  import FieldWrapper from "./FieldWrapper.vue"
@@ -25,7 +25,14 @@ const props = withDefaults(defineProps<{
25
25
  * the <textarea> for native validation.
26
26
  */
27
27
  required?: boolean
28
- }>(), { palette: undefined })
28
+ /**
29
+ * Grows with what is typed, wrapped lines included, from `rows` up to
30
+ * `maxRows`, then scrolls; the drag handle goes, since the height is the
31
+ * text's. For a composer, where a fixed box is either cramped or empty.
32
+ */
33
+ autosize?: boolean
34
+ maxRows?: number
35
+ }>(), { palette: undefined, maxRows: 8 })
29
36
 
30
37
  defineEmits<{ (e: "update:modelValue", value: string): void }>()
31
38
 
@@ -49,9 +56,31 @@ const field = useFieldControl(() => ({
49
56
  disabled: props.disabled,
50
57
  }))
51
58
 
59
+ const box = ref<HTMLTextAreaElement | null>(null)
60
+
61
+ // Measured rather than styled with `field-sizing: content`, which WebKit does
62
+ // not have: an app in a WebKitGTK window would keep a fixed box.
63
+ function fit() {
64
+ const el = box.value
65
+ if (!props.autosize || !el) return
66
+ const style = getComputedStyle(el)
67
+ const line = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.4
68
+ const borders = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth)
69
+ const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
70
+ el.style.height = "auto"
71
+ const max = line * props.maxRows + padding + borders
72
+ el.style.height = `${Math.min(el.scrollHeight + borders, max)}px`
73
+ el.style.overflowY = el.scrollHeight + borders > max + 1 ? "auto" : "hidden"
74
+ }
75
+ watch(() => props.modelValue, () => nextTick(fit))
76
+ onMounted(fit)
77
+
78
+ defineExpose({ el: box, fit })
79
+
52
80
  const textareaClass = computed(() =>
53
81
  mergeClass(
54
- "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",
82
+ props.autosize ? "w-full resize-none" : "w-full resize-y",
83
+ "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",
55
84
  field.value.invalid ? "border-danger focus:border-danger focus:ring-danger-soft" : "border-line",
56
85
  hasField.value ? "" : (attrs.class as string),
57
86
  ),
@@ -72,6 +101,7 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
72
101
  :style="attrs.style"
73
102
  >
74
103
  <textarea
104
+ ref="box"
75
105
  v-bind="textareaAttrs"
76
106
  :id="field.id"
77
107
  :value="modelValue"
@@ -83,7 +113,7 @@ usePaletteControl({ label: () => props.label, disabled: () => field.value.disabl
83
113
  :aria-describedby="field.describedBy"
84
114
  :class="textareaClass"
85
115
  :style="hasField ? undefined : (attrs.style as never)"
86
- @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
116
+ @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value); fit()"
87
117
  />
88
118
  </FieldWrapper>
89
119
  </template>
@@ -10,7 +10,7 @@ import { usePaletteTheme } from "../theme/palette"
10
10
  * An app adds its own rows through `#start` and `#end` (LpThemeStudioSection
11
11
  * gives them the panel's look) and drops built-in ones with `hide`.
12
12
  */
13
- import { computed, ref } from "vue"
13
+ import { computed, h, ref, type FunctionalComponent } from "vue"
14
14
  import { useLabels } from "../composables/useLpConfig"
15
15
  import { STUDIO_BRANDS, STUDIO_SYSTEM, useThemeStudio, type StudioState, type ThemeStudio } from "../theme/studio"
16
16
  import type { TokenSet } from "../theme/tokens"
@@ -20,7 +20,10 @@ import LpCodeBlock from "./LpCodeBlock.vue"
20
20
  import LpColorPicker from "./LpColorPicker.vue"
21
21
  import LpIcon from "./LpIcon.vue"
22
22
  import LpSegmented from "./LpSegmented.vue"
23
+ import LpDisclosure from "./LpDisclosure.vue"
24
+ import LpThemeProvider from "./LpThemeProvider.vue"
23
25
  import LpThemeStudioSection from "./LpThemeStudioSection.vue"
26
+ import { withBrand } from "../theme/define"
24
27
 
25
28
  export type ThemeStudioPart = "themes" | "brand" | "corners" | "size" | "motion" | "export"
26
29
 
@@ -100,17 +103,37 @@ function origin(event: Event): RevealOrigin | undefined {
100
103
  }
101
104
 
102
105
  const TILE =
103
- "flex min-w-0 flex-col items-center gap-1 rounded-control p-1.5 text-[11px] text-muted transition-colors duration-(--duration-fast) hover:bg-surface-soft hover:text-ink"
104
- const SWATCH = "relative size-8 overflow-hidden rounded-full ring-2 transition-[box-shadow] duration-(--duration-fast)"
106
+ "group flex min-w-0 flex-col gap-1.5 text-left text-xs text-muted transition-colors duration-(--duration-fast) hover:text-ink aria-pressed:text-ink"
107
+ const FRAME = "relative block aspect-[8/5] w-full overflow-hidden rounded-control transition-[box-shadow] duration-(--duration-fast)"
108
+ const ring = (name: string) =>
109
+ state.value.theme === name ? "ring-2 ring-brand" : "ring-1 ring-line group-hover:ring-line-strong"
110
+
111
+ /** A theme as it would look with the tuning picked here: the brand rides on whichever theme is chosen. */
112
+ const tuned = (theme: TokenSet): TokenSet => (state.value.brand ? withBrand(theme, state.value.brand) : theme)
113
+
114
+ // A window of the theme in miniature: its surfaces, text and a button in its
115
+ // brand. Painted through LpThemeProvider, so it shows the tokens as the app
116
+ // would, rather than a guess made from a few of them.
117
+ const Miniature: FunctionalComponent<{ theme: TokenSet }> = (p) =>
118
+ h(LpThemeProvider, { theme: p.theme, class: "absolute inset-0 flex bg-surface" }, () => [
119
+ h("span", { class: "h-full w-1/4 border-r border-line bg-surface-raised" }),
120
+ h("span", { class: "flex min-w-0 flex-1 flex-col gap-[10%] p-[10%]" }, [
121
+ h("span", { class: "h-[12%] w-3/4 rounded-full bg-ink/80" }),
122
+ h("span", { class: "h-[10%] w-1/2 rounded-full bg-muted/60" }),
123
+ h("span", { class: "mt-auto h-[24%] w-1/2 rounded-control bg-brand" }),
124
+ ]),
125
+ ])
105
126
 
106
127
  const tuneTo = <K extends keyof Omit<StudioState, "theme">>(key: K, value: string) => studio.tune({ [key]: value } as Partial<StudioState>)
107
128
 
108
- usePaletteTheme(() => studio ?? undefined)
129
+ usePaletteTheme(() => studio ?? undefined, { hide: props.hide })
109
130
  </script>
110
131
 
111
132
  <template>
112
- <div class="flex flex-col gap-4">
113
- <div class="flex items-center justify-between">
133
+ <div class="@container flex flex-col gap-4">
134
+ <!-- An app that titles the studio itself (a settings section) passes an
135
+ empty title; the row then shows only while Reset has something to undo. -->
136
+ <div v-if="text.title || changed" class="flex items-center justify-between">
114
137
  <p class="text-sm font-semibold text-ink">{{ text.title }}</p>
115
138
  <LpButton v-if="changed" variant="ghost" size="xs" @click="studio.reset(origin($event))">
116
139
  <LpIcon name="lucide:rotate-ccw" :size="12" />{{ text.reset }}
@@ -120,39 +143,39 @@ usePaletteTheme(() => studio ?? undefined)
120
143
  <slot name="start" :studio="studio" />
121
144
 
122
145
  <LpThemeStudioSection v-if="shows('themes')" :label="text.themes">
123
- <div class="grid grid-cols-4 gap-2">
124
- <!-- Follows the OS between its pair: drawn as both halves. -->
146
+ <div class="grid grid-cols-3 gap-2 @md:grid-cols-4 @2xl:grid-cols-8">
147
+ <!-- Follows the OS between its pair: both halves, split corner to corner. -->
125
148
  <button
126
149
  v-if="studio.system.value"
127
150
  type="button"
128
- :class="[TILE, state.theme === STUDIO_SYSTEM && 'bg-surface-soft text-ink']"
151
+ :class="TILE"
129
152
  :aria-pressed="state.theme === STUDIO_SYSTEM"
130
153
  @click="studio.pickTheme(STUDIO_SYSTEM, origin($event))"
131
154
  >
132
- <span
133
- :class="[SWATCH, state.theme === STUDIO_SYSTEM ? 'ring-brand' : 'ring-line']"
134
- :style="{ background: `linear-gradient(135deg, ${studio.system.value.light.colors.surface} 50%, ${studio.system.value.dark.colors.surface} 50%)` }"
135
- />
155
+ <span :class="[FRAME, ring(STUDIO_SYSTEM)]">
156
+ <Miniature :theme="tuned(studio.system.value.light)" />
157
+ <span class="absolute inset-0 [clip-path:polygon(100%_0,100%_100%,0_100%)]">
158
+ <Miniature :theme="tuned(studio.system.value.dark)" />
159
+ </span>
160
+ </span>
136
161
  <span class="max-w-full truncate">{{ text.system }}</span>
137
162
  </button>
138
163
  <button
139
164
  v-for="t in studio.themes.value"
140
165
  :key="t.name"
141
166
  type="button"
142
- :class="[TILE, state.theme === t.name && 'bg-surface-soft text-ink']"
167
+ :class="TILE"
143
168
  :aria-pressed="state.theme === t.name"
144
169
  @click="studio.pickTheme(t.name, origin($event))"
145
170
  >
146
- <span :class="[SWATCH, state.theme === t.name ? 'ring-brand' : 'ring-line']" :style="{ background: t.colors.surface }">
147
- <span class="absolute inset-x-0 bottom-0 h-1/2" :style="{ background: t.colors.brand }" />
148
- </span>
171
+ <span :class="[FRAME, ring(t.name)]"><Miniature :theme="tuned(t)" /></span>
149
172
  <span class="max-w-full truncate">{{ t.name }}</span>
150
173
  </button>
151
174
  </div>
152
175
  </LpThemeStudioSection>
153
176
 
154
177
  <LpThemeStudioSection v-if="shows('brand')" :label="text.brand">
155
- <div class="flex flex-wrap items-center gap-1.5">
178
+ <div class="flex flex-wrap items-center gap-2">
156
179
  <button
157
180
  v-for="color in swatches"
158
181
  :key="color"
@@ -164,55 +187,68 @@ usePaletteTheme(() => studio ?? undefined)
164
187
  :aria-pressed="brand === color"
165
188
  @click="setBrand(color)"
166
189
  />
167
- <LpColorPicker :model-value="brand" size="sm" :aria-label="text.anyBrand" @update:model-value="setBrand" />
190
+ <LpColorPicker
191
+ variant="swatch"
192
+ size="sm"
193
+ :selected="!swatches.includes(brand)"
194
+ :swatches="[]"
195
+ :model-value="brand"
196
+ :aria-label="text.anyBrand"
197
+ @update:model-value="setBrand"
198
+ />
168
199
  </div>
169
200
  </LpThemeStudioSection>
170
201
 
171
- <LpThemeStudioSection v-if="shows('corners')" :label="text.corners">
172
- <LpSegmented
173
- :model-value="state.corners"
174
- size="sm"
175
- :options="[{ value: 'sharp', label: text.sharp }, { value: 'default', label: text.default }, { value: 'round', label: text.round }]"
176
- @update:model-value="tuneTo('corners', $event)"
177
- />
178
- </LpThemeStudioSection>
179
-
180
- <LpThemeStudioSection v-if="shows('size')" :label="text.size">
181
- <LpSegmented
182
- :model-value="state.size"
183
- size="sm"
184
- :options="[{ value: 'compact', label: text.compact }, { value: 'default', label: text.default }, { value: 'comfortable', label: text.comfortable }]"
185
- @update:model-value="tuneTo('size', $event)"
186
- />
187
- </LpThemeStudioSection>
188
-
189
- <LpThemeStudioSection v-if="shows('motion')" :label="text.motion">
190
- <LpSegmented
191
- :model-value="state.motion"
192
- size="sm"
193
- :options="[{ value: 'fast', label: text.fast }, { value: 'normal', label: text.normal }, { value: 'slow', label: text.slow }]"
194
- @update:model-value="tuneTo('motion', $event)"
195
- />
196
- </LpThemeStudioSection>
202
+ <div v-if="shows('corners') || shows('size') || shows('motion')" class="flex flex-col gap-1.5">
203
+ <LpThemeStudioSection v-if="shows('corners')" inline :label="text.corners">
204
+ <LpSegmented
205
+ :model-value="state.corners"
206
+ size="sm"
207
+ :options="[{ value: 'sharp', label: text.sharp }, { value: 'default', label: text.default }, { value: 'round', label: text.round }]"
208
+ @update:model-value="tuneTo('corners', $event)"
209
+ />
210
+ </LpThemeStudioSection>
211
+ <LpThemeStudioSection v-if="shows('size')" inline :label="text.size">
212
+ <LpSegmented
213
+ :model-value="state.size"
214
+ size="sm"
215
+ :options="[{ value: 'compact', label: text.compact }, { value: 'default', label: text.default }, { value: 'comfortable', label: text.comfortable }]"
216
+ @update:model-value="tuneTo('size', $event)"
217
+ />
218
+ </LpThemeStudioSection>
219
+ <LpThemeStudioSection v-if="shows('motion')" inline :label="text.motion">
220
+ <LpSegmented
221
+ :model-value="state.motion"
222
+ size="sm"
223
+ :options="[{ value: 'fast', label: text.fast }, { value: 'normal', label: text.normal }, { value: 'slow', label: text.slow }]"
224
+ @update:model-value="tuneTo('motion', $event)"
225
+ />
226
+ </LpThemeStudioSection>
227
+ </div>
197
228
 
198
229
  <slot name="end" :studio="studio" />
199
230
 
200
- <div v-if="shows('export')" class="flex flex-col gap-2 border-t border-line pt-3">
201
- <div class="flex items-center justify-between gap-2">
202
- <span class="text-[11px] font-medium tracking-wide text-muted uppercase">{{ text.export }}</span>
231
+ <!-- Taking the theme into code is the developer's errand, not the person
232
+ picking colours: folded, it no longer opened every studio with an
233
+ empty block of code. -->
234
+ <LpDisclosure v-if="shows('export')">
235
+ <template #trigger>
236
+ <span class="text-xs font-medium text-muted-strong">{{ text.export }}</span>
237
+ </template>
238
+ <div class="flex flex-col gap-2 pt-1">
203
239
  <LpSegmented
204
240
  v-model="format"
205
241
  size="sm"
206
242
  :options="[{ value: 'code', label: 'Code' }, { value: 'json', label: 'JSON' }, { value: 'css', label: 'CSS' }]"
207
243
  />
244
+ <LpCodeBlock
245
+ :model-value="exported[format]"
246
+ :lang="format === 'json' ? 'json' : format === 'css' ? 'plain' : 'ts'"
247
+ copyable
248
+ height="140px"
249
+ class="text-[11px]"
250
+ />
208
251
  </div>
209
- <LpCodeBlock
210
- :model-value="exported[format]"
211
- :lang="format === 'json' ? 'json' : format === 'css' ? 'plain' : 'ts'"
212
- copyable
213
- height="140px"
214
- class="text-[11px]"
215
- />
216
- </div>
252
+ </LpDisclosure>
217
253
  </div>
218
254
  </template>
@@ -1,16 +1,24 @@
1
1
  <script setup lang="ts">
2
2
  /*
3
- * One row of LpThemeStudio: a small caps label over its controls. The studio
4
- * builds its own rows with it, and an app adding its own (a font, a density
5
- * of its own, a layout switch) through the studio's slots uses it too, so the
6
- * added row reads as part of the panel.
3
+ * One row of LpThemeStudio: a label and its controls. The studio builds its
4
+ * own rows with it, and an app adding its own (a font, a density of its own,
5
+ * a layout switch) through the studio's slots uses it too, so the added row
6
+ * reads as part of the panel.
7
+ *
8
+ * `inline` puts a single control beside its label, as a settings row does: a
9
+ * three-way switch under a label of its own read as three loose rows of
10
+ * buttons, and on a wide page it sat in the corner of an empty stripe.
7
11
  */
8
- defineProps<{ label: string }>()
12
+ defineProps<{ label: string; inline?: boolean }>()
9
13
  </script>
10
14
 
11
15
  <template>
12
- <div class="flex flex-col gap-2">
13
- <span class="text-[11px] font-medium tracking-wide text-muted uppercase">{{ label }}</span>
16
+ <div v-if="inline" class="flex min-h-8 flex-wrap items-center justify-between gap-x-4 gap-y-1.5">
17
+ <span class="text-xs font-medium text-muted-strong">{{ label }}</span>
18
+ <slot />
19
+ </div>
20
+ <div v-else class="flex flex-col gap-2">
21
+ <span class="text-xs font-medium text-muted-strong">{{ label }}</span>
14
22
  <slot />
15
23
  </div>
16
24
  </template>
@@ -228,6 +228,9 @@ function followSystem() {
228
228
  // With the studio, "System" is its base theme, so the tuning stays on top.
229
229
  if (tuner) {
230
230
  tuner.pickTheme(STUDIO_SYSTEM)
231
+ // A v-model bound to the switcher learns "system" too, or it would keep
232
+ // the previous name and win over the studio: the check stayed on it.
233
+ emit("update:modelValue", STUDIO_SYSTEM)
231
234
  emit("change", tuner.theme.value)
232
235
  return
233
236
  }
@@ -300,7 +303,7 @@ const triggerLabel = computed(
300
303
  () => props["aria-label"] ?? text.value.trigger.replace("{name}", labelName.value),
301
304
  )
302
305
 
303
- usePaletteTheme(() => tuner ?? undefined)
306
+ usePaletteTheme(() => tuner ?? undefined, { hide: props.studioHide })
304
307
  </script>
305
308
 
306
309
  <template>
@@ -20,9 +20,10 @@ import {
20
20
  type Connection,
21
21
  type Node,
22
22
  type NodeTypesObject,
23
+ type Padding,
23
24
  } from "@vue-flow/core"
24
25
  import { MiniMap } from "@vue-flow/minimap"
25
- import { computed, defineComponent, h, markRaw, watch, type Component } from "vue"
26
+ import { computed, defineComponent, h, markRaw, ref, watch, type Component } from "vue"
26
27
  import LpInfraNode, { type InfraNodeData } from "./LpInfraNode.vue"
27
28
  import LpLaneNode from "./LpLaneNode.vue"
28
29
  import LpServiceNode, { type ServiceNodeData } from "./LpServiceNode.vue"
@@ -325,6 +326,7 @@ const {
325
326
  getSelectedNodes,
326
327
  fitView,
327
328
  setViewport,
329
+ onNodesInitialized,
328
330
  } = useVueFlow()
329
331
  onNodeClick(({ node }) => emit("node-select", node.id))
330
332
  // A node usually has two levels of interest — pick it out, or open it — and a
@@ -389,13 +391,42 @@ onEdgeContextMenu(({ edge, event }) => {
389
391
  onPaneClick(() => emit("pane-click"))
390
392
  onConnect((conn) => emit("connect", conn))
391
393
 
394
+ // The legend and the minimap float over the canvas, so a plain fit puts the
395
+ // outermost nodes under them. Fitting reserves their corner instead: the side
396
+ // they sit on gets their height on top of the usual margin. On a phone both
397
+ // are hidden and only the margin is left.
398
+ const legendBox = ref<HTMLElement | null>(null)
399
+ const MINIMAP_HEIGHT = 165
400
+ const FRAME = 24
401
+ function framePadding(): Padding {
402
+ const wide = typeof window === "undefined" || window.matchMedia("(min-width: 640px)").matches
403
+ const legendHeight = wide && props.legend ? (legendBox.value?.offsetHeight ?? 0) + 15 : 0
404
+ const legendTop = props.legendPosition.startsWith("top")
405
+ const bottom = Math.max(legendTop ? 0 : legendHeight, wide && props.minimap ? MINIMAP_HEIGHT : 0)
406
+ return {
407
+ top: `${FRAME + (legendTop ? legendHeight : 0)}px`,
408
+ bottom: `${FRAME + bottom}px`,
409
+ left: `${FRAME}px`,
410
+ right: `${FRAME}px`,
411
+ }
412
+ }
413
+ function fitFramed(params: Parameters<typeof fitView>[0] = {}) {
414
+ return fitView({ padding: framePadding(), ...params })
415
+ }
416
+ let fitted = false
417
+ onNodesInitialized(() => {
418
+ if (fitted) return
419
+ fitted = true
420
+ void fitFramed()
421
+ })
422
+
392
423
  // Restore a saved viewport (pan/zoom). The host calls this after loading the
393
424
  // user's persisted CanvasViewport on mount.
394
425
  async function applyViewport(v: CanvasViewport): Promise<void> {
395
426
  await setViewport({ x: v.x, y: v.y, zoom: v.zoom })
396
427
  }
397
428
 
398
- defineExpose({ fitView, setViewport: applyViewport })
429
+ defineExpose({ fitView: fitFramed, setViewport: applyViewport })
399
430
  </script>
400
431
 
401
432
  <template>
@@ -413,7 +444,6 @@ defineExpose({ fitView, setViewport: applyViewport })
413
444
  :default-viewport="{ zoom: 0.9 }"
414
445
  :min-zoom="0.3"
415
446
  :max-zoom="2"
416
- fit-view-on-init
417
447
  class="lp-topology"
418
448
  >
419
449
  <Background :gap="22" :size="1.4" pattern-color="var(--color-line-strong)" />
@@ -428,13 +458,14 @@ defineExpose({ fitView, setViewport: applyViewport })
428
458
  node-color="var(--color-surface-soft)"
429
459
  mask-color="rgba(0,0,0,0.55)"
430
460
  />
431
- <Controls v-if="controls" />
461
+ <Controls v-if="controls" :fit-view-params="{ padding: framePadding() }" />
432
462
 
433
463
  <!-- Edge-type legend: makes the colour/dash encoding self-explanatory. -->
434
464
  <!-- On a phone the legend would sit on the graph itself; the edge styles
435
465
  stay readable without it there. -->
436
466
  <Panel v-if="legend" :position="legendPosition" :class="legendOffset" class="max-sm:hidden">
437
467
  <div
468
+ ref="legendBox"
438
469
  class="flex flex-col gap-1.5 rounded-card border border-line bg-surface-overlay/90 px-3 py-2 text-[11px] backdrop-blur"
439
470
  >
440
471
  <div
@@ -28,6 +28,13 @@ export interface ChartSeries {
28
28
  * own scale and its own ticks.
29
29
  */
30
30
  axis?: "left" | "right"
31
+ /**
32
+ * Start with this series switched off, the legend turning it back on. For a
33
+ * series worth having but loud next to the rest: messages in thousands beside
34
+ * people in tens flatten the people line on a shared axis until they are
35
+ * hidden, so the chart opens on the readable view.
36
+ */
37
+ hidden?: boolean
31
38
  }
32
39
 
33
40
  export type ChartAxis = "left" | "right"
@@ -58,6 +65,25 @@ export interface ChartMarker {
58
65
  color?: string
59
66
  }
60
67
 
68
+ /**
69
+ * Which x labels to print with every `stride`-th one shown. The last point is
70
+ * always labelled (it is usually "now"), and when it lands within half a
71
+ * stride to the previous shown label that one gives way: "20 22 23" read as
72
+ * one smudge at the end of an hourly axis.
73
+ */
74
+ export function xLabelIndices(count: number, stride: number): number[] {
75
+ if (count <= 0) return []
76
+ const step = Math.max(1, Math.floor(stride))
77
+ const out: number[] = []
78
+ for (let i = 0; i < count; i += step) out.push(i)
79
+ const last = count - 1
80
+ if (out[out.length - 1] !== last) {
81
+ if (out.length > 1 && last - out[out.length - 1] <= step / 2) out.pop()
82
+ out.push(last)
83
+ }
84
+ return out
85
+ }
86
+
61
87
  /** The x index a marker points at, or -1 when it names nothing on the axis. */
62
88
  export const markerIndex = (at: number | string, labels: string[], count: number): number => {
63
89
  if (typeof at === "number") return Number.isInteger(at) && at >= 0 && at < count ? at : -1