@leavepulse/ui 0.71.2 → 0.72.1

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 (276) hide show
  1. package/dist/assets/{LpKeys-BxRhfQda.css → LpKeys-C5VMcBOW.css} +10 -5
  2. package/dist/assets/LpTabs-CDchzoRL.css +18 -0
  3. package/dist/chunks/{ContextMenuItems-eS7qx9gD.js → ContextMenuItems-BXumicbh.js} +1 -1
  4. package/dist/chunks/{DropzoneLinkField-DkFhdfEJ.js → DropzoneLinkField-DpOB20Lx.js} +2 -2
  5. package/dist/chunks/{FieldWrapper-YzbvvC3M.js → FieldWrapper-DHhRxO5G.js} +1 -1
  6. package/dist/chunks/{LayoutCanvas-Ba4A9QC5.js → LayoutCanvas-CNOAUk7D.js} +1 -1
  7. package/dist/chunks/{LayoutNode-CTTTLMDM.js → LayoutNode-DF5BMN0J.js} +1 -1
  8. package/dist/chunks/{LpActionBar-B7anGlkI.js → LpActionBar-BqicINE_.js} +3 -3
  9. package/dist/chunks/{LpAddressInput-Bu6ws3Fp.js → LpAddressInput-CkbKLufb.js} +7 -7
  10. package/dist/chunks/{LpAlert-BHi7ARqu.js → LpAlert-BigMdAbu.js} +3 -3
  11. package/dist/chunks/{LpAppShell-C5YW5pI7.js → LpAppShell-DIMy-vjP.js} +68 -18
  12. package/dist/chunks/{LpAutocomplete-DDqe2M5g.js → LpAutocomplete-DOu7FN5P.js} +2 -2
  13. package/dist/chunks/{LpAvatar-DqfKCIdr.js → LpAvatar-Cg6Az3wz.js} +1 -1
  14. package/dist/chunks/{LpBadge-C8fKEmTF.js → LpBadge-Huo8EKew.js} +3 -3
  15. package/dist/chunks/LpBottomNav-BZIpahzA.js +89 -0
  16. package/dist/chunks/{LpBreadcrumbs-DhcoStFa.js → LpBreadcrumbs-DGtpBhWt.js} +3 -3
  17. package/dist/chunks/{LpButton-BQDAFXBd.js → LpButton-CW34A1e4.js} +1 -1
  18. package/dist/chunks/{LpCalendar-NmrA47J9.js → LpCalendar-CuoWks-0.js} +1 -1
  19. package/dist/chunks/{LpCard-CqKLnEhl.js → LpCard-B0sFXQzc.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-BoOuLnGB.js → LpCheckbox-iaD63DEF.js} +2 -2
  21. package/dist/chunks/{LpCodeBlock-Czu-fq1M.js → LpCodeBlock-CFFa_pJb.js} +2 -2
  22. package/dist/chunks/{LpCodeDisplay-C9pIMF2R.js → LpCodeDisplay-CFmGMAmP.js} +6 -6
  23. package/dist/chunks/{LpColorPicker-Ee1DZiRd.js → LpColorPicker-1y1QJHZR.js} +56 -28
  24. package/dist/chunks/{LpConfirmDialog-i15pulMT.js → LpConfirmDialog-BDystNtm.js} +3 -3
  25. package/dist/chunks/{LpConfirmHost-D0oGavtd.js → LpConfirmHost-oOsxJHfQ.js} +1 -1
  26. package/dist/chunks/{LpContextMenu-DXpFJjlO.js → LpContextMenu-rx2tCjwk.js} +2 -2
  27. package/dist/chunks/{LpCopyButton-Dmq7DmFw.js → LpCopyButton-BNcGbVXF.js} +2 -2
  28. package/dist/chunks/{LpCountdown-CXrEK0ev.js → LpCountdown-Cak7cq3X.js} +3 -3
  29. package/dist/chunks/{LpDataList-CFPtSsBB.js → LpDataList-B6Meb4M9.js} +3 -3
  30. package/dist/chunks/{LpDatePicker-cC9K-Pk8.js → LpDatePicker-CA0QTfBC.js} +4 -4
  31. package/dist/chunks/{LpDateRangePicker-BVQKPbvo.js → LpDateRangePicker-u3J51xXk.js} +2 -2
  32. package/dist/chunks/{LpDisclosure-hRnX3FWL.js → LpDisclosure-Cpa7Hc5T.js} +1 -1
  33. package/dist/chunks/{LpDrawer-u4rxnuR-.js → LpDrawer-RRamxbLV.js} +1 -1
  34. package/dist/chunks/{LpDropdownMenu-BgZ0Z_Im.js → LpDropdownMenu-Dl7Z4ZhS.js} +2 -2
  35. package/dist/chunks/{LpEditable-CjxuOVh_.js → LpEditable-BNogo5DL.js} +5 -5
  36. package/dist/chunks/{LpEmptyState-CKFTtQgJ.js → LpEmptyState-EQHiRYhG.js} +7 -3
  37. package/dist/chunks/{LpFieldNotice-CPI8EwtU.js → LpFieldNotice-CzQ2_5rQ.js} +1 -1
  38. package/dist/chunks/{LpFileTree-BH0gNlSJ.js → LpFileTree-CHgtVh14.js} +2 -2
  39. package/dist/chunks/{LpFileTreeNode-gPVB1oXM.js → LpFileTreeNode-LqHgHd3B.js} +5 -5
  40. package/dist/chunks/{LpFormField-gRASfyj2.js → LpFormField-CC57DdTi.js} +2 -2
  41. package/dist/chunks/{LpGauge-DFdt33x8.js → LpGauge-BO1PYYqD.js} +2 -2
  42. package/dist/chunks/{LpHintLayer-C2asBIfs.js → LpHintLayer-CvAj4aa6.js} +2 -2
  43. package/dist/chunks/{LpHints-CNZQ76JV.js → LpHints-BtRvHQQ0.js} +3 -3
  44. package/dist/chunks/{LpIcon-DkP-9mzK.js → LpIcon--5WSb3jJ.js} +1 -1
  45. package/dist/chunks/{LpInfraNode-yTh4-5qa.js → LpInfraNode-CxngdQAG.js} +2 -2
  46. package/dist/chunks/{LpInput-2H-5ILOB.js → LpInput-DqmL-438.js} +2 -2
  47. package/dist/chunks/{LpLightbox-lAt5uIfx.js → LpLightbox-7YP6zSDm.js} +3 -3
  48. package/dist/chunks/{LpLoader-DjJEX8hL.js → LpLoader-CUo1MX_0.js} +1 -1
  49. package/dist/chunks/{LpLogViewer-x78YfhJf.js → LpLogViewer-DQOIsnZ1.js} +6 -6
  50. package/dist/chunks/{LpMapPicker-Dp5RSCi9.js → LpMapPicker-Dm3vJHN2.js} +2 -2
  51. package/dist/chunks/{LpMediaGallery-D92oJfY_.js → LpMediaGallery-znEbvIEo.js} +3 -3
  52. package/dist/chunks/{LpModal-CpH0bNo7.js → LpModal-BnjZ2iem.js} +1 -1
  53. package/dist/chunks/{LpModalNav-Cuna8c_3.js → LpModalNav-CH1UE9Ch.js} +4 -4
  54. package/dist/chunks/{LpNotificationBell-BdUQqZhs.js → LpNotificationBell-CrywodQ_.js} +8 -8
  55. package/dist/chunks/{LpNumberField-DZY3Z670.js → LpNumberField-e0IBkWU1.js} +1 -1
  56. package/dist/chunks/{LpNumberFlow-Dwb9MV1A.js → LpNumberFlow-CcxEGUy0.js} +4 -2
  57. package/dist/chunks/{LpPagination-DbeACavM.js → LpPagination-BIIiYHXR.js} +1 -1
  58. package/dist/chunks/{LpPaletteTheme-DkikZ362.js → LpPaletteTheme-BzHBh-mN.js} +4 -4
  59. package/dist/chunks/{LpPasswordInput-BzAZsf5V.js → LpPasswordInput-DVgRgTVx.js} +2 -2
  60. package/dist/chunks/{LpPhoneInput-BMpd-0QR.js → LpPhoneInput-CETEp5W7.js} +2 -2
  61. package/dist/chunks/{LpPie-C1uIUFxu.js → LpPie-BjDbeytx.js} +6 -6
  62. package/dist/chunks/{LpPipeline-zjEM1Lwc.js → LpPipeline-zZUj7I6K.js} +4 -4
  63. package/dist/chunks/{LpProgress-WNaH4emv.js → LpProgress-rOlyafqp.js} +1 -1
  64. package/dist/chunks/{LpQuota-Cz9PvklM.js → LpQuota-B12yijdB.js} +3 -3
  65. package/dist/chunks/{LpRadioGroup-CsXeV9Ct.js → LpRadioGroup-ggaPL92T.js} +1 -1
  66. package/dist/chunks/{LpRangeCalendar-l-0nSFmY.js → LpRangeCalendar-CYYUYqJ7.js} +1 -1
  67. package/dist/chunks/{LpRelativeTime-UM8EkfrN.js → LpRelativeTime-DwgCb5-8.js} +1 -1
  68. package/dist/chunks/{LpRepeater-OvzByE5L.js → LpRepeater-BQB0twJ5.js} +3 -3
  69. package/dist/chunks/LpRollingText-B54Pje7P.js +86 -0
  70. package/dist/chunks/{LpScramble-TDO5Kdou.js → LpScramble-DbQyBjUY.js} +1 -1
  71. package/dist/chunks/{LpSecretField-DwQiPOko.js → LpSecretField-uoD6Fngf.js} +7 -7
  72. package/dist/chunks/{LpSection-DcQsieHG.js → LpSection-DFK_uoJE.js} +1 -1
  73. package/dist/chunks/{LpSegmented-D2VST0o3.js → LpSegmented-CeACsMMA.js} +4 -2
  74. package/dist/chunks/{LpSelect-D3pRJfRY.js → LpSelect-Cxq4yBGd.js} +3 -3
  75. package/dist/chunks/LpShift-C-UGuKMN.js +171 -0
  76. package/dist/chunks/LpSidebar-DXdbvgM6.js +494 -0
  77. package/dist/chunks/LpSidebarNav-CE6CVCb0.js +367 -0
  78. package/dist/chunks/LpSidebarRow-CF9VFZ2w.js +304 -0
  79. package/dist/chunks/{LpStat-Dzn6WFzi.js → LpStat-CjpXrC8R.js} +6 -4
  80. package/dist/chunks/{LpStatus-BQU1TUsv.js → LpStatus-g-8z9b_u.js} +3 -3
  81. package/dist/chunks/{LpStepMark-CXa7DJy_.js → LpStepMark-DU7xalwW.js} +3 -3
  82. package/dist/chunks/{LpStepper-C9RM_yTG.js → LpStepper-CbRXiXNs.js} +1 -1
  83. package/dist/chunks/{LpSwap-DzE6M3Mg.js → LpSwap-Xi4j9PR_.js} +1 -1
  84. package/dist/chunks/{LpSwitch-bGZxUDSx.js → LpSwitch-BWcXEahf.js} +1 -1
  85. package/dist/chunks/LpTabFace-CG0VqD8I.js +77 -0
  86. package/dist/chunks/{LpTableFilter-CTZMqFDU.js → LpTableFilter-CYtK9GsF.js} +3 -3
  87. package/dist/chunks/{LpTagsInput-DrjmrOge.js → LpTagsInput-D1RzCwb5.js} +5 -5
  88. package/dist/chunks/{LpTextarea-BbYqmG1S.js → LpTextarea-MdIzNTjw.js} +32 -7
  89. package/dist/chunks/{useTheme-BOV_-AiY.js → LpThemeProvider-8MG6yqU9.js} +66 -1
  90. package/dist/chunks/{LpThemeStudio-IupHqP0x.js → LpThemeStudio-B0Kk1bX0.js} +138 -104
  91. package/dist/chunks/LpThemeStudioSection-B7bt8Iyg.js +32 -0
  92. package/dist/chunks/{LpThemeSwitcher-DarPHuX8.js → LpThemeSwitcher-CI4I7ygL.js} +6 -6
  93. package/dist/chunks/{LpTimeField-DQ8WvPr2.js → LpTimeField-BawSVsmt.js} +1 -1
  94. package/dist/chunks/{LpToaster-BTTl_wuy.js → LpToaster-C-P1nmMS.js} +2 -2
  95. package/dist/chunks/{LpTopologyCanvas-Cq-MiJwo.js → LpTopologyCanvas-CbEZhsP1.js} +1 -1
  96. package/dist/chunks/{LpTour-BgCfNlaW.js → LpTour-DRyzgpjt.js} +1 -1
  97. package/dist/chunks/{LpTruncate-TcqfkyJM.js → LpTruncate-4WvCWqnW.js} +1 -1
  98. package/dist/chunks/{LpUptimeBar-BbhVTPnL.js → LpUptimeBar-B2LNSNxs.js} +4 -4
  99. package/dist/chunks/{MenuTitle-Dab3EGOp.js → MenuTitle-BmCNNTKH.js} +1 -1
  100. package/dist/chunks/{OptionContent-DPzNsybd.js → OptionContent-22UT8zLn.js} +1 -1
  101. package/dist/chunks/{PickerField-DwLw7j0L.js → PickerField-Bfh8id3d.js} +2 -2
  102. package/dist/chunks/{RevealToggle-BVFM_FAU.js → RevealToggle-jBLJwVwO.js} +2 -2
  103. package/dist/chunks/{TourCard--2IfgnUU.js → TourCard-BOBcGbEo.js} +5 -5
  104. package/dist/chunks/{icons-CLu1iR1j.js → icons-DMITlfw5.js} +1 -1
  105. package/dist/component-names.d.ts +1 -1
  106. package/dist/components/ContextMenuItems.vue.js +1 -1
  107. package/dist/components/DropzoneLinkField.vue.js +1 -1
  108. package/dist/components/FieldWrapper.vue.js +1 -1
  109. package/dist/components/LayoutCanvas.vue.js +1 -1
  110. package/dist/components/LayoutNode.vue.js +1 -1
  111. package/dist/components/LpActionBar.vue.js +1 -1
  112. package/dist/components/LpAddressInput.vue.js +1 -1
  113. package/dist/components/LpAlert.vue.js +1 -1
  114. package/dist/components/LpAppShell.vue.d.ts +67 -2
  115. package/dist/components/LpAppShell.vue.js +1 -1
  116. package/dist/components/LpAutocomplete.vue.js +1 -1
  117. package/dist/components/LpAvatar.vue.js +1 -1
  118. package/dist/components/LpBadge.vue.js +1 -1
  119. package/dist/components/LpBottomNav.vue.d.ts +28 -0
  120. package/dist/components/LpBottomNav.vue.js +4 -0
  121. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  122. package/dist/components/LpButton.vue.js +1 -1
  123. package/dist/components/LpCalendar.vue.js +1 -1
  124. package/dist/components/LpCard.vue.js +1 -1
  125. package/dist/components/LpChart.vue.js +4 -4
  126. package/dist/components/LpCheckbox.vue.js +1 -1
  127. package/dist/components/LpCodeBlock.vue.js +1 -1
  128. package/dist/components/LpCodeDisplay.vue.js +1 -1
  129. package/dist/components/LpColorPicker.vue.d.ts +15 -2
  130. package/dist/components/LpColorPicker.vue.js +1 -1
  131. package/dist/components/LpCommandPalette.vue.js +4 -4
  132. package/dist/components/LpConfirmDialog.vue.js +1 -1
  133. package/dist/components/LpConfirmHost.vue.js +1 -1
  134. package/dist/components/LpContextMenu.vue.js +1 -1
  135. package/dist/components/LpCopyButton.vue.js +1 -1
  136. package/dist/components/LpCountdown.vue.js +1 -1
  137. package/dist/components/LpDataList.vue.js +1 -1
  138. package/dist/components/LpDatePicker.vue.js +1 -1
  139. package/dist/components/LpDateRangePicker.vue.js +1 -1
  140. package/dist/components/LpDisclosure.vue.js +1 -1
  141. package/dist/components/LpDock.vue.js +1 -1
  142. package/dist/components/LpDrawer.vue.js +1 -1
  143. package/dist/components/LpDropdownMenu.vue.js +1 -1
  144. package/dist/components/LpDropzone.vue.js +2 -2
  145. package/dist/components/LpEditable.vue.js +1 -1
  146. package/dist/components/LpEmptyState.vue.d.ts +2 -0
  147. package/dist/components/LpEmptyState.vue.js +1 -1
  148. package/dist/components/LpFieldNotice.vue.js +1 -1
  149. package/dist/components/LpFileTree.vue.js +1 -1
  150. package/dist/components/LpFileTreeNode.vue.js +1 -1
  151. package/dist/components/LpFormField.vue.js +1 -1
  152. package/dist/components/LpGauge.vue.js +1 -1
  153. package/dist/components/LpHintLayer.vue.js +1 -1
  154. package/dist/components/LpHints.vue.js +1 -1
  155. package/dist/components/LpIcon.vue.js +1 -1
  156. package/dist/components/LpImageEditor.vue.js +3 -3
  157. package/dist/components/LpInfraNode.vue.js +1 -1
  158. package/dist/components/LpInput.vue.js +1 -1
  159. package/dist/components/LpKeys.vue.js +13 -10
  160. package/dist/components/LpLightbox.vue.js +1 -1
  161. package/dist/components/LpLoader.vue.js +1 -1
  162. package/dist/components/LpLogViewer.vue.d.ts +1 -1
  163. package/dist/components/LpLogViewer.vue.js +1 -1
  164. package/dist/components/LpMapPicker.vue.js +1 -1
  165. package/dist/components/LpMediaGallery.vue.js +1 -1
  166. package/dist/components/LpModal.vue.js +1 -1
  167. package/dist/components/LpModalNav.vue.js +1 -1
  168. package/dist/components/LpNotificationBell.vue.js +1 -1
  169. package/dist/components/LpNumberField.vue.js +1 -1
  170. package/dist/components/LpNumberFlow.vue.js +1 -1
  171. package/dist/components/LpPagination.vue.js +1 -1
  172. package/dist/components/LpPaletteFacts.vue.js +2 -2
  173. package/dist/components/LpPaletteHeader.vue.js +2 -2
  174. package/dist/components/LpPaletteStats.vue.js +2 -2
  175. package/dist/components/LpPaletteTheme.vue.js +1 -1
  176. package/dist/components/LpPasswordInput.vue.js +1 -1
  177. package/dist/components/LpPhoneInput.vue.js +1 -1
  178. package/dist/components/LpPie.vue.js +1 -1
  179. package/dist/components/LpPipeline.vue.js +1 -1
  180. package/dist/components/LpProgress.vue.js +1 -1
  181. package/dist/components/LpQuota.vue.js +1 -1
  182. package/dist/components/LpRadioGroup.vue.js +1 -1
  183. package/dist/components/LpRangeCalendar.vue.js +1 -1
  184. package/dist/components/LpRelativeTime.vue.js +1 -1
  185. package/dist/components/LpRepeater.vue.js +1 -1
  186. package/dist/components/LpRollingText.vue.d.ts +2 -2
  187. package/dist/components/LpRollingText.vue.js +1 -1
  188. package/dist/components/LpScramble.vue.js +1 -1
  189. package/dist/components/LpSecretField.vue.js +1 -1
  190. package/dist/components/LpSection.vue.js +1 -1
  191. package/dist/components/LpSegmented.vue.js +1 -1
  192. package/dist/components/LpSelect.vue.js +1 -1
  193. package/dist/components/LpShift.vue.d.ts +20 -0
  194. package/dist/components/LpShift.vue.js +1 -1
  195. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  196. package/dist/components/LpSidebar.vue.d.ts +74 -23
  197. package/dist/components/LpSidebar.vue.js +1 -1
  198. package/dist/components/LpSidebarNav.vue.d.ts +31 -3
  199. package/dist/components/LpSidebarNav.vue.js +1 -1
  200. package/dist/components/LpSidebarRow.vue.d.ts +49 -0
  201. package/dist/components/LpSidebarRow.vue.js +4 -0
  202. package/dist/components/LpStat.vue.d.ts +11 -0
  203. package/dist/components/LpStat.vue.js +1 -1
  204. package/dist/components/LpStatus.vue.js +1 -1
  205. package/dist/components/LpStepMark.vue.js +1 -1
  206. package/dist/components/LpStepper.vue.js +1 -1
  207. package/dist/components/LpSwap.vue.js +1 -1
  208. package/dist/components/LpSwitch.vue.js +1 -1
  209. package/dist/components/LpTabFace.vue.d.ts +14 -0
  210. package/dist/components/LpTabFace.vue.js +4 -0
  211. package/dist/components/LpTable.vue.js +11 -11
  212. package/dist/components/LpTableFilter.vue.js +1 -1
  213. package/dist/components/LpTabs.vue.d.ts +4 -4
  214. package/dist/components/LpTabs.vue.js +18 -72
  215. package/dist/components/LpTagsInput.vue.js +1 -1
  216. package/dist/components/LpTextarea.vue.d.ts +19 -1
  217. package/dist/components/LpTextarea.vue.js +1 -1
  218. package/dist/components/LpThemeProvider.vue.js +1 -1
  219. package/dist/components/LpThemeStudio.vue.d.ts +2 -2
  220. package/dist/components/LpThemeStudio.vue.js +1 -1
  221. package/dist/components/LpThemeStudioSection.vue.d.ts +4 -1
  222. package/dist/components/LpThemeStudioSection.vue.js +1 -1
  223. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  224. package/dist/components/LpTimeField.vue.js +1 -1
  225. package/dist/components/LpToaster.vue.js +1 -1
  226. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  227. package/dist/components/LpTour.vue.js +1 -1
  228. package/dist/components/LpTruncate.vue.js +1 -1
  229. package/dist/components/LpUptimeBar.vue.js +1 -1
  230. package/dist/components/MenuTitle.vue.js +1 -1
  231. package/dist/components/OptionContent.vue.js +1 -1
  232. package/dist/components/PickerField.vue.js +1 -1
  233. package/dist/components/RevealToggle.vue.js +1 -1
  234. package/dist/components/TourCard.vue.js +1 -1
  235. package/dist/components/menuWhen.js +1 -1
  236. package/dist/components/rolling.d.ts +5 -0
  237. package/dist/components/rolling.js +23 -0
  238. package/dist/components/sidebar.d.ts +38 -0
  239. package/dist/components/sidebar.js +6 -1
  240. package/dist/components/tabs.d.ts +3 -0
  241. package/dist/components/tabs.js +6 -0
  242. package/dist/icons-all.js +1 -1
  243. package/dist/index.d.ts +1 -0
  244. package/dist/index.js +154 -153
  245. package/package.json +1 -1
  246. package/src/component-names.ts +1 -0
  247. package/src/components/LpAppShell.vue +75 -7
  248. package/src/components/LpBottomNav.vue +84 -0
  249. package/src/components/LpColorPicker.vue +32 -1
  250. package/src/components/LpEmptyState.vue +4 -1
  251. package/src/components/LpKeys.vue +10 -1
  252. package/src/components/LpNumberFlow.vue +4 -2
  253. package/src/components/LpRollingText.vue +27 -2
  254. package/src/components/LpSegmented.vue +3 -1
  255. package/src/components/LpShift.vue +189 -2
  256. package/src/components/LpSidebar.vue +304 -18
  257. package/src/components/LpSidebarNav.vue +211 -78
  258. package/src/components/LpSidebarRow.vue +242 -0
  259. package/src/components/LpStat.vue +7 -1
  260. package/src/components/LpTabFace.vue +52 -0
  261. package/src/components/LpTabs.vue +6 -41
  262. package/src/components/LpTextarea.vue +34 -4
  263. package/src/components/LpThemeStudio.vue +91 -55
  264. package/src/components/LpThemeStudioSection.vue +15 -7
  265. package/src/components/rolling.ts +28 -0
  266. package/src/components/sidebar.ts +41 -0
  267. package/src/components/tabs.ts +9 -0
  268. package/src/index.ts +1 -0
  269. package/src/kit-icons.ts +1 -1
  270. package/dist/assets/LpTabs-DT0XPTc2.css +0 -18
  271. package/dist/chunks/LpRollingText-DiL717LK.js +0 -51
  272. package/dist/chunks/LpShift-DXNsauQZ.js +0 -40
  273. package/dist/chunks/LpSidebar-Bk7b-K4l.js +0 -256
  274. package/dist/chunks/LpSidebarNav-BR2AW3KT.js +0 -216
  275. package/dist/chunks/LpThemeProvider-DcbfOxsU.js +0 -69
  276. 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>
@@ -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"
@@ -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>
@@ -34,7 +34,11 @@ const steps = computed(() => (Array.isArray(props.combo) ? [props.combo] : combo
34
34
  <span class="lp-keys" :class="`lp-keys--${size}`">
35
35
  <template v-for="(step, stepIndex) in steps" :key="stepIndex">
36
36
  <span v-if="stepIndex" class="lp-keys__then">{{ text.then }}</span>
37
- <kbd v-for="(key, index) in step" :key="`${key}-${index}`">{{ appleModifierLabel(key.trim(), apple) }}</kbd>
37
+ <!-- A chord never splits: squeezed, the keys wrap between steps, not
38
+ between Ctrl and K. -->
39
+ <span class="lp-keys__step">
40
+ <kbd v-for="(key, index) in step" :key="`${key}-${index}`">{{ appleModifierLabel(key.trim(), apple) }}</kbd>
41
+ </span>
38
42
  </template>
39
43
  <span v-if="label || $slots.default" class="lp-keys__label"><slot>{{ label }}</slot></span>
40
44
  </span>
@@ -48,6 +52,11 @@ const steps = computed(() => (Array.isArray(props.combo) ? [props.combo] : combo
48
52
  gap: 3px;
49
53
  color: var(--color-muted);
50
54
  }
55
+ .lp-keys__step {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: 3px;
59
+ }
51
60
  .lp-keys__then {
52
61
  padding: 0 2px;
53
62
  font-size: 0.68rem;
@@ -375,7 +375,9 @@ const label = computed(
375
375
  class="lp-nf inline-flex items-baseline tabular-nums"
376
376
  >
377
377
  <span class="sr-only">{{ label }}</span>
378
- <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>
379
381
 
380
382
  <span class="lp-nf__track inline-flex items-baseline" aria-hidden="true">
381
383
  <TransitionGroup name="lp-nf-cell">
@@ -402,7 +404,7 @@ const label = computed(
402
404
  </TransitionGroup>
403
405
  </span>
404
406
 
405
- <span v-if="suffix" aria-hidden="true">{{ suffix }}</span>
407
+ <span v-if="suffix" aria-hidden="true" class="whitespace-pre">{{ suffix }}</span>
406
408
  </span>
407
409
  </template>
408
410
 
@@ -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
 
@@ -25,7 +25,8 @@
25
25
  * The mechanism — measure the natural size, pin it in pixels, tween — lives in
26
26
  * `useShift`, and LpModal drives its panel with the same composable.
27
27
  */
28
- import { computed } from "vue"
28
+ import { computed, onBeforeUnmount, watch } from "vue"
29
+ import { prefersReducedMotion } from "../composables/easing"
29
30
  import { useShift, type ShiftAxis } from "../composables/useShift"
30
31
 
31
32
  const props = withDefaults(
@@ -43,8 +44,26 @@ const props = withDefaults(
43
44
  * canvas), where a tween would fight the thing already animating it.
44
45
  */
45
46
  disabled?: boolean
47
+ /**
48
+ * Which edge the content holds to while the box catches up, for a flex
49
+ * row. A box pinned to the right (`ml-auto`) grows leftwards, and content
50
+ * laid out from its left edge throws every unchanged control to the right
51
+ * for the first frame and slides it back: the controls that did not change
52
+ * are the ones that jump. `"end"` keeps them where they are and lets the
53
+ * new one come in from the free side.
54
+ */
55
+ anchor?: "start" | "end"
56
+ /**
57
+ * Fade the box's own children in and out as they come and go. Without it
58
+ * the box glides but a control still pops into being as the edge uncovers
59
+ * it, and one that leaves is gone in a frame while its space closes up.
60
+ * Only direct children: what a child does inside itself is its own affair,
61
+ * so a control that changes its label carries the label as its `key` and
62
+ * is replaced, the old one fading as the new one arrives.
63
+ */
64
+ presence?: boolean
46
65
  }>(),
47
- { axis: "height", disabled: false },
66
+ { axis: "height", disabled: false, anchor: "start", presence: false },
48
67
  )
49
68
 
50
69
  const { el, tweening, resizing } = useShift({
@@ -67,6 +86,168 @@ const TRANSITION_CLASS: Record<ShiftAxis, string> = {
67
86
  }
68
87
  const transitionClass = computed(() => TRANSITION_CLASS[props.axis])
69
88
 
89
+ /*
90
+ * Presence. A leaving element is already out of the DOM when the observer
91
+ * hears of it, so where it stood is read from the last time it was seen: its
92
+ * offset from the anchored edge, which stays true while the box eases towards
93
+ * that edge. A clone stands there, out of the flow, and fades.
94
+ *
95
+ * What stays but is moved by the change (a title centred in a box that just
96
+ * lost its subtitle) glides from where it was instead of jumping (FLIP).
97
+ */
98
+ const PRESENCE_MS = 200
99
+ const MOVE_MS = 160
100
+ const EASE = "cubic-bezier(0.2, 0, 0, 1)"
101
+ const GHOST = "data-lp-shift-ghost"
102
+
103
+ interface Seen {
104
+ from: number
105
+ top: number
106
+ width: number
107
+ height: number
108
+ x: number
109
+ y: number
110
+ }
111
+
112
+ const seen = new WeakMap<Element, Seen>()
113
+ let presenceObserver: MutationObserver | undefined
114
+ let rememberTimer: ReturnType<typeof setTimeout> | undefined
115
+ let presenceSize: ResizeObserver | undefined
116
+
117
+ function rememberSoon(box: HTMLElement, wait = PRESENCE_MS + 80) {
118
+ clearTimeout(rememberTimer)
119
+ rememberTimer = setTimeout(() => remember(box), wait)
120
+ }
121
+
122
+ function elementsOf(box: HTMLElement): Element[] {
123
+ return [...box.querySelectorAll("*")].filter((node) => !node.closest(`[${GHOST}]`) && !(node instanceof SVGElement && !(node instanceof SVGSVGElement)))
124
+ }
125
+
126
+ function remember(box: HTMLElement) {
127
+ const outer = box.getBoundingClientRect()
128
+ for (const node of elementsOf(box)) {
129
+ const r = node.getBoundingClientRect()
130
+ seen.set(node, {
131
+ from: props.anchor === "end" ? outer.right - r.right : r.left - outer.left,
132
+ top: r.top - outer.top,
133
+ width: r.width,
134
+ height: r.height,
135
+ x: r.left,
136
+ y: r.top,
137
+ })
138
+ }
139
+ }
140
+
141
+ const offset = () => (props.anchor === "end" ? -6 : 6)
142
+
143
+ function ghost(box: HTMLElement, node: Element) {
144
+ const at = seen.get(node)
145
+ if (!at || !(node instanceof HTMLElement) || node.hasAttribute(GHOST) || at.width === 0) return
146
+ const copy = node.cloneNode(true) as HTMLElement
147
+ copy.setAttribute(GHOST, "")
148
+ copy.setAttribute("aria-hidden", "true")
149
+ copy.inert = true
150
+ Object.assign(copy.style, {
151
+ position: "absolute",
152
+ top: `${at.top}px`,
153
+ [props.anchor === "end" ? "right" : "left"]: `${at.from}px`,
154
+ width: `${at.width}px`,
155
+ height: `${at.height}px`,
156
+ margin: "0",
157
+ pointerEvents: "none",
158
+ })
159
+ box.appendChild(copy)
160
+ copy
161
+ .animate(
162
+ [
163
+ { opacity: 1, transform: "none", filter: "blur(0)" },
164
+ { opacity: 0, transform: `translateX(${offset()}px) scale(0.96)`, filter: "blur(2px)" },
165
+ ],
166
+ { duration: PRESENCE_MS, easing: EASE },
167
+ )
168
+ .finished.catch(() => undefined)
169
+ .finally(() => copy.remove())
170
+ }
171
+
172
+ function arrive(node: Element) {
173
+ if (!(node instanceof HTMLElement) || node.hasAttribute(GHOST)) return
174
+ node.animate(
175
+ [
176
+ { opacity: 0, transform: `translateX(${offset()}px) scale(0.96)`, filter: "blur(2px)" },
177
+ { opacity: 1, transform: "none", filter: "blur(0)" },
178
+ ],
179
+ { duration: PRESENCE_MS, delay: 40, easing: EASE, fill: "backwards" },
180
+ )
181
+ }
182
+
183
+ function glide(node: Element) {
184
+ const was = seen.get(node)
185
+ if (!was || !(node instanceof HTMLElement)) return
186
+ const now = node.getBoundingClientRect()
187
+ const dx = was.x - now.left
188
+ const dy = was.y - now.top
189
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return
190
+ node.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "none" }], { duration: MOVE_MS, easing: EASE })
191
+ }
192
+
193
+ watch(
194
+ [el, () => props.presence && !props.disabled],
195
+ ([box, on]) => {
196
+ presenceObserver?.disconnect()
197
+ presenceObserver = undefined
198
+ presenceSize?.disconnect()
199
+ presenceSize = undefined
200
+ if (!box || !on) return
201
+ remember(box)
202
+ presenceObserver = new MutationObserver((records) => {
203
+ const real = records.filter(
204
+ (r) =>
205
+ ![...r.addedNodes, ...r.removedNodes].every((n) => n instanceof Element && n.hasAttribute(GHOST)),
206
+ )
207
+ if (real.length && !prefersReducedMotion()) {
208
+ const added = new Set<Node>()
209
+ for (const record of real) {
210
+ record.removedNodes.forEach((node) => node instanceof Element && ghost(box, node))
211
+ record.addedNodes.forEach((node) => {
212
+ added.add(node)
213
+ if (node instanceof Element) arrive(node)
214
+ })
215
+ }
216
+ // Measured in the next frame, which still comes before the paint: by
217
+ // then the box has taken the classes the change gave it (held to its
218
+ // edge), and the place an element lands is the one it will keep.
219
+ const parents = real.map((r) => r.target).filter((t): t is Element => t instanceof Element && !t.hasAttribute(GHOST))
220
+ requestAnimationFrame(() => {
221
+ for (const parent of parents) for (const child of parent.children) if (!added.has(child)) glide(child)
222
+ })
223
+ }
224
+ rememberSoon(box)
225
+ })
226
+ presenceObserver.observe(box, { childList: true, subtree: true })
227
+ presenceSize = new ResizeObserver(() => rememberSoon(box, 60))
228
+ presenceSize.observe(box)
229
+ void document.fonts?.ready.then(() => rememberSoon(box, 0))
230
+ },
231
+ { flush: "post" },
232
+ )
233
+
234
+ // A child that changed inside itself (a label rolling to a longer word) moves
235
+ // its neighbours without any child coming or going: when the box starts to
236
+ // resize, every child glides from where it was last seen.
237
+ watch(resizing, (now) => {
238
+ const box = el.value
239
+ if (!now || !box || !presenceObserver || prefersReducedMotion()) return
240
+ requestAnimationFrame(() => {
241
+ for (const child of box.children) if (!child.hasAttribute(GHOST)) glide(child)
242
+ })
243
+ })
244
+
245
+ onBeforeUnmount(() => {
246
+ presenceObserver?.disconnect()
247
+ presenceSize?.disconnect()
248
+ clearTimeout(rememberTimer)
249
+ })
250
+
70
251
  defineExpose({ resizing })
71
252
  </script>
72
253
 
@@ -83,6 +264,12 @@ defineExpose({ resizing })
83
264
  ? `${transitionClass} duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none`
84
265
  : '',
85
266
  resizing ? 'overflow-hidden' : '',
267
+ // While the box holds a width its content has outgrown, a flex row would
268
+ // squeeze its children to fit (a button's keys folding into a column)
269
+ // instead of letting the easing edge clip them.
270
+ resizing && axis !== 'height' ? '[&>*]:shrink-0' : '',
271
+ resizing && anchor === 'end' ? 'justify-end' : '',
272
+ presence ? 'relative' : '',
86
273
  ]"
87
274
  >
88
275
  <slot :resizing="resizing" />