@leavepulse/ui 0.77.3 → 0.78.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 (597) hide show
  1. package/README.md +68 -0
  2. package/dist/assets/{LpCommandPalette-DOIZHw-g.css → LpCommandPalette-DDJKICKh.css} +19 -19
  3. package/dist/assets/{LpDock-Cbh0wQUv.css → LpDock-Dw82tWVD.css} +3 -3
  4. package/dist/assets/{LpDropzone-B_qRq1-R.css → LpDropzone-DNbh_NFJ.css} +10 -10
  5. package/dist/assets/{LpImageEditor-XM5bAziW.css → LpImageEditor-Bgu20XLs.css} +6 -6
  6. package/dist/assets/{LpTable-Bv-EcQs7.css → LpTable-CCk0ZHo7.css} +19 -19
  7. package/dist/assets/LpTabs-Bh7a-Bdl.css +18 -0
  8. package/dist/chunks/AppShellContextNav-C6Ho2mof.js +49 -0
  9. package/dist/chunks/ChartRangeSelector-HO0sMMz6.js +129 -0
  10. package/dist/chunks/{ContextMenuItems-BXumicbh.js → ContextMenuItems-DmaKSJVv.js} +1 -1
  11. package/dist/chunks/{DropzoneLinkField-C-9BCGJn.js → DropzoneLinkField-DXIr0eFz.js} +2 -2
  12. package/dist/chunks/{FieldWrapper-CORPzo9s.js → FieldWrapper-DnuMTrqW.js} +1 -1
  13. package/dist/chunks/IconCloseButton-COZEsa9u.js +29 -0
  14. package/dist/chunks/{LayoutCanvas-CNOAUk7D.js → LayoutCanvas-CZKOltfA.js} +1 -1
  15. package/dist/chunks/{LayoutNode-DF5BMN0J.js → LayoutNode-D2f7v683.js} +45 -45
  16. package/dist/chunks/{LpActionBar-BP1ld7-S.js → LpActionBar-BGMk5KoG.js} +3 -3
  17. package/dist/chunks/{LpActivityLine-CJvzrNpj.js → LpActivityLine-Ea-t5Vza.js} +4 -2
  18. package/dist/chunks/{LpAddressInput-E8KtwU7-.js → LpAddressInput-ZqoZ-N2Y.js} +12 -11
  19. package/dist/chunks/{LpAlert-n5XPAZfv.js → LpAlert-CjvTZIKF.js} +3 -3
  20. package/dist/chunks/LpAppShell-BNAVTQsY.js +582 -0
  21. package/dist/chunks/{LpAutocomplete-C-SpPfBG.js → LpAutocomplete-DkY6CSyD.js} +24 -42
  22. package/dist/chunks/{LpAvatar-Cg6Az3wz.js → LpAvatar-cQVsNk6F.js} +2 -2
  23. package/dist/chunks/{LpBadge-wUYLN4U9.js → LpBadge-D5QQdDQ_.js} +2 -2
  24. package/dist/chunks/{LpBottomNav-CbNQQPuL.js → LpBottomNav-zxLGGbjm.js} +9 -2
  25. package/dist/chunks/{LpBreadcrumbs-DPdbth-c.js → LpBreadcrumbs-CIIBsygI.js} +4 -3
  26. package/dist/chunks/{LpButton-CFb44IjC.js → LpButton-DSHioIfD.js} +5 -4
  27. package/dist/chunks/{LpCalendar-CuoWks-0.js → LpCalendar-D4-LVjfF.js} +1 -1
  28. package/dist/chunks/{LpCard-r13WRqox.js → LpCard-BPtYXwnT.js} +1 -1
  29. package/dist/chunks/{LpCheckbox-Cs4SHaHX.js → LpCheckbox-D6RuDW4X.js} +38 -16
  30. package/dist/chunks/{LpCodeBlock-DRC8VbXW.js → LpCodeBlock-DuzmnIHl.js} +19 -41
  31. package/dist/chunks/{LpCodeDisplay-CE9TxCA4.js → LpCodeDisplay-CO8fngxa.js} +5 -5
  32. package/dist/chunks/{LpCollapse-CWS9M_lm.js → LpCollapse-DrAw5DBZ.js} +5 -4
  33. package/dist/chunks/{LpColorPicker-1y1QJHZR.js → LpColorPicker-BxAHEnR5.js} +11 -12
  34. package/dist/chunks/{LpCommandScope-CtUhJe8T.js → LpCommandScope-DRUvHiX7.js} +1 -1
  35. package/dist/chunks/{LpConfirmDialog-DlD45Bi_.js → LpConfirmDialog-6oCIJTDa.js} +3 -3
  36. package/dist/chunks/{LpConfirmHost-BIWjBLmI.js → LpConfirmHost-DgHvfT6s.js} +1 -1
  37. package/dist/chunks/{LpContextMenu-rx2tCjwk.js → LpContextMenu--eMGIql6.js} +13 -15
  38. package/dist/chunks/{LpContextSwitcher-Dwh6dyID.js → LpContextSwitcher-1C1tExdl.js} +2 -2
  39. package/dist/chunks/{LpCopyButton-xF7wFVoU.js → LpCopyButton-7EOSIuwa.js} +3 -3
  40. package/dist/chunks/{LpCountdown-DgeY3cLG.js → LpCountdown-DVl7vXI1.js} +4 -3
  41. package/dist/chunks/{LpDataList-Jxyl9H5w.js → LpDataList-DgBeM01y.js} +13 -4
  42. package/dist/chunks/{LpDatePicker-CA0QTfBC.js → LpDatePicker-DwSDY0mt.js} +12 -18
  43. package/dist/chunks/{LpDateRangePicker-u3J51xXk.js → LpDateRangePicker-CNQXdTVv.js} +10 -16
  44. package/dist/chunks/{LpDisclosure-Cpa7Hc5T.js → LpDisclosure-DG7JCo-d.js} +2 -2
  45. package/dist/chunks/{LpDrawer-CrG8t_KP.js → LpDrawer-Bt9UUqt_.js} +61 -61
  46. package/dist/chunks/{LpDropdownMenu-Dl7Z4ZhS.js → LpDropdownMenu-CLzxIOgM.js} +9 -17
  47. package/dist/chunks/{LpEditable-Cy6GLbFx.js → LpEditable-DVT0Rh8F.js} +9 -6
  48. package/dist/chunks/{LpEmptyState-EQHiRYhG.js → LpEmptyState-DurPF001.js} +1 -1
  49. package/dist/chunks/{LpFieldNotice-CzQ2_5rQ.js → LpFieldNotice-DJ10cYIo.js} +1 -1
  50. package/dist/chunks/LpFileTree-BSx1kBoV.js +735 -0
  51. package/dist/chunks/{LpFileTreeNode-cT6BIWvz.js → LpFileTreeNode-4dVmFHb3.js} +116 -34
  52. package/dist/chunks/{LpFormField-Ch69Jd-X.js → LpFormField-BKYn9dyP.js} +3 -3
  53. package/dist/chunks/{LpFrame-BVQCQLDI.js → LpFrame-Bv5Fphte.js} +25 -11
  54. package/dist/chunks/{LpHintExample-9Py0wc-L.js → LpHintExample-YzCXiSUp.js} +1 -1
  55. package/dist/chunks/{LpHintLayer-CgObdUsM.js → LpHintLayer-BeKRsRon.js} +2 -2
  56. package/dist/chunks/{LpHints-Bz-BlaSl.js → LpHints-qf3xYsza.js} +3 -3
  57. package/dist/chunks/{LpHoverCard-spGxyw7T.js → LpHoverCard-DanH3Nz1.js} +23 -5
  58. package/dist/chunks/{LpIcon--5WSb3jJ.js → LpIcon-C6JpQ5td.js} +6 -3
  59. package/dist/chunks/{LpIdenticon-BzdC_qz9.js → LpIdenticon-Dbbr4fvt.js} +2 -1
  60. package/dist/chunks/{LpInfraNode-46XOCkfq.js → LpInfraNode-BAU40Km2.js} +2 -2
  61. package/dist/chunks/{LpInput-Tw-yxT4I.js → LpInput-DL6HEJdi.js} +16 -28
  62. package/dist/chunks/LpInspector-CuoMAhiw.js +209 -0
  63. package/dist/chunks/{LpLightbox-B95yu22B.js → LpLightbox-xvpQ64MM.js} +57 -32
  64. package/dist/chunks/{LpLoader-DVwkOmt9.js → LpLoader-Bf1yZwaU.js} +1 -1
  65. package/dist/chunks/LpLogInspector-6cNnXqp4.js +119 -0
  66. package/dist/chunks/{LpLogViewer-ngqkns4m.js → LpLogViewer-Dvb1E9_G.js} +234 -53
  67. package/dist/chunks/{LpMapPicker-DRGgh89B.js → LpMapPicker-D5_YLLBC.js} +4 -3
  68. package/dist/chunks/{LpMasonry--k0571qG.js → LpMasonry-DPYekFxs.js} +8 -5
  69. package/dist/chunks/{LpMediaGallery-Bkram-MO.js → LpMediaGallery-Df-tiWeq.js} +3 -3
  70. package/dist/chunks/{LpModal-CoGhLTyH.js → LpModal-DrtQ_1A2.js} +11 -13
  71. package/dist/chunks/{LpModalNav-BGvs2y5Q.js → LpModalNav-DkmGCnZE.js} +11 -37
  72. package/dist/chunks/{LpNotificationBell-B0034Lk-.js → LpNotificationBell-D_wklO1m.js} +9 -9
  73. package/dist/chunks/{LpNumberField-e0IBkWU1.js → LpNumberField-CNHsmSDZ.js} +7 -16
  74. package/dist/chunks/{LpOtpInput-CeBPsM7D.js → LpOtpInput-BRpLBbOD.js} +2 -2
  75. package/dist/chunks/{LpPagination-BIIiYHXR.js → LpPagination-snQ020xQ.js} +6 -4
  76. package/dist/chunks/{LpPaletteTheme-DiLYKQiZ.js → LpPaletteTheme-dVtxHx-F.js} +4 -4
  77. package/dist/chunks/{LpPasswordInput-BkHKQDhM.js → LpPasswordInput-B-zD6tUS.js} +2 -2
  78. package/dist/chunks/{LpPhoneInput-CETEp5W7.js → LpPhoneInput-BSL7NX4z.js} +15 -26
  79. package/dist/chunks/{LpPie-DrZepi0p.js → LpPie-9t_guO49.js} +22 -21
  80. package/dist/chunks/{LpPipeline-B3aOVOkV.js → LpPipeline-rjXvEDRJ.js} +16 -7
  81. package/dist/chunks/{LpPopover-DmZ3wUtP.js → LpPopover-BQSuxtai.js} +10 -1
  82. package/dist/chunks/{LpProgress-Yr6ONG5t.js → LpProgress-D_MSjowb.js} +2 -1
  83. package/dist/chunks/{LpQuota-Dk2MyI2Z.js → LpQuota-CgWPgdVt.js} +1 -1
  84. package/dist/chunks/{LpRadioGroup-ggaPL92T.js → LpRadioGroup-BrazjLY_.js} +4 -4
  85. package/dist/chunks/{LpRangeCalendar-CYYUYqJ7.js → LpRangeCalendar-BrqlGZzE.js} +1 -1
  86. package/dist/chunks/{LpRangeSlider-CM-0ZITZ.js → LpRangeSlider-fz61tzc-.js} +5 -9
  87. package/dist/chunks/{LpRelativeTime-DwgCb5-8.js → LpRelativeTime-BovUk8MP.js} +1 -1
  88. package/dist/chunks/{LpRepeater-D7ZmnIYd.js → LpRepeater-BkJHSPI1.js} +3 -3
  89. package/dist/chunks/{LpRevealOnScroll-DcQjexLy.js → LpRevealOnScroll-CPwyiQAr.js} +1 -1
  90. package/dist/chunks/{LpScramble-Bn9OoOtC.js → LpScramble-DsE77iVp.js} +1 -1
  91. package/dist/chunks/{LpScrollArea-MyEnVB_5.js → LpScrollArea-ay4Xo_hg.js} +22 -10
  92. package/dist/chunks/{LpSecretField-BbmqM9ye.js → LpSecretField-DBvU6iAw.js} +10 -9
  93. package/dist/chunks/{LpSection-DFK_uoJE.js → LpSection-BfteipxU.js} +1 -1
  94. package/dist/chunks/{LpSegmented-CeACsMMA.js → LpSegmented-DbEJhbv-.js} +8 -10
  95. package/dist/chunks/{LpSelect-Cxq4yBGd.js → LpSelect-Y-iuSymW.js} +47 -30
  96. package/dist/chunks/{LpShaderBackdrop-Bote0iuy.js → LpShaderBackdrop-wQygYb5h.js} +13 -10
  97. package/dist/chunks/{LpShift-CCq7dofl.js → LpShift-DAgS_d2x.js} +18 -15
  98. package/dist/chunks/{LpSidebar-DzE1783C.js → LpSidebar-kSkpPh3-.js} +74 -87
  99. package/dist/chunks/{LpSidebarNav-DE3PhXVP.js → LpSidebarNav-C2mvqrY2.js} +8 -6
  100. package/dist/chunks/{LpSidebarRow-BRYhNDSo.js → LpSidebarRow-eRP4Q31q.js} +6 -5
  101. package/dist/chunks/{LpSlider-DxYoCJJO.js → LpSlider-A4ixovgS.js} +5 -9
  102. package/dist/chunks/{LpSpotlight-4F3NeUMY.js → LpSpotlight-DwxkhFgR.js} +2 -1
  103. package/dist/chunks/{LpStat-f2LfDCJ4.js → LpStat-pMNZgFcq.js} +2 -2
  104. package/dist/chunks/{LpStatus-DglxttQ7.js → LpStatus-DGJFwk5F.js} +2 -2
  105. package/dist/chunks/{LpStepMark-DpzKVyh8.js → LpStepMark-Dw2MRTrt.js} +2 -2
  106. package/dist/chunks/{LpStepper-DpsocBNJ.js → LpStepper-DNnh7lCu.js} +1 -1
  107. package/dist/chunks/{LpSwap-C96c0LWh.js → LpSwap-M7nFeW3C.js} +1 -1
  108. package/dist/chunks/{LpSwitch-BnXd1llc.js → LpSwitch-DnC2-0aV.js} +3 -3
  109. package/dist/chunks/{LpTabFace-DPCwKkH2.js → LpTabFace-DQoV8mQG.js} +2 -2
  110. package/dist/chunks/{LpTableFilter-CiVBpaq0.js → LpTableFilter-DgARTsA1.js} +4 -4
  111. package/dist/chunks/{LpTableOfContents-BN5zvpnx.js → LpTableOfContents-CKkWEK6R.js} +14 -9
  112. package/dist/chunks/{LpTagsInput-C-mDG648.js → LpTagsInput-Dt0mteSj.js} +19 -27
  113. package/dist/chunks/{LpTextarea-TE-yZv2U.js → LpTextarea-Co0b9PVx.js} +8 -16
  114. package/dist/chunks/LpThemeProvider-XK6ZeWAE.js +81 -0
  115. package/dist/chunks/{LpThemeStudio-BpORDvH6.js → LpThemeStudio-DzYfE-5k.js} +12 -195
  116. package/dist/chunks/{LpThemeSwitcher-BnEn_mvf.js → LpThemeSwitcher-CFLkdbBL.js} +8 -6
  117. package/dist/chunks/{LpTimeField-BawSVsmt.js → LpTimeField-DeOcKWJq.js} +6 -9
  118. package/dist/chunks/{LpToaster-IwTXjbbU.js → LpToaster-QFj0P-5m.js} +10 -8
  119. package/dist/chunks/{LpTooltip-BcBvIx4z.js → LpTooltip-BJf3lZMq.js} +51 -19
  120. package/dist/chunks/LpTopologyCanvas-BOHHHZ_6.js +673 -0
  121. package/dist/chunks/{LpTour-BZq-mUMv.js → LpTour-ClKYhx4h.js} +7 -5
  122. package/dist/chunks/{LpTruncate-CflAIwiJ.js → LpTruncate-C6u3gjc6.js} +6 -5
  123. package/dist/chunks/{LpTypewriter-BCL_a6wE.js → LpTypewriter-CZIzpdbe.js} +2 -1
  124. package/dist/chunks/{LpUptimeBar-DqNvvMBK.js → LpUptimeBar-BnYmR2rj.js} +3 -3
  125. package/dist/chunks/{LpVideoBackdrop-DS1wGv-X.js → LpVideoBackdrop-Ct4St4ZL.js} +10 -13
  126. package/dist/chunks/LpWindow-B8YSRVTH.js +297 -0
  127. package/dist/chunks/LpWorkspaceSwitcher-D0e9upAy.js +139 -0
  128. package/dist/chunks/{MenuTitle-BmCNNTKH.js → MenuTitle-Bh5dAdV1.js} +1 -1
  129. package/dist/chunks/{OptionContent-22UT8zLn.js → OptionContent-CGb8QlHA.js} +1 -1
  130. package/dist/chunks/{PickerField-Bfh8id3d.js → PickerField-BM1dyafh.js} +4 -4
  131. package/dist/chunks/{RevealToggle-BEit1n6Z.js → RevealToggle-CCPDE8Mv.js} +3 -3
  132. package/dist/chunks/SliderRail-DhPREa_R.js +18 -0
  133. package/dist/chunks/{TourCard-DNclH3u_.js → TourCard-DAzTYfav.js} +4 -4
  134. package/dist/chunks/{icons-DMITlfw5.js → icons-CYfcpSDS.js} +26 -10
  135. package/dist/component-names.d.ts +1 -1
  136. package/dist/components/AppShellContextNav.vue.d.ts +13 -0
  137. package/dist/components/AppShellContextNav.vue.js +4 -0
  138. package/dist/components/ChartRangeSelector.vue.d.ts +21 -0
  139. package/dist/components/ChartRangeSelector.vue.js +4 -0
  140. package/dist/components/ContextMenuItems.vue.js +1 -1
  141. package/dist/components/DropzoneLinkField.vue.d.ts +2 -2
  142. package/dist/components/DropzoneLinkField.vue.js +1 -1
  143. package/dist/components/FieldWrapper.vue.js +1 -1
  144. package/dist/components/IconCloseButton.vue.d.ts +7 -0
  145. package/dist/components/IconCloseButton.vue.js +4 -0
  146. package/dist/components/LayoutCanvas.vue.js +1 -1
  147. package/dist/components/LayoutNode.vue.js +1 -1
  148. package/dist/components/LpActionBar.vue.js +1 -1
  149. package/dist/components/LpActivityLine.vue.js +1 -1
  150. package/dist/components/LpAddressInput.vue.js +1 -1
  151. package/dist/components/LpAlert.vue.js +1 -1
  152. package/dist/components/LpAppShell.vue.d.ts +110 -2
  153. package/dist/components/LpAppShell.vue.js +1 -1
  154. package/dist/components/LpAutocomplete.vue.js +1 -1
  155. package/dist/components/LpAvatar.vue.js +1 -1
  156. package/dist/components/LpBadge.vue.js +1 -1
  157. package/dist/components/LpBottomNav.vue.d.ts +5 -0
  158. package/dist/components/LpBottomNav.vue.js +1 -1
  159. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  160. package/dist/components/LpButton.vue.d.ts +14 -2
  161. package/dist/components/LpButton.vue.js +1 -1
  162. package/dist/components/LpCalendar.vue.js +1 -1
  163. package/dist/components/LpCard.vue.js +1 -1
  164. package/dist/components/LpChart.vue.d.ts +74 -2
  165. package/dist/components/LpChart.vue.js +326 -85
  166. package/dist/components/LpCheckbox.vue.d.ts +6 -2
  167. package/dist/components/LpCheckbox.vue.js +1 -1
  168. package/dist/components/LpCodeBlock.vue.js +1 -1
  169. package/dist/components/LpCodeDisplay.vue.js +1 -1
  170. package/dist/components/LpCollapse.vue.js +1 -1
  171. package/dist/components/LpColorPicker.vue.js +1 -1
  172. package/dist/components/LpCommandPalette.vue.d.ts +8 -0
  173. package/dist/components/LpCommandPalette.vue.js +107 -56
  174. package/dist/components/LpCommandScope.vue.js +1 -1
  175. package/dist/components/LpConfirmDialog.vue.js +1 -1
  176. package/dist/components/LpConfirmHost.vue.js +1 -1
  177. package/dist/components/LpContextMenu.vue.js +1 -1
  178. package/dist/components/LpContextSwitcher.vue.js +1 -1
  179. package/dist/components/LpCopyButton.vue.js +1 -1
  180. package/dist/components/LpCountdown.vue.js +1 -1
  181. package/dist/components/LpDataList.vue.d.ts +1 -1
  182. package/dist/components/LpDataList.vue.js +1 -1
  183. package/dist/components/LpDatePicker.vue.d.ts +3 -15
  184. package/dist/components/LpDatePicker.vue.js +1 -1
  185. package/dist/components/LpDateRangePicker.vue.d.ts +3 -13
  186. package/dist/components/LpDateRangePicker.vue.js +1 -1
  187. package/dist/components/LpDisclosure.vue.js +1 -1
  188. package/dist/components/LpDock.vue.js +21 -18
  189. package/dist/components/LpDrawer.vue.d.ts +1 -1
  190. package/dist/components/LpDrawer.vue.js +1 -1
  191. package/dist/components/LpDropdownMenu.vue.js +1 -1
  192. package/dist/components/LpDropzone.vue.js +14 -11
  193. package/dist/components/LpEditable.vue.js +1 -1
  194. package/dist/components/LpEmptyState.vue.js +1 -1
  195. package/dist/components/LpFieldNotice.vue.js +1 -1
  196. package/dist/components/LpFileTree.vue.d.ts +96 -6
  197. package/dist/components/LpFileTree.vue.js +1 -1
  198. package/dist/components/LpFileTreeNode.vue.d.ts +48 -0
  199. package/dist/components/LpFileTreeNode.vue.js +1 -1
  200. package/dist/components/LpFormField.vue.js +1 -1
  201. package/dist/components/LpFrame.vue.js +1 -1
  202. package/dist/components/LpGauge.vue.d.ts +1 -1
  203. package/dist/components/LpHintExample.vue.js +1 -1
  204. package/dist/components/LpHintLayer.vue.js +1 -1
  205. package/dist/components/LpHints.vue.js +1 -1
  206. package/dist/components/LpHoverCard.vue.js +1 -1
  207. package/dist/components/LpIcon.vue.d.ts +2 -0
  208. package/dist/components/LpIcon.vue.js +1 -1
  209. package/dist/components/LpIdenticon.vue.js +1 -1
  210. package/dist/components/LpImageEditor.vue.js +7 -6
  211. package/dist/components/LpInfraNode.vue.js +1 -1
  212. package/dist/components/LpInput.vue.d.ts +1 -1
  213. package/dist/components/LpInput.vue.js +1 -1
  214. package/dist/components/LpInspector.vue.d.ts +87 -0
  215. package/dist/components/LpInspector.vue.js +4 -0
  216. package/dist/components/LpLightbox.vue.d.ts +8 -0
  217. package/dist/components/LpLightbox.vue.js +1 -1
  218. package/dist/components/LpLoader.vue.d.ts +1 -1
  219. package/dist/components/LpLoader.vue.js +1 -1
  220. package/dist/components/LpLogInspector.vue.d.ts +18 -0
  221. package/dist/components/LpLogInspector.vue.js +4 -0
  222. package/dist/components/LpLogViewer.vue.d.ts +93 -0
  223. package/dist/components/LpLogViewer.vue.js +1 -1
  224. package/dist/components/LpMapPicker.vue.js +1 -1
  225. package/dist/components/LpMasonry.vue.js +1 -1
  226. package/dist/components/LpMediaGallery.vue.js +1 -1
  227. package/dist/components/LpModal.vue.d.ts +4 -2
  228. package/dist/components/LpModal.vue.js +1 -1
  229. package/dist/components/LpModalNav.vue.d.ts +5 -5
  230. package/dist/components/LpModalNav.vue.js +1 -1
  231. package/dist/components/LpNotificationBell.vue.d.ts +2 -2
  232. package/dist/components/LpNotificationBell.vue.js +1 -1
  233. package/dist/components/LpNumberField.vue.js +1 -1
  234. package/dist/components/LpOtpInput.vue.js +1 -1
  235. package/dist/components/LpPagination.vue.js +1 -1
  236. package/dist/components/LpPaletteFacts.vue.js +1 -1
  237. package/dist/components/LpPaletteHeader.vue.js +2 -2
  238. package/dist/components/LpPaletteTheme.vue.js +1 -1
  239. package/dist/components/LpPasswordInput.vue.js +1 -1
  240. package/dist/components/LpPhoneInput.vue.js +1 -1
  241. package/dist/components/LpPie.vue.d.ts +2 -2
  242. package/dist/components/LpPie.vue.js +1 -1
  243. package/dist/components/LpPipeline.vue.d.ts +4 -0
  244. package/dist/components/LpPipeline.vue.js +1 -1
  245. package/dist/components/LpPopover.vue.d.ts +7 -3
  246. package/dist/components/LpPopover.vue.js +1 -1
  247. package/dist/components/LpProgress.vue.d.ts +1 -1
  248. package/dist/components/LpProgress.vue.js +1 -1
  249. package/dist/components/LpQuota.vue.js +1 -1
  250. package/dist/components/LpRadioGroup.vue.js +1 -1
  251. package/dist/components/LpRangeCalendar.vue.js +1 -1
  252. package/dist/components/LpRangeSlider.vue.js +1 -1
  253. package/dist/components/LpRelativeTime.vue.js +1 -1
  254. package/dist/components/LpRepeater.vue.js +1 -1
  255. package/dist/components/LpRevealOnScroll.vue.js +1 -1
  256. package/dist/components/LpScramble.vue.js +1 -1
  257. package/dist/components/LpScrollArea.vue.js +1 -1
  258. package/dist/components/LpSecretField.vue.js +1 -1
  259. package/dist/components/LpSection.vue.js +1 -1
  260. package/dist/components/LpSegmented.vue.js +1 -1
  261. package/dist/components/LpSelect.vue.js +1 -1
  262. package/dist/components/LpShaderBackdrop.vue.js +1 -1
  263. package/dist/components/LpShift.vue.js +1 -1
  264. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  265. package/dist/components/LpSidebar.vue.d.ts +1 -1
  266. package/dist/components/LpSidebar.vue.js +1 -1
  267. package/dist/components/LpSidebarNav.vue.js +1 -1
  268. package/dist/components/LpSidebarRow.vue.js +1 -1
  269. package/dist/components/LpSlider.vue.js +1 -1
  270. package/dist/components/LpSparkline.vue.d.ts +1 -1
  271. package/dist/components/LpSpotlight.vue.js +1 -1
  272. package/dist/components/LpStat.vue.js +1 -1
  273. package/dist/components/LpStatus.vue.js +1 -1
  274. package/dist/components/LpStepMark.vue.js +1 -1
  275. package/dist/components/LpStepper.vue.js +1 -1
  276. package/dist/components/LpSwap.vue.js +1 -1
  277. package/dist/components/LpSwitch.vue.js +1 -1
  278. package/dist/components/LpTabFace.vue.js +1 -1
  279. package/dist/components/LpTable.vue.d.ts +132 -0
  280. package/dist/components/LpTable.vue.js +547 -135
  281. package/dist/components/LpTableFilter.vue.js +1 -1
  282. package/dist/components/LpTableOfContents.vue.js +1 -1
  283. package/dist/components/LpTabs.vue.js +6 -5
  284. package/dist/components/LpTagsInput.vue.js +1 -1
  285. package/dist/components/LpTextarea.vue.js +1 -1
  286. package/dist/components/LpThemeProvider.vue.d.ts +2 -0
  287. package/dist/components/LpThemeProvider.vue.js +1 -1
  288. package/dist/components/LpThemeStudio.vue.js +1 -1
  289. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  290. package/dist/components/LpTimeField.vue.js +1 -1
  291. package/dist/components/LpToaster.vue.js +1 -1
  292. package/dist/components/LpTooltip.vue.js +1 -1
  293. package/dist/components/LpTopologyCanvas.vue.d.ts +131 -5
  294. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  295. package/dist/components/LpTour.vue.js +1 -1
  296. package/dist/components/LpTruncate.vue.js +1 -1
  297. package/dist/components/LpTypewriter.vue.js +1 -1
  298. package/dist/components/LpUptimeBar.vue.js +1 -1
  299. package/dist/components/LpVideoBackdrop.vue.js +1 -1
  300. package/dist/components/LpWindow.vue.d.ts +99 -0
  301. package/dist/components/LpWindow.vue.js +4 -0
  302. package/dist/components/LpWorkspaceSwitcher.vue.d.ts +18 -0
  303. package/dist/components/LpWorkspaceSwitcher.vue.js +4 -0
  304. package/dist/components/MenuTitle.vue.js +1 -1
  305. package/dist/components/OptionContent.vue.js +1 -1
  306. package/dist/components/PickerField.vue.d.ts +2 -2
  307. package/dist/components/PickerField.vue.js +1 -1
  308. package/dist/components/RevealToggle.vue.js +1 -1
  309. package/dist/components/SliderRail.vue.d.ts +3 -0
  310. package/dist/components/SliderRail.vue.js +4 -0
  311. package/dist/components/TourCard.vue.js +1 -1
  312. package/dist/components/address.js +2 -1
  313. package/dist/components/appShell.d.ts +36 -0
  314. package/dist/components/appShell.js +30 -0
  315. package/dist/components/calendar.d.ts +12 -3
  316. package/dist/components/calendar.js +2 -5
  317. package/dist/components/chart.d.ts +21 -0
  318. package/dist/components/chart.js +4 -3
  319. package/dist/components/chartWindow.d.ts +57 -0
  320. package/dist/components/chartWindow.js +136 -0
  321. package/dist/components/color.js +1 -1
  322. package/dist/components/commandPalette.d.ts +16 -2
  323. package/dist/components/commandPalette.js +39 -4
  324. package/dist/components/controlSize.d.ts +5 -0
  325. package/dist/components/controlSize.js +8 -0
  326. package/dist/components/dropdown.d.ts +5 -0
  327. package/dist/components/dropdown.js +2 -0
  328. package/dist/components/exportData.d.ts +6 -1
  329. package/dist/components/exportData.js +25 -18
  330. package/dist/components/fileTree.d.ts +67 -0
  331. package/dist/components/fileTree.js +141 -1
  332. package/dist/components/identicon.js +3 -2
  333. package/dist/components/logTools.d.ts +93 -0
  334. package/dist/components/logTools.js +210 -0
  335. package/dist/components/menuWhen.js +1 -1
  336. package/dist/components/meter.js +2 -1
  337. package/dist/components/pipeline.d.ts +2 -0
  338. package/dist/components/pipeline.js +5 -0
  339. package/dist/components/relativeTime.js +2 -1
  340. package/dist/components/reveal.js +2 -1
  341. package/dist/components/select.d.ts +5 -0
  342. package/dist/components/select.js +10 -0
  343. package/dist/components/sidebar.d.ts +1 -0
  344. package/dist/components/tableFeatures.d.ts +58 -0
  345. package/dist/components/tableFeatures.js +87 -0
  346. package/dist/components/topologyGraph.d.ts +164 -0
  347. package/dist/components/topologyGraph.js +416 -0
  348. package/dist/components/tour.js +5 -3
  349. package/dist/components/videoBackdrop.js +7 -6
  350. package/dist/composables/clamp.d.ts +2 -0
  351. package/dist/composables/clamp.js +4 -0
  352. package/dist/composables/devWarn.d.ts +3 -0
  353. package/dist/composables/devWarn.js +14 -0
  354. package/dist/composables/easing.d.ts +13 -4
  355. package/dist/composables/easing.js +16 -10
  356. package/dist/composables/overlayHistory.js +14 -1
  357. package/dist/composables/pointerDrag.d.ts +15 -0
  358. package/dist/composables/pointerDrag.js +40 -0
  359. package/dist/composables/propCheck.d.ts +10 -0
  360. package/dist/composables/propCheck.js +78 -0
  361. package/dist/composables/realm.d.ts +6 -0
  362. package/dist/composables/realm.js +23 -0
  363. package/dist/composables/scroll.js +4 -2
  364. package/dist/composables/scrollMemory.d.ts +12 -1
  365. package/dist/composables/scrollMemory.js +36 -12
  366. package/dist/composables/spatialNav.js +2 -1
  367. package/dist/composables/textEntry.d.ts +17 -0
  368. package/dist/composables/textEntry.js +19 -0
  369. package/dist/composables/useAsyncTask.d.ts +6 -0
  370. package/dist/composables/useChartSync.d.ts +5 -0
  371. package/dist/composables/useChartSync.js +33 -0
  372. package/dist/composables/useCloseGuard.d.ts +3 -1
  373. package/dist/composables/useCloseGuard.js +6 -3
  374. package/dist/composables/useCommands.d.ts +50 -0
  375. package/dist/composables/useCommands.js +66 -0
  376. package/dist/composables/useField.d.ts +37 -0
  377. package/dist/composables/useField.js +27 -2
  378. package/dist/composables/useFitScale.js +4 -1
  379. package/dist/composables/useFormDraft.d.ts +13 -2
  380. package/dist/composables/useFormDraft.js +17 -9
  381. package/dist/composables/useHintSystem.js +2 -2
  382. package/dist/composables/useHotkeys.js +11 -32
  383. package/dist/composables/useInView.js +2 -1
  384. package/dist/composables/useInputFilter.js +7 -5
  385. package/dist/composables/useInputMask.js +5 -3
  386. package/dist/composables/useLinkedView.d.ts +97 -0
  387. package/dist/composables/useLinkedView.js +139 -0
  388. package/dist/composables/useOptimisticToggle.d.ts +3 -1
  389. package/dist/composables/useOptimisticToggle.js +2 -1
  390. package/dist/composables/useOverlayHistory.d.ts +7 -0
  391. package/dist/composables/useOverlayHistory.js +16 -2
  392. package/dist/composables/usePaintSelect.js +18 -14
  393. package/dist/composables/usePaletteCommands.d.ts +7 -0
  394. package/dist/composables/usePaletteCommands.js +12 -1
  395. package/dist/composables/usePersisted.d.ts +10 -0
  396. package/dist/composables/usePersisted.js +1 -1
  397. package/dist/composables/usePillTransition.d.ts +2 -2
  398. package/dist/composables/usePillTransition.js +5 -5
  399. package/dist/composables/useResizeObserver.js +2 -1
  400. package/dist/composables/useReveal.js +2 -1
  401. package/dist/composables/useRovingFocus.d.ts +2 -2
  402. package/dist/composables/useRovingFocus.js +9 -11
  403. package/dist/composables/useScramble.js +7 -1
  404. package/dist/composables/useShift.js +8 -6
  405. package/dist/composables/useTilt.js +2 -1
  406. package/dist/composables/useTween.js +9 -5
  407. package/dist/composables/useView.d.ts +1 -0
  408. package/dist/composables/useView.js +14 -2
  409. package/dist/composables/useZoomPan.js +2 -4
  410. package/dist/composables/windowing.d.ts +40 -0
  411. package/dist/composables/windowing.js +118 -0
  412. package/dist/icons-all.js +1 -1
  413. package/dist/icons.d.ts +4 -0
  414. package/dist/index.d.ts +30 -5
  415. package/dist/index.js +310 -264
  416. package/dist/layout/tree.js +19 -15
  417. package/dist/layout/useLayout.d.ts +2 -0
  418. package/dist/layout/useLayout.js +5 -1
  419. package/dist/layout/useWorkspaces.d.ts +52 -0
  420. package/dist/layout/useWorkspaces.js +140 -0
  421. package/dist/{chunks/backdrop-Dkf2vM81.js → theme/backdrop.js} +5 -5
  422. package/dist/theme/config.js +25 -0
  423. package/dist/theme/define.js +61 -0
  424. package/dist/{chunks/palette-Co8tSASo.js → theme/palette.js} +2 -2
  425. package/dist/theme/presets.js +239 -0
  426. package/dist/theme/studio.js +134 -0
  427. package/dist/theme/tokens.js +63 -0
  428. package/dist/theme/useTheme.d.ts +3 -2
  429. package/dist/theme/useTheme.js +325 -0
  430. package/package.json +8 -1
  431. package/src/component-names.ts +5 -1
  432. package/src/components/AppShellContextNav.vue +44 -0
  433. package/src/components/ChartRangeSelector.vue +120 -0
  434. package/src/components/IconCloseButton.vue +21 -0
  435. package/src/components/LayoutCanvas.vue +3 -0
  436. package/src/components/LayoutNode.vue +37 -38
  437. package/src/components/LpActivityLine.vue +4 -2
  438. package/src/components/LpAddressInput.vue +3 -3
  439. package/src/components/LpAppShell.vue +362 -24
  440. package/src/components/LpAutocomplete.vue +13 -30
  441. package/src/components/LpBottomNav.vue +9 -1
  442. package/src/components/LpBreadcrumbs.vue +2 -1
  443. package/src/components/LpButton.vue +11 -4
  444. package/src/components/LpChart.vue +336 -37
  445. package/src/components/LpCheckbox.vue +43 -9
  446. package/src/components/LpCodeBlock.vue +17 -23
  447. package/src/components/LpCollapse.vue +5 -4
  448. package/src/components/LpColorPicker.vue +8 -9
  449. package/src/components/LpCommandPalette.vue +133 -50
  450. package/src/components/LpContextMenu.vue +15 -13
  451. package/src/components/LpCountdown.vue +2 -1
  452. package/src/components/LpDataList.vue +13 -1
  453. package/src/components/LpDatePicker.vue +5 -17
  454. package/src/components/LpDateRangePicker.vue +5 -16
  455. package/src/components/LpDock.vue +25 -16
  456. package/src/components/LpDrawer.vue +70 -61
  457. package/src/components/LpDropdownMenu.vue +6 -10
  458. package/src/components/LpDropzone.vue +12 -7
  459. package/src/components/LpEditable.vue +6 -2
  460. package/src/components/LpFileTree.vue +507 -66
  461. package/src/components/LpFileTreeNode.vue +106 -5
  462. package/src/components/LpFormField.vue +1 -1
  463. package/src/components/LpFrame.vue +33 -13
  464. package/src/components/LpHoverCard.vue +30 -5
  465. package/src/components/LpIcon.vue +6 -2
  466. package/src/components/LpIdenticon.vue +2 -1
  467. package/src/components/LpImageEditor.vue +5 -2
  468. package/src/components/LpInput.vue +15 -23
  469. package/src/components/LpInspector.vue +197 -0
  470. package/src/components/LpLightbox.vue +74 -29
  471. package/src/components/LpLogInspector.vue +85 -0
  472. package/src/components/LpLogViewer.vue +239 -29
  473. package/src/components/LpMapPicker.vue +2 -1
  474. package/src/components/LpMasonry.vue +8 -5
  475. package/src/components/LpModal.vue +12 -12
  476. package/src/components/LpModalNav.vue +2 -21
  477. package/src/components/LpNumberField.vue +6 -15
  478. package/src/components/LpOtpInput.vue +2 -2
  479. package/src/components/LpPagination.vue +5 -3
  480. package/src/components/LpPhoneInput.vue +8 -19
  481. package/src/components/LpPie.vue +25 -23
  482. package/src/components/LpPipeline.vue +14 -1
  483. package/src/components/LpPopover.vue +23 -2
  484. package/src/components/LpProgress.vue +2 -1
  485. package/src/components/LpRadioGroup.vue +2 -2
  486. package/src/components/LpRangeSlider.vue +5 -6
  487. package/src/components/LpRevealOnScroll.vue +1 -1
  488. package/src/components/LpScrollArea.vue +28 -11
  489. package/src/components/LpSecretField.vue +2 -1
  490. package/src/components/LpSegmented.vue +6 -8
  491. package/src/components/LpSelect.vue +54 -24
  492. package/src/components/LpShaderBackdrop.vue +12 -9
  493. package/src/components/LpShift.vue +18 -15
  494. package/src/components/LpSidebar.vue +77 -74
  495. package/src/components/LpSidebarNav.vue +3 -2
  496. package/src/components/LpSidebarRow.vue +2 -1
  497. package/src/components/LpSlider.vue +5 -6
  498. package/src/components/LpSpotlight.vue +2 -1
  499. package/src/components/LpSwitch.vue +5 -3
  500. package/src/components/LpTable.vue +609 -93
  501. package/src/components/LpTableFilter.vue +1 -1
  502. package/src/components/LpTableOfContents.vue +16 -9
  503. package/src/components/LpTabs.vue +2 -1
  504. package/src/components/LpTagsInput.vue +12 -19
  505. package/src/components/LpTextarea.vue +5 -11
  506. package/src/components/LpThemeProvider.vue +29 -13
  507. package/src/components/LpTimeField.vue +4 -7
  508. package/src/components/LpToaster.vue +7 -6
  509. package/src/components/LpTooltip.vue +62 -18
  510. package/src/components/LpTopologyCanvas.vue +567 -42
  511. package/src/components/LpTour.vue +7 -5
  512. package/src/components/LpTruncate.vue +4 -3
  513. package/src/components/LpTypewriter.vue +2 -1
  514. package/src/components/LpVideoBackdrop.vue +11 -13
  515. package/src/components/LpWindow.vue +373 -0
  516. package/src/components/LpWorkspaceSwitcher.vue +116 -0
  517. package/src/components/PickerField.vue +2 -2
  518. package/src/components/SliderRail.vue +10 -0
  519. package/src/components/address.ts +3 -1
  520. package/src/components/appShell.ts +74 -0
  521. package/src/components/calendar.ts +12 -5
  522. package/src/components/chart.ts +27 -3
  523. package/src/components/chartWindow.ts +198 -0
  524. package/src/components/color.ts +2 -1
  525. package/src/components/commandPalette.ts +66 -5
  526. package/src/components/controlSize.ts +6 -0
  527. package/src/components/dropdown.ts +3 -0
  528. package/src/components/exportData.ts +33 -18
  529. package/src/components/fileTree.ts +215 -0
  530. package/src/components/identicon.ts +4 -2
  531. package/src/components/logTools.ts +298 -0
  532. package/src/components/meter.ts +2 -1
  533. package/src/components/pipeline.ts +6 -0
  534. package/src/components/relativeTime.ts +2 -1
  535. package/src/components/reveal.ts +3 -1
  536. package/src/components/select.ts +14 -0
  537. package/src/components/sidebar.ts +1 -0
  538. package/src/components/tableFeatures.ts +149 -0
  539. package/src/components/topologyGraph.ts +655 -0
  540. package/src/components/tour.ts +5 -3
  541. package/src/components/videoBackdrop.ts +7 -6
  542. package/src/composables/clamp.ts +2 -0
  543. package/src/composables/devWarn.ts +19 -0
  544. package/src/composables/easing.ts +37 -12
  545. package/src/composables/overlayHistory.ts +24 -0
  546. package/src/composables/pointerDrag.ts +54 -0
  547. package/src/composables/propCheck.ts +90 -0
  548. package/src/composables/realm.ts +25 -0
  549. package/src/composables/scroll.ts +4 -2
  550. package/src/composables/scrollMemory.ts +56 -14
  551. package/src/composables/spatialNav.ts +2 -1
  552. package/src/composables/textEntry.ts +33 -0
  553. package/src/composables/useAsyncTask.ts +6 -0
  554. package/src/composables/useChartSync.ts +52 -0
  555. package/src/composables/useCloseGuard.ts +22 -10
  556. package/src/composables/useCommands.ts +123 -0
  557. package/src/composables/useField.ts +41 -2
  558. package/src/composables/useFitScale.ts +4 -1
  559. package/src/composables/useFormDraft.ts +38 -14
  560. package/src/composables/useHintSystem.ts +2 -2
  561. package/src/composables/useHotkeys.ts +19 -48
  562. package/src/composables/useInView.ts +2 -1
  563. package/src/composables/useInputFilter.ts +8 -5
  564. package/src/composables/useInputMask.ts +8 -3
  565. package/src/composables/useLinkedView.ts +222 -0
  566. package/src/composables/useOptimisticToggle.ts +7 -1
  567. package/src/composables/useOverlayHistory.ts +20 -0
  568. package/src/composables/usePaintSelect.ts +20 -14
  569. package/src/composables/usePaletteCommands.ts +13 -0
  570. package/src/composables/usePersisted.ts +10 -0
  571. package/src/composables/usePillTransition.ts +9 -6
  572. package/src/composables/useResizeObserver.ts +2 -1
  573. package/src/composables/useReveal.ts +2 -1
  574. package/src/composables/useRovingFocus.ts +11 -14
  575. package/src/composables/useScramble.ts +12 -1
  576. package/src/composables/useShift.ts +8 -6
  577. package/src/composables/useTilt.ts +2 -1
  578. package/src/composables/useTween.ts +10 -5
  579. package/src/composables/useView.ts +17 -3
  580. package/src/composables/useZoomPan.ts +2 -5
  581. package/src/composables/windowing.ts +190 -0
  582. package/src/icons.ts +27 -6
  583. package/src/index.ts +41 -4
  584. package/src/kit-icons.ts +1 -1
  585. package/src/layout/tree.ts +31 -16
  586. package/src/layout/useLayout.ts +6 -0
  587. package/src/layout/useWorkspaces.ts +234 -0
  588. package/src/nuxt.ts +9 -1
  589. package/src/runtime/propCheck.plugin.ts +10 -0
  590. package/src/theme/studio.ts +7 -5
  591. package/src/theme/useTheme.ts +89 -15
  592. package/src/tokens/tokens.css +8 -1
  593. package/dist/assets/LpTabs-CJ0rYQbx.css +0 -18
  594. package/dist/chunks/LpAppShell-Dgejipzz.js +0 -319
  595. package/dist/chunks/LpFileTree-CooHinsC.js +0 -425
  596. package/dist/chunks/LpThemeProvider-8MG6yqU9.js +0 -640
  597. package/dist/chunks/LpTopologyCanvas-B1gIbS0n.js +0 -298
@@ -11,6 +11,7 @@ import { menuPaletteCommands } from "./commandPalette"
11
11
  import LpDataList from "./LpDataList.vue"
12
12
  import LpCheckbox from "./LpCheckbox.vue"
13
13
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
14
+ import LpEditable from "./LpEditable.vue"
14
15
  import LpEmptyState from "./LpEmptyState.vue"
15
16
  import LpBadge from "./LpBadge.vue"
16
17
  import LpIcon from "./LpIcon.vue"
@@ -29,7 +30,21 @@ import { usePaintSelect } from "../composables/usePaintSelect"
29
30
  import { useRovingFocus } from "../composables/useRovingFocus"
30
31
  import { useLabels } from "../composables/useLpConfig"
31
32
  import { useResizeObserver } from "../composables/useResizeObserver"
32
- import { compareValues } from "./tableSort"
33
+ import {
34
+ clampColumnWidth,
35
+ groupRows,
36
+ nextSorts,
37
+ orderPinned,
38
+ pinOffsets,
39
+ presetHidden,
40
+ rowWindow,
41
+ sortByKeys,
42
+ type ColumnPin,
43
+ type ColumnPreset,
44
+ } from "./tableFeatures"
45
+ import { devWarn } from "../composables/devWarn"
46
+ import { startPointerDrag } from "../composables/pointerDrag"
47
+ import { isHTMLElement, nodeWindow } from "../composables/useView"
33
48
  import LpButton from "./LpButton.vue"
34
49
  import { downloadFile, fileStem, toCsv, type CsvOptions } from "./exportData"
35
50
  import {
@@ -100,6 +115,41 @@ export interface TableColumn<Row> {
100
115
  * user's own show/hide applies as usual.
101
116
  */
102
117
  hideBelow?: number
118
+ /** Stick to this edge while the table scrolls sideways. Pinned columns move to that edge. */
119
+ pin?: ColumnPin
120
+ /** Bounds in px for `resizableColumns`. */
121
+ minWidth?: number
122
+ maxWidth?: number
123
+ /** Set false to keep this column out of `resizableColumns`. */
124
+ resizable?: boolean
125
+ /**
126
+ * The cell edits in place (LpEditable), and a save emits `cellEdit`. A cell
127
+ * slot for the column wins: it draws the cell itself. Values are edited as
128
+ * text; `parseEdit` turns the text back into the value the row holds.
129
+ */
130
+ editable?: boolean | ((row: Row) => boolean)
131
+ /** A message rejects the text and keeps the cell open. */
132
+ validateEdit?: (value: string, row: Row) => string | true | void
133
+ parseEdit?: (value: string, row: Row) => unknown
134
+ }
135
+
136
+ /** A saved inline edit. The table holds no rows of its own to change: apply it to yours. */
137
+ export interface TableCellEdit<Row> {
138
+ row: Row
139
+ key: string
140
+ value: unknown
141
+ previous: unknown
142
+ }
143
+
144
+ /**
145
+ * "Everything this query matches, except these": what a bulk action sends to
146
+ * the server, in place of ids the client never loaded. See `selectionToken`.
147
+ */
148
+ export interface TableSelectionToken {
149
+ sorts: SortState[]
150
+ hidden: Record<string, unknown[]>
151
+ excluded: (string | number)[]
152
+ total: number
103
153
  }
104
154
 
105
155
  export interface TableLabels {
@@ -137,6 +187,13 @@ export interface TableLabels {
137
187
  pageSelected: string
138
188
  selectAllMatching: string
139
189
  allMatchingSelected: string
190
+ columnPresets: string
191
+ resizeColumn: string
192
+ expandRow: string
193
+ collapseRow: string
194
+ groupCount: string
195
+ expandGroup: string
196
+ collapseGroup: string
140
197
  }
141
198
 
142
199
  export interface TableExportOptions extends CsvOptions {
@@ -165,6 +222,8 @@ export interface TableQuery {
165
222
  page: number
166
223
  pageSize: number
167
224
  sort: SortState | null
225
+ /** Every sort key in order, with `multiSort`. `sort` is the first of them. */
226
+ sorts?: SortState[]
168
227
  hidden: Record<string, unknown[]>
169
228
  }
170
229
 
@@ -288,8 +347,70 @@ const props = withDefaults(
288
347
  * With `load`: the user chose "select all N" beyond the loaded page
289
348
  * (v-model:allMatching). The keys in `selected` are then only the loaded
290
349
  * ones; acting on everything that matches is the consumer's to do.
350
+ *
351
+ * It holds only while every loaded row is ticked: a new page, sort or
352
+ * filter brings unticked rows and clears it, so a bulk action never runs
353
+ * on a set the user can no longer see.
291
354
  */
292
355
  allMatching?: boolean
356
+ /**
357
+ * Keep "select all N" across pages and sorts, and let the user untick rows
358
+ * out of it: the selection is then the query and its exceptions, emitted
359
+ * as `update:selectionToken` for a bulk action to send, with no need for
360
+ * ids the client never loaded. `selected` stays the loaded keys. A change
361
+ * of filter means a different set of rows, so it clears the selection.
362
+ */
363
+ selectionToken?: boolean
364
+ /** Sort by several columns: Shift-click adds a column (v-model:sorts). */
365
+ multiSort?: boolean
366
+ /** Sort keys in order (v-model:sorts), with `multiSort`. Left unset the table keeps them. */
367
+ sorts?: SortState[]
368
+ /**
369
+ * Drag a column's edge, or focus it and use the arrows, to set its width;
370
+ * double-click clears it. Sets a fixed table layout once a width is set.
371
+ */
372
+ resizableColumns?: boolean
373
+ /** Widths in px by column key (v-model:columnWidths). */
374
+ columnWidths?: Record<string, number>
375
+ /** Named sets of columns, offered in the table's menu. */
376
+ columnPresets?: readonly ColumnPreset[]
377
+ /** The chosen preset's id (v-model:columnPreset). */
378
+ columnPreset?: string | null
379
+ /**
380
+ * Group rows under a heading: a column key or a function. Client side, so
381
+ * with `load` it groups the loaded page. Groups follow the row order, so
382
+ * sort by the same column to keep them put.
383
+ */
384
+ groupBy?: string | ((row: T) => unknown)
385
+ groupLabel?: (value: unknown, rows: T[]) => string
386
+ /** Collapsed group ids, `String(value)` (v-model:collapsedGroups). */
387
+ collapsedGroups?: string[]
388
+ /**
389
+ * Rows open a panel beneath them, drawn by the `expanded` slot. A function
390
+ * says which rows can. Ignored with `virtualRowHeight`.
391
+ */
392
+ expandable?: boolean | ((row: T) => boolean)
393
+ /** Open row keys (v-model:expanded). */
394
+ expanded?: RowKey[]
395
+ /**
396
+ * Draw only the rows in view, for thousands of them, at this height in px.
397
+ * Give the table a `max-height`, or a limited parent: with room for every
398
+ * row, every row is in view and all of them are drawn.
399
+ *
400
+ * The price of drawing a window:
401
+ * - Every row, group heading included, must be exactly this tall: keep cells
402
+ * on one line (`nowrap`). A taller row drifts the scroll position.
403
+ * - Row enter, leave and move animations are off.
404
+ * - `expandable` is ignored: a panel has no fixed height.
405
+ * - Arrow keys and typeahead reach the rows drawn, not the whole list.
406
+ * - Sweeping ticks reaches the rows drawn; Ctrl+A and the header tick do
407
+ * cover all of them.
408
+ * - Find in page sees the rows drawn.
409
+ * `stickyHeader`, pins and the row menu work as usual.
410
+ */
411
+ virtualRowHeight?: number
412
+ /** Rows drawn beyond each edge of the view. */
413
+ virtualOverscan?: number
293
414
  }>(),
294
415
  { palette: undefined,
295
416
  rows: () => [],
@@ -307,6 +428,14 @@ const props = withDefaults(
307
428
  sort: undefined,
308
429
  filters: undefined,
309
430
  filtered: false,
431
+ sorts: undefined,
432
+ columnWidths: undefined,
433
+ columnPreset: undefined,
434
+ groupBy: undefined,
435
+ collapsedGroups: undefined,
436
+ expanded: undefined,
437
+ virtualRowHeight: undefined,
438
+ virtualOverscan: 6,
310
439
  },
311
440
  )
312
441
 
@@ -346,6 +475,13 @@ const labelSet = useLabels<TableLabels>(
346
475
  pageSelected: "All {count} on this page are selected.",
347
476
  selectAllMatching: "Select all {total}",
348
477
  allMatchingSelected: "All {total} are selected.",
478
+ columnPresets: "Column presets",
479
+ resizeColumn: "Resize {column}",
480
+ expandRow: "Expand row",
481
+ collapseRow: "Collapse row",
482
+ groupCount: "{label} ({count})",
483
+ expandGroup: "Expand {label}",
484
+ collapseGroup: "Collapse {label}",
349
485
  },
350
486
  () => props.labels,
351
487
  )
@@ -384,6 +520,13 @@ const emit = defineEmits<{
384
520
  /** The empty table's reset was pressed: clear the filters kept outside it. */
385
521
  (e: "resetFilters"): void
386
522
  (e: "update:allMatching", value: boolean): void
523
+ (e: "update:selectionToken", value: TableSelectionToken | null): void
524
+ (e: "update:sorts", value: SortState[]): void
525
+ (e: "update:columnWidths", value: Record<string, number>): void
526
+ (e: "update:columnPreset", value: string | null): void
527
+ (e: "update:collapsedGroups", value: string[]): void
528
+ (e: "update:expanded", value: RowKey[]): void
529
+ (e: "cellEdit", edit: TableCellEdit<T>): void
387
530
  (e: "rowClick", row: T): void
388
531
  }>()
389
532
 
@@ -435,10 +578,11 @@ function onRowContextMenu(row: T) {
435
578
  */
436
579
  function openRowMenu(event: Event) {
437
580
  const button = event.currentTarget
438
- if (!(button instanceof HTMLElement)) return
581
+ if (!isHTMLElement(button)) return
439
582
  const rect = button.getBoundingClientRect()
583
+ const win = nodeWindow(button)
440
584
  button.dispatchEvent(
441
- new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left, clientY: rect.bottom }),
585
+ new win.MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left, clientY: rect.bottom }),
442
586
  )
443
587
  }
444
588
 
@@ -478,7 +622,16 @@ const contextMenu = computed<ContextMenuItemDef[]>(() => {
478
622
  // emitted an event nobody was listening for, and left the rows exactly as they
479
623
  // were: a control that answers a click by doing nothing.
480
624
  const ownSort = ref<SortState | null>(null)
481
- const activeSort = computed(() => (props.sort !== undefined ? props.sort : ownSort.value))
625
+ const ownSorts = ref<SortState[]>([])
626
+ const activeSort = computed(() => {
627
+ if (props.multiSort) return (props.sorts ?? ownSorts.value)[0] ?? null
628
+ return props.sort !== undefined ? props.sort : ownSort.value
629
+ })
630
+ const sortList = computed<SortState[]>(() => {
631
+ if (props.multiSort) return props.sorts ?? ownSorts.value
632
+ return activeSort.value ? [activeSort.value] : []
633
+ })
634
+ const sortOf = (col: TableColumn<T>) => sortList.value.find((sort) => sort.key === col.key)
482
635
 
483
636
  // ── server-backed rows ───────────────────────────────────────
484
637
  const loaded = shallowRef<T[]>([])
@@ -492,6 +645,14 @@ const busy = computed(() => props.loading || task.pending.value)
492
645
  const sourceRows = computed<T[]>(() => (props.load ? loaded.value : props.rows))
493
646
  const sourceIndex = computed(() => new Map(sourceRows.value.map((row, i) => [row, i])))
494
647
 
648
+ // New objects drop what position-keyed selection pointed at, without a word.
649
+ let warnedRowKey = false
650
+ watch(sourceRows, () => {
651
+ if (warnedRowKey || props.rowKey || !props.selected.length) return
652
+ warnedRowKey = true
653
+ devWarn("LpTable: rows were replaced while some are selected, and without `rowKey` the selection now points at positions, not rows. Set `rowKey`.")
654
+ })
655
+
495
656
  // The tallest page shown so far, held as a floor: the last page is usually
496
657
  // short, and without it the pager below jumps up the moment you reach it.
497
658
  /*
@@ -547,12 +708,14 @@ function writeHidden(next: Record<string, Set<unknown>>) {
547
708
  * out beside the new one, and the table would show both lists for a moment.
548
709
  */
549
710
  const animating = ref(false)
711
+ /* Longer than any row transition: a fallback off switch, not a measure of one. */
712
+ const MOTION_WINDOW_MS = 600
550
713
  let motionTimer: ReturnType<typeof setTimeout> | undefined
551
714
  function withMotion(change: () => void) {
552
715
  if (!props.load) {
553
716
  animating.value = true
554
717
  if (motionTimer) clearTimeout(motionTimer)
555
- motionTimer = setTimeout(() => (animating.value = false), 600)
718
+ motionTimer = setTimeout(() => (animating.value = false), MOTION_WINDOW_MS)
556
719
  }
557
720
  change()
558
721
  }
@@ -576,6 +739,113 @@ const visibleColumns = computed(() =>
576
739
  ),
577
740
  )
578
741
 
742
+ // Pinned columns at their edges; with no pins, the columns as given.
743
+ const shownColumns = computed(() => orderPinned(visibleColumns.value))
744
+
745
+ // ── column widths and pins ───────────────────────────────────
746
+ const RESIZE_STEP_PX = 16
747
+ /* Room counted for a column with no width of its own, once the layout is fixed. */
748
+ const UNSET_COLUMN_PX = 120
749
+ const TICK_COLUMN_WIDTH = "3.25rem"
750
+ const EXPAND_COLUMN_WIDTH = "2.5rem"
751
+
752
+ const ownWidths = ref<Record<string, number>>({})
753
+ const widths = computed(() => props.columnWidths ?? ownWidths.value)
754
+ const fixedLayout = computed(() => !!props.resizableColumns && Object.keys(widths.value).length > 0)
755
+ const tableStyle = computed(() => {
756
+ if (!fixedLayout.value) return undefined
757
+ const sum = shownColumns.value.reduce((room, col) => room + (widths.value[col.key] ?? UNSET_COLUMN_PX), 0)
758
+ return { minWidth: `${sum}px` }
759
+ })
760
+
761
+ function writeWidths(next: Record<string, number>) {
762
+ ownWidths.value = next
763
+ emit("update:columnWidths", next)
764
+ }
765
+
766
+ const canResize = (col: TableColumn<T>) => !!props.resizableColumns && col.resizable !== false && !!col.label
767
+ /* Marks the header cells something measures, and only those. */
768
+ const measuredCol = (col: TableColumn<T>) => (props.resizableColumns || col.pin ? col.key : undefined)
769
+
770
+ function headerWidths(): Record<string, number> {
771
+ const measured: Record<string, number> = {}
772
+ for (const cell of frameEl.value?.querySelectorAll<HTMLElement>("th[data-lp-col]") ?? []) {
773
+ measured[cell.dataset.lpCol!] = cell.offsetWidth
774
+ }
775
+ return measured
776
+ }
777
+
778
+ function setWidth(col: TableColumn<T>, width: number) {
779
+ writeWidths({ ...widths.value, [col.key]: clampColumnWidth(width, col.minWidth, col.maxWidth) })
780
+ }
781
+
782
+ function clearWidth(col: TableColumn<T>) {
783
+ const { [col.key]: _gone, ...rest } = widths.value
784
+ writeWidths(rest)
785
+ }
786
+
787
+ /*
788
+ * The first drag fixes the layout, which would hand the free room out anew, so
789
+ * every column starts from the width it has on screen.
790
+ */
791
+ function startResize(event: PointerEvent, col: TableColumn<T>) {
792
+ const handle = event.currentTarget
793
+ if (!isHTMLElement(handle) || event.button !== 0) return
794
+ event.stopPropagation()
795
+ const measured = headerWidths()
796
+ if (!fixedLayout.value) writeWidths(measured)
797
+ const from = measured[col.key] ?? handle.parentElement?.offsetWidth ?? 0
798
+ const startX = event.clientX
799
+ startPointerDrag(event, { move: (e) => setWidth(col, from + e.clientX - startX), end: () => {} })
800
+ }
801
+
802
+ function onResizeKey(event: KeyboardEvent, col: TableColumn<T>) {
803
+ const step = event.key === "ArrowRight" ? RESIZE_STEP_PX : event.key === "ArrowLeft" ? -RESIZE_STEP_PX : 0
804
+ if (!step) return
805
+ event.preventDefault()
806
+ event.stopPropagation()
807
+ const measured = headerWidths()
808
+ if (!fixedLayout.value) writeWidths(measured)
809
+ setWidth(col, (widths.value[col.key] ?? measured[col.key] ?? UNSET_COLUMN_PX) + step)
810
+ }
811
+
812
+ const pinWidths = ref<Record<string, number>>({})
813
+ const pinOffset = computed(() => pinOffsets(shownColumns.value, pinWidths.value))
814
+ function measurePins() {
815
+ if (shownColumns.value.some((col) => col.pin)) pinWidths.value = headerWidths()
816
+ }
817
+ watch([shownColumns, widths, tableWidth], measurePins, { flush: "post" })
818
+ onMounted(measurePins)
819
+
820
+ const PIN_HEAD = "sticky z-[11] bg-surface-soft"
821
+ const PIN_CELL = "sticky z-[1] bg-surface"
822
+ const pinStyle = (col: TableColumn<T>) => (col.pin ? { [col.pin]: `${pinOffset.value[col.key] ?? 0}px` } : undefined)
823
+
824
+ function headStyle(col: TableColumn<T>) {
825
+ const width = (props.resizableColumns ? widths.value[col.key] : undefined)
826
+ const size = width != null ? `${width}px` : col.width
827
+ if (!size && !col.pin) return undefined
828
+ return { ...(size ? { width: size } : {}), ...pinStyle(col) }
829
+ }
830
+
831
+ // ── column presets ───────────────────────────────────────────
832
+ const ownPreset = ref<string | null>(null)
833
+ const activePreset = computed(() => (props.columnPreset !== undefined ? props.columnPreset : ownPreset.value))
834
+
835
+ function showPreset(id: string | null | undefined) {
836
+ const preset = props.columnPresets?.find((p) => p.id === id)
837
+ if (!preset) return
838
+ hiddenColumns.value = presetHidden(preset, props.columns)
839
+ if (preset.widths && props.resizableColumns) writeWidths({ ...widths.value, ...preset.widths })
840
+ }
841
+
842
+ function choosePreset(id: string | null) {
843
+ ownPreset.value = id
844
+ emit("update:columnPreset", id)
845
+ showPreset(id)
846
+ }
847
+ watch(() => props.columnPreset, showPreset, { immediate: true })
848
+
579
849
  const facetColumns = computed(() => props.columns.filter((col) => col.filterable).map((col) => col.key))
580
850
  const localFacets = useTableFacets({
581
851
  rows: () => props.load ? [] : props.rows,
@@ -665,6 +935,11 @@ function toggleColumn(key: string) {
665
935
  if (next.has(key)) next.delete(key)
666
936
  else next.add(key)
667
937
  hiddenColumns.value = next
938
+ // The set on screen is no longer the preset's.
939
+ if (activePreset.value != null) {
940
+ ownPreset.value = null
941
+ emit("update:columnPreset", null)
942
+ }
668
943
  }
669
944
 
670
945
  /** Rows left once the per-column value filters have been applied. */
@@ -677,42 +952,43 @@ const filteredRows = computed<T[]>(() => {
677
952
  )
678
953
  })
679
954
 
680
- // We compare by raw cell value with a stable-ish coerce.
681
955
  function sortRows(rows: T[]): T[] {
682
- const s = activeSort.value
683
- if (!s || props.load) return rows
684
- const col = props.columns.find((c) => c.key === s.key)
685
- if (!col?.sortable) return rows
686
- const factor = s.dir === "asc" ? 1 : -1
687
- const own = col.compare
688
- const value = col.sortValue ?? ((row: T) => row[s.key])
689
- return [...rows].sort((a, b) => (own ? own(a, b) : compareValues(value(a), value(b))) * factor)
956
+ if (!sortList.value.length || props.load) return rows
957
+ return sortByKeys(rows, sortList.value, props.columns)
690
958
  }
691
959
 
692
960
  const displayRows = computed<T[]>(() => sortRows(filteredRows.value))
693
961
 
694
- function toggleSort(col: TableColumn<T>) {
962
+ function writeSorts(next: SortState[]) {
963
+ withMotion(() => {
964
+ ownSorts.value = next
965
+ ownSort.value = next[0] ?? null
966
+ if (props.multiSort) emit("update:sorts", next)
967
+ emit("update:sort", next[0] ?? null)
968
+ })
969
+ }
970
+
971
+ function toggleSort(col: TableColumn<T>, event?: MouseEvent) {
695
972
  if (!col.sortable) return
696
- const s = activeSort.value
697
973
  // Cycle: none → asc → desc → none, scoped to the clicked column.
698
- let next: SortState | null
699
- if (!s || s.key !== col.key) next = { key: col.key, dir: "asc" }
700
- else if (s.dir === "asc") next = { key: col.key, dir: "desc" }
701
- else next = null
702
974
  // Kept locally as well as emitted: a parent that binds v-model overwrites
703
975
  // this on the way back, and one that does not still gets a table that sorts.
704
- withMotion(() => {
705
- ownSort.value = next
706
- emit("update:sort", next)
707
- })
976
+ writeSorts(nextSorts(sortList.value, col.key, !!(props.multiSort && event?.shiftKey)))
708
977
  }
709
978
 
710
979
  function sortIcon(col: TableColumn<T>): string {
711
- const s = activeSort.value
712
- if (s?.key !== col.key) return "lucide:chevrons-up-down"
980
+ const s = sortOf(col)
981
+ if (!s) return "lucide:chevrons-up-down"
713
982
  return s.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down"
714
983
  }
715
984
 
985
+ /* The key's place in a sort by several columns; nothing to say with just one. */
986
+ function sortPosition(col: TableColumn<T>): number | undefined {
987
+ if (sortList.value.length < 2) return undefined
988
+ const at = sortList.value.findIndex((sort) => sort.key === col.key)
989
+ return at < 0 ? undefined : at + 1
990
+ }
991
+
716
992
  /**
717
993
  * What right-clicking a header offers: the directions by name.
718
994
  *
@@ -727,11 +1003,16 @@ function sortIcon(col: TableColumn<T>): string {
727
1003
  // back before the popover can take it, or the popover reads that focus move as
728
1004
  // an outside interaction and dismisses itself. So the trigger is clicked once
729
1005
  // the menu is gone.
1006
+ const MENU_CLOSE_LIMIT_MS = 600
1007
+ const FOCUS_RETURN_MS = 30
730
1008
  function openFilter(key: string) {
731
1009
  const open = () => frameEl.value?.querySelector<HTMLElement>(`[data-lp-filter="${CSS.escape(key)}"]`)?.click()
732
1010
  const started = performance.now()
1011
+ // The menu this was picked from, in the table's own document; a menu that
1012
+ // opens elsewhere in the meantime is no reason to wait.
1013
+ const closing = [...(frameEl.value?.ownerDocument ?? document).querySelectorAll("[data-reka-menu-content]")]
733
1014
  const wait = () => {
734
- if (!document.querySelector("[data-reka-menu-content]") || performance.now() - started > 600) setTimeout(open, 30)
1015
+ if (!closing.some((menu) => menu.isConnected) || performance.now() - started > MENU_CLOSE_LIMIT_MS) setTimeout(open, FOCUS_RETURN_MS)
735
1016
  else requestAnimationFrame(wait)
736
1017
  }
737
1018
  requestAnimationFrame(wait)
@@ -739,11 +1020,7 @@ function openFilter(key: string) {
739
1020
 
740
1021
  function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
741
1022
  const s = activeSort.value
742
- const set = (next: SortState | null) => () =>
743
- withMotion(() => {
744
- ownSort.value = next
745
- emit("update:sort", next)
746
- })
1023
+ const set = (next: SortState | null) => () => writeSorts(next ? [next] : [])
747
1024
 
748
1025
  const items: ContextMenuItemDef[] = []
749
1026
 
@@ -806,8 +1083,20 @@ function tableMenu(): ContextMenuItemDef[] {
806
1083
  ? [{ label: text.value.exportCsv, icon: "lucide:download", onSelect: () => exportCsv() }]
807
1084
  : []
808
1085
  if (!offered.length) return exportItem
1086
+ const presets: ContextMenuItemDef[] = props.columnPresets?.length
1087
+ ? [{
1088
+ label: text.value.columnPresets,
1089
+ icon: "lucide:layout-template",
1090
+ children: props.columnPresets.map((preset) => ({
1091
+ label: preset.label,
1092
+ checked: activePreset.value === preset.id,
1093
+ onSelect: () => choosePreset(preset.id),
1094
+ })),
1095
+ }]
1096
+ : []
809
1097
  return [
810
1098
  ...exportItem,
1099
+ ...presets,
811
1100
  {
812
1101
  label: text.value.columns,
813
1102
  icon: "lucide:columns-3",
@@ -833,6 +1122,7 @@ function fetchPage(delay = 0) {
833
1122
  .map(([key, set]) => [key, [...set]]),
834
1123
  )
835
1124
  const query: TableQuery = { page: page.value, pageSize: props.pageSize, sort: activeSort.value, hidden }
1125
+ if (props.multiSort) query.sorts = sortList.value
836
1126
  task.run((signal) => load(query, signal), {
837
1127
  delay,
838
1128
  apply: (result) => {
@@ -858,7 +1148,7 @@ function fetchPage(delay = 0) {
858
1148
  // A new sort, filter or page size starts from the first page: page 4 of a
859
1149
  // different question, or of a different cut of the rows, is a page of nothing
860
1150
  // in particular.
861
- watch([activeSort, hiddenValues, () => props.pageSize], () => {
1151
+ watch([sortList, hiddenValues, () => props.pageSize], () => {
862
1152
  if (!props.load) return
863
1153
  if (page.value !== 1) page.value = 1
864
1154
  else fetchPage(props.debounce)
@@ -878,6 +1168,7 @@ onMounted(() => {
878
1168
  if (state) {
879
1169
  if (state.sort !== undefined) {
880
1170
  ownSort.value = state.sort
1171
+ ownSorts.value = state.sort ? [state.sort] : []
881
1172
  emit("update:sort", state.sort)
882
1173
  }
883
1174
  if (state.hidden) writeHidden(mergeRestored(hiddenValues.value, state.hidden))
@@ -916,7 +1207,7 @@ function exportCsv(options: TableExportOptions = {}): string {
916
1207
  }))
917
1208
  const csv = toCsv(rows, columns, options)
918
1209
  if (options.download !== false) {
919
- downloadFile(`${fileStem(options.filename ?? props.exportFilename, "table")}.csv`, csv, "text/csv;charset=utf-8")
1210
+ downloadFile(`${fileStem(options.filename ?? props.exportFilename, "table")}.csv`, csv, "text/csv;charset=utf-8", tableEl.value?.ownerDocument)
920
1211
  }
921
1212
  return csv
922
1213
  }
@@ -925,16 +1216,28 @@ defineExpose({ reload: () => fetchPage(), exportCsv })
925
1216
 
926
1217
  // ── selection ────────────────────────────────────────────────
927
1218
  const selectedSet = computed(() => new Set(props.selected))
1219
+ /*
1220
+ * With `selectionToken`, "all N" is the query and its exceptions rather than
1221
+ * keys: the rows the client never loaded have no key to hold.
1222
+ */
1223
+ const excluded = ref<Set<RowKey>>(new Set())
1224
+ const tokenActive = computed(() => !!props.selectionToken && !!props.load && props.allMatching)
1225
+ const isSelected = (key: RowKey) => (tokenActive.value ? !excluded.value.has(key) : selectedSet.value.has(key))
928
1226
  // "All" is what the user can see: rows a header filter hides are left alone.
929
1227
  const allKeys = computed(() => filteredRows.value.map((r, i) => keyFor(r, i)))
930
1228
  const allChecked = computed(
931
- () => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k)),
1229
+ () => allKeys.value.length > 0 && allKeys.value.every(isSelected),
932
1230
  )
933
1231
  const someChecked = computed(
934
- () => !allChecked.value && allKeys.value.some((k) => selectedSet.value.has(k)),
1232
+ () => !allChecked.value && allKeys.value.some(isSelected),
935
1233
  )
936
1234
 
937
1235
  function toggleAll(checked: boolean) {
1236
+ if (tokenActive.value) {
1237
+ if (checked) excluded.value = new Set()
1238
+ else clearSelection()
1239
+ return
1240
+ }
938
1241
  const next = new Set(selectedSet.value)
939
1242
  for (const key of allKeys.value) {
940
1243
  if (checked) next.add(key)
@@ -944,6 +1247,14 @@ function toggleAll(checked: boolean) {
944
1247
  }
945
1248
 
946
1249
  function toggleRow(key: RowKey, checked: boolean) {
1250
+ if (tokenActive.value) {
1251
+ const out = new Set(excluded.value)
1252
+ if (checked) out.delete(key)
1253
+ else out.add(key)
1254
+ if (out.size >= total.value) clearSelection()
1255
+ else excluded.value = out
1256
+ return
1257
+ }
947
1258
  const next = new Set(selectedSet.value)
948
1259
  if (checked) next.add(key)
949
1260
  else next.delete(key)
@@ -957,7 +1268,7 @@ const keyByText = computed(
957
1268
  )
958
1269
  const paint = usePaintSelect<RowKey>({
959
1270
  root: frameEl,
960
- enabled: () => !!props.selectable,
1271
+ enabled: () => !!props.selectable && !tokenActive.value,
961
1272
  selection: () => props.selected,
962
1273
  set: (keys) => emit("update:selected", keys),
963
1274
  resolve: (raw) => keyByText.value.get(raw),
@@ -978,8 +1289,29 @@ const ticksShown = computed(
978
1289
  function clearSelection() {
979
1290
  emit("update:selected", [])
980
1291
  if (props.allMatching) emit("update:allMatching", false)
1292
+ excluded.value = new Set()
981
1293
  }
982
1294
 
1295
+ watch([tokenActive, sortList, excluded, total], () => {
1296
+ if (!tokenActive.value) {
1297
+ if (excluded.value.size) excluded.value = new Set()
1298
+ emit("update:selectionToken", null)
1299
+ return
1300
+ }
1301
+ emit("update:selectionToken", {
1302
+ sorts: sortList.value,
1303
+ hidden: Object.fromEntries(
1304
+ Object.entries(hiddenValues.value).filter(([, set]) => set.size).map(([key, set]) => [key, [...set]]),
1305
+ ),
1306
+ excluded: [...excluded.value],
1307
+ total: total.value,
1308
+ })
1309
+ })
1310
+ // Another filter is another set of rows: what was selected no longer describes it.
1311
+ watch(hiddenValues, () => {
1312
+ if (tokenActive.value) clearSelection()
1313
+ })
1314
+
983
1315
  /*
984
1316
  * A long press on a row starts selection in hold mode. Presses that begin on
985
1317
  * something the row holds that answers clicks of its own (a link, a button, a
@@ -995,7 +1327,7 @@ function onRowPointerDown(event: PointerEvent, key: RowKey) {
995
1327
  // Once something is selected, a click on a row adds or removes it, as in a
996
1328
  // phone's selection mode; with nothing selected it is the row's click again.
997
1329
  function onRowClick(row: T, key: RowKey) {
998
- if (holdMode.value && props.selected.length) toggleRow(key, !selectedSet.value.has(key))
1330
+ if (holdMode.value && props.selected.length) toggleRow(key, !isSelected(key))
999
1331
  else emit("rowClick", row)
1000
1332
  }
1001
1333
 
@@ -1021,7 +1353,7 @@ const pageAllSelected = computed(
1021
1353
  // "Select all" beyond the page is a separate, explicit step: the header tick
1022
1354
  // only ever means the rows the user can see.
1023
1355
  watch(pageAllSelected, (all) => {
1024
- if (!all && props.allMatching) emit("update:allMatching", false)
1356
+ if (!all && props.allMatching && !props.selectionToken) emit("update:allMatching", false)
1025
1357
  })
1026
1358
  /*
1027
1359
  * One status line that rolls between its states ("60 rows", "3 selected",
@@ -1030,6 +1362,9 @@ watch(pageAllSelected, (all) => {
1030
1362
  */
1031
1363
  const statusText = computed(() => {
1032
1364
  if (!(props.selectable && props.selected.length)) return countText.value
1365
+ if (tokenActive.value && excluded.value.size) {
1366
+ return text.value.selectedCount.replace("{count}", String(total.value - excluded.value.size))
1367
+ }
1033
1368
  return props.allMatching
1034
1369
  ? text.value.allMatchingSelected.replace("{total}", String(total.value))
1035
1370
  : text.value.selectedCount.replace("{count}", String(props.selected.length))
@@ -1070,7 +1405,102 @@ function skeletonWidth(row: number, col: number): string {
1070
1405
  return `${SKELETON_WIDTHS[(row * 3 + col * 2) % SKELETON_WIDTHS.length]}%`
1071
1406
  }
1072
1407
 
1073
- const colSpan = computed(() => visibleColumns.value.length + (props.selectable ? 1 : 0))
1408
+ // ── expansion, groups and the rows drawn ─────────────────────
1409
+ const VIEWPORT = "[data-reka-scroll-area-viewport]"
1410
+ const expansionOn = computed(() => !!props.expandable && !props.virtualRowHeight)
1411
+ const ownExpanded = ref<RowKey[]>([])
1412
+ const expandedKeys = computed(() => new Set(props.expanded ?? ownExpanded.value))
1413
+ const canExpand = (row: T) => (typeof props.expandable === "function" ? props.expandable(row) : !!props.expandable)
1414
+
1415
+ function toggleExpanded(key: RowKey) {
1416
+ const next = new Set(expandedKeys.value)
1417
+ if (!next.delete(key)) next.add(key)
1418
+ ownExpanded.value = [...next]
1419
+ emit("update:expanded", ownExpanded.value)
1420
+ }
1421
+
1422
+ const ownCollapsed = ref<string[]>([])
1423
+ const collapsedSet = computed(() => new Set(props.collapsedGroups ?? ownCollapsed.value))
1424
+
1425
+ function toggleGroup(id: string) {
1426
+ const next = new Set(collapsedSet.value)
1427
+ if (!next.delete(id)) next.add(id)
1428
+ ownCollapsed.value = [...next]
1429
+ emit("update:collapsedGroups", ownCollapsed.value)
1430
+ }
1431
+
1432
+ type ViewEntry =
1433
+ | { kind: "row"; id: string; row: T; index: number }
1434
+ | { kind: "detail"; id: string; row: T; index: number }
1435
+ | { kind: "group"; id: string; group: string; value: unknown; rows: T[]; label: string; collapsed: boolean }
1436
+
1437
+ function groupValue(row: T): unknown {
1438
+ return typeof props.groupBy === "function" ? props.groupBy(row) : row[props.groupBy as keyof T]
1439
+ }
1440
+
1441
+ const allEntries = computed<ViewEntry[]>(() => {
1442
+ const rows = displayRows.value
1443
+ const indexes = props.groupBy !== undefined ? new Map(rows.map((row, i) => [row, i])) : null
1444
+ const entries: ViewEntry[] = []
1445
+ const pushRow = (row: T, index: number) => {
1446
+ const key = keyFor(row, index)
1447
+ entries.push({ kind: "row", id: String(key), row, index })
1448
+ if (expansionOn.value && expandedKeys.value.has(key) && canExpand(row)) {
1449
+ entries.push({ kind: "detail", id: `lp-detail:${key}`, row, index })
1450
+ }
1451
+ }
1452
+ if (!indexes) {
1453
+ rows.forEach((row, index) => pushRow(row, index))
1454
+ return entries
1455
+ }
1456
+ for (const group of groupRows(rows, groupValue)) {
1457
+ const collapsed = collapsedSet.value.has(group.id)
1458
+ const name = props.groupLabel?.(group.value, group.rows) ?? group.id
1459
+ const label = text.value.groupCount.replace("{label}", name).replace("{count}", String(group.rows.length))
1460
+ entries.push({ kind: "group", id: `lp-group:${group.id}`, group: group.id, value: group.value, rows: group.rows, label, collapsed })
1461
+ if (!collapsed) for (const row of group.rows) pushRow(row, indexes.get(row) ?? 0)
1462
+ }
1463
+ return entries
1464
+ })
1465
+
1466
+ const viewportEl = shallowRef<HTMLElement | null>(null)
1467
+ const scrollTop = ref(0)
1468
+ const viewportHeight = ref(0)
1469
+ const onViewportScroll = () => (scrollTop.value = viewportEl.value?.scrollTop ?? 0)
1470
+ onMounted(() => {
1471
+ if (props.virtualRowHeight) viewportEl.value = frameEl.value?.querySelector<HTMLElement>(VIEWPORT) ?? null
1472
+ })
1473
+ watch(viewportEl, (el, _, onCleanup) => {
1474
+ if (!el) return
1475
+ el.addEventListener("scroll", onViewportScroll, { passive: true })
1476
+ onCleanup(() => el.removeEventListener("scroll", onViewportScroll))
1477
+ })
1478
+ useResizeObserver(viewportEl, (_, el) => (viewportHeight.value = el.clientHeight), { immediate: true })
1479
+
1480
+ const rowWindowed = computed(() =>
1481
+ props.virtualRowHeight
1482
+ ? rowWindow(scrollTop.value, viewportHeight.value, props.virtualRowHeight, allEntries.value.length, props.virtualOverscan)
1483
+ : null,
1484
+ )
1485
+ const viewEntries = computed(() =>
1486
+ rowWindowed.value ? allEntries.value.slice(rowWindowed.value.start, rowWindowed.value.end) : allEntries.value,
1487
+ )
1488
+ /* The row's place among all of them, for assistive tech: the header is row 1. */
1489
+ const rowOffset = (entry: ViewEntry) => (rowWindowed.value?.start ?? 0) + viewEntries.value.indexOf(entry) + 2
1490
+ const rowStyle = computed(() => (props.virtualRowHeight ? { height: `${props.virtualRowHeight}px` } : undefined))
1491
+
1492
+ function isEditable(col: TableColumn<T>, row: T): boolean {
1493
+ return typeof col.editable === "function" ? col.editable(row) : !!col.editable
1494
+ }
1495
+
1496
+ function saveCell(col: TableColumn<T>, row: T, value: string) {
1497
+ const parsed = col.parseEdit ? col.parseEdit(value, row) : value
1498
+ emit("cellEdit", { row, key: col.key, value: parsed, previous: row[col.key] })
1499
+ }
1500
+
1501
+ const colSpan = computed(
1502
+ () => visibleColumns.value.length + (props.selectable ? 1 : 0) + (expansionOn.value ? 1 : 0),
1503
+ )
1074
1504
 
1075
1505
  /*
1076
1506
  * Keyboard navigation over the rows. A table is one Tab stop, not one per row:
@@ -1082,10 +1512,9 @@ const colSpan = computed(() => visibleColumns.value.length + (props.selectable ?
1082
1512
  * cursor survives a re-sort.
1083
1513
  */
1084
1514
  const rovingItems = computed(() =>
1085
- displayRows.value.map((row, index) => ({
1086
- id: String(keyFor(row, index)),
1087
- label: String(row[props.columns[0]?.key ?? ""] ?? ""),
1088
- })),
1515
+ viewEntries.value.flatMap((entry) =>
1516
+ entry.kind === "row" ? [{ id: entry.id, label: String(entry.row[props.columns[0]?.key ?? ""] ?? "") }] : [],
1517
+ ),
1089
1518
  )
1090
1519
 
1091
1520
  const rowByKey = computed(() => {
@@ -1113,7 +1542,7 @@ function onRowKeydown(event: KeyboardEvent) {
1113
1542
  const row = rowByKey.value.get(key)
1114
1543
  if (row) {
1115
1544
  const rowKey = keyFor(row, displayRows.value.indexOf(row))
1116
- toggleRow(rowKey, !selectedSet.value.has(rowKey))
1545
+ toggleRow(rowKey, !isSelected(rowKey))
1117
1546
  }
1118
1547
  return
1119
1548
  }
@@ -1283,7 +1712,13 @@ usePaletteCommands(() => {
1283
1712
  :title-icon="menuHead?.icon"
1284
1713
  :title-image="menuHead?.image"
1285
1714
  >
1286
- <table ref="tableEl" class="w-full border-collapse text-sm">
1715
+ <table
1716
+ ref="tableEl"
1717
+ class="w-full border-collapse text-sm"
1718
+ :class="fixedLayout ? 'table-fixed' : ''"
1719
+ :style="tableStyle"
1720
+ :aria-rowcount="virtualRowHeight ? allEntries.length + 1 : undefined"
1721
+ >
1287
1722
  <!-- The background belongs on `thead` itself, not only on its row:
1288
1723
  `border-collapse` collapses the borders, and with a sticky header the
1289
1724
  rows show through that seam — a band of body text rides across the
@@ -1293,7 +1728,13 @@ usePaletteCommands(() => {
1293
1728
  :class="stickyHeader ? 'sticky top-0 z-10 bg-surface-soft' : ''"
1294
1729
  >
1295
1730
  <tr class="border-b border-line bg-surface-soft">
1296
- <th v-if="selectable" class="w-px p-0" :class="cellY">
1731
+ <th v-if="expansionOn" class="w-px p-0" :style="fixedLayout ? { width: EXPAND_COLUMN_WIDTH } : undefined" :class="cellY" />
1732
+ <th
1733
+ v-if="selectable"
1734
+ class="w-px p-0"
1735
+ :class="cellY"
1736
+ :style="fixedLayout ? { width: ticksShown ? TICK_COLUMN_WIDTH : '0' } : undefined"
1737
+ >
1297
1738
  <div class="lp-tick lp-tick-open" :class="ticksShown ? '' : 'lp-tick-hidden'" :inert="!ticksShown">
1298
1739
  <LpCheckbox
1299
1740
  :model-value="allChecked"
@@ -1303,14 +1744,15 @@ usePaletteCommands(() => {
1303
1744
  </div>
1304
1745
  </th>
1305
1746
  <th
1306
- v-for="col in visibleColumns"
1747
+ v-for="col in shownColumns"
1307
1748
  :key="col.key"
1308
- :style="col.width ? { width: col.width } : undefined"
1749
+ :style="headStyle(col)"
1309
1750
  class="px-4 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
1310
- :class="[alignClass(col.align), cellY]"
1751
+ :class="[alignClass(col.align), cellY, col.pin ? PIN_HEAD : '', canResize(col) && !col.pin ? 'relative' : '']"
1752
+ :data-lp-col="measuredCol(col)"
1311
1753
  :aria-sort="
1312
- activeSort?.key === col.key
1313
- ? activeSort.dir === 'asc'
1754
+ sortOf(col)
1755
+ ? sortOf(col)!.dir === 'asc'
1314
1756
  ? 'ascending'
1315
1757
  : 'descending'
1316
1758
  : undefined
@@ -1329,9 +1771,9 @@ usePaletteCommands(() => {
1329
1771
  class="group/sort -mx-1 inline-flex items-center gap-1 rounded-small px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
1330
1772
  :class="[
1331
1773
  col.align === 'right' ? 'flex-row-reverse' : '',
1332
- activeSort?.key === col.key ? 'text-ink' : '',
1774
+ sortOf(col) ? 'text-ink' : '',
1333
1775
  ]"
1334
- @click="toggleSort(col)"
1776
+ @click="toggleSort(col, $event)"
1335
1777
  >
1336
1778
  {{ col.label }}
1337
1779
  <LpIcon
@@ -1340,11 +1782,12 @@ usePaletteCommands(() => {
1340
1782
  :size="13"
1341
1783
  class="shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]"
1342
1784
  :class="
1343
- activeSort?.key === col.key
1785
+ sortOf(col)
1344
1786
  ? 'opacity-100'
1345
1787
  : 'opacity-40 group-hover/sort:opacity-70'
1346
1788
  "
1347
1789
  />
1790
+ <span v-if="sortPosition(col)" class="tabular-nums text-brand">{{ sortPosition(col) }}</span>
1348
1791
  </button>
1349
1792
  </LpContextMenu>
1350
1793
  <template v-else>{{ col.label }}</template>
@@ -1372,6 +1815,20 @@ usePaletteCommands(() => {
1372
1815
  @update:hidden="(next) => setHidden(col.key, next)"
1373
1816
  />
1374
1817
  </LpPopover>
1818
+ <span
1819
+ v-if="canResize(col)"
1820
+ role="separator"
1821
+ aria-orientation="vertical"
1822
+ tabindex="0"
1823
+ :aria-label="text.resizeColumn.replace('{column}', col.label)"
1824
+ :aria-valuenow="widths[col.key]"
1825
+ class="absolute inset-y-0 right-0 w-1.5 cursor-col-resize touch-none outline-none transition-colors duration-fast hover:bg-brand focus-visible:bg-brand"
1826
+ :data-lp-resize="col.key"
1827
+ @pointerdown="startResize($event, col)"
1828
+ @click.stop
1829
+ @dblclick.stop="clearWidth(col)"
1830
+ @keydown="onResizeKey($event, col)"
1831
+ />
1375
1832
  </th>
1376
1833
  </tr>
1377
1834
  </thead>
@@ -1389,7 +1846,7 @@ usePaletteCommands(() => {
1389
1846
  <TransitionGroup
1390
1847
  tag="tbody"
1391
1848
  name="lp-row"
1392
- :css="animating"
1849
+ :css="animating && !virtualRowHeight"
1393
1850
  :class="busy && displayRows.length ? 'opacity-60 transition-opacity' : 'transition-opacity'"
1394
1851
  :aria-busy="busy || undefined"
1395
1852
  :ref="(el: unknown) => roving.setContainer((el as { $el?: HTMLElement })?.$el ?? null)"
@@ -1405,9 +1862,10 @@ usePaletteCommands(() => {
1405
1862
  :key="`lp-table-skeleton-${n}`"
1406
1863
  class="lp-row-empty border-b border-line/60 last:border-0"
1407
1864
  >
1865
+ <td v-if="expansionOn" class="w-px p-0" :class="cellY" />
1408
1866
  <td v-if="selectable" class="w-px p-0" :class="cellY" />
1409
1867
  <td
1410
- v-for="(col, ci) in visibleColumns"
1868
+ v-for="(col, ci) in shownColumns"
1411
1869
  :key="col.key"
1412
1870
  class="px-4"
1413
1871
  :class="[alignClass(col.align), cellY]"
@@ -1436,43 +1894,101 @@ usePaletteCommands(() => {
1436
1894
  <!-- A plain <tr>, because TransitionGroup animates element children and
1437
1895
  nothing else. The right-click menu is the table's one menu, told
1438
1896
  which row the event came from — see `contextMenu`. -->
1439
- <tr
1440
- v-for="(row, index) in displayRows"
1441
- :key="keyFor(row, index)"
1442
- @contextmenu="onRowContextMenu(row)"
1443
- class="group border-b border-line/60 outline-none transition-colors last:border-0 hover:bg-surface-soft/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[active]:bg-surface-soft/40"
1444
- :class="selectedSet.has(keyFor(row, index)) ? 'bg-brand-soft/40' : ''"
1445
- v-bind="roving.itemProps(String(keyFor(row, index)))"
1446
- :data-paint-key="selectable ? String(keyFor(row, index)) : undefined"
1447
- :data-lp-context-row="rowMenu ? '' : undefined"
1448
- @click="onRowClick(row, keyFor(row, index))"
1449
- @pointerdown="onRowPointerDown($event, keyFor(row, index))"
1450
- @keydown="onRowKeydown"
1451
- >
1452
- <td
1453
- v-if="selectable"
1454
- class="w-px cursor-pointer p-0 select-none"
1455
- :class="cellY"
1456
- @click.stop
1457
- @pointerdown.stop="paint.onPointerDown($event, keyFor(row, index))"
1458
- >
1459
- <div class="lp-tick lp-tick-open" :class="ticksShown ? '' : 'lp-tick-cue'">
1460
- <LpCheckbox
1461
- :model-value="selectedSet.has(keyFor(row, index))"
1462
- @update:model-value="(v) => toggleRow(keyFor(row, index), v)"
1463
- />
1464
- </div>
1465
- </td>
1466
- <td
1467
- v-for="(col, ci) in visibleColumns"
1468
- :key="col.key"
1469
- class="px-4 text-ink"
1470
- :class="[alignClass(col.align), cellY, col.nowrap ? 'whitespace-nowrap' : '']"
1897
+ <tr v-if="rowWindowed?.before" key="lp-virtual-before" aria-hidden="true">
1898
+ <td :colspan="colSpan" class="p-0" :style="{ height: `${rowWindowed.before}px` }" />
1899
+ </tr>
1900
+ <!-- One keyed fragment per entry: TransitionGroup reads through it to
1901
+ the row, so the rows animate as they always did. -->
1902
+ <template v-for="entry in viewEntries" :key="entry.id">
1903
+ <tr v-if="entry.kind === 'group'" class="border-b border-line/60 bg-surface-soft/60" :style="rowStyle">
1904
+ <td :colspan="colSpan" class="px-4 text-xs font-medium text-muted" :class="cellY">
1905
+ <slot name="group" :value="entry.value" :rows="entry.rows" :label="entry.label" :collapsed="entry.collapsed" :toggle="() => toggleGroup(entry.group)">
1906
+ <button
1907
+ type="button"
1908
+ class="inline-flex items-center gap-1 rounded-small outline-none transition-colors duration-fast hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
1909
+ :aria-expanded="!entry.collapsed"
1910
+ :aria-label="(entry.collapsed ? text.expandGroup : text.collapseGroup).replace('{label}', entry.label)"
1911
+ @click="toggleGroup(entry.group)"
1912
+ >
1913
+ <LpIcon name="lucide:chevron-right" :size="14" class="transition-transform duration-fast motion-reduce:transition-none" :class="entry.collapsed ? '' : 'rotate-90'" />
1914
+ {{ entry.label }}
1915
+ </button>
1916
+ </slot>
1917
+ </td>
1918
+ </tr>
1919
+ <tr v-else-if="entry.kind === 'detail'" class="border-b border-line/60 last:border-0" data-lp-row-detail>
1920
+ <td :colspan="colSpan" class="px-4 py-3">
1921
+ <slot name="expanded" :row="entry.row" :close="() => toggleExpanded(keyFor(entry.row, entry.index))" />
1922
+ </td>
1923
+ </tr>
1924
+ <!-- A plain <tr>, because TransitionGroup animates element children and
1925
+ nothing else. The right-click menu is the table's one menu, told
1926
+ which row the event came from — see `contextMenu`. -->
1927
+ <tr
1928
+ v-else
1929
+ @contextmenu="onRowContextMenu(entry.row)"
1930
+ class="group border-b border-line/60 outline-none transition-colors last:border-0 hover:bg-surface-soft/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[active]:bg-surface-soft/40"
1931
+ :class="isSelected(keyFor(entry.row, entry.index)) ? 'bg-brand-soft/40' : ''"
1932
+ :style="rowStyle"
1933
+ :aria-rowindex="virtualRowHeight ? rowOffset(entry) : undefined"
1934
+ v-bind="roving.itemProps(String(keyFor(entry.row, entry.index)))"
1935
+ :data-paint-key="selectable ? String(keyFor(entry.row, entry.index)) : undefined"
1936
+ :data-lp-context-row="rowMenu ? '' : undefined"
1937
+ @click="onRowClick(entry.row, keyFor(entry.row, entry.index))"
1938
+ @pointerdown="onRowPointerDown($event, keyFor(entry.row, entry.index))"
1939
+ @keydown="onRowKeydown"
1471
1940
  >
1472
- <slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]" :open-menu="openRowMenu">
1473
- {{ row[col.key] }}
1474
- </slot>
1475
- </td>
1941
+ <td v-if="expansionOn" class="w-px p-0 text-center" :class="cellY" @click.stop @keydown.stop>
1942
+ <button
1943
+ v-if="canExpand(entry.row)"
1944
+ type="button"
1945
+ class="inline-grid size-6 place-items-center rounded-small text-muted outline-none transition-colors duration-fast hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
1946
+ :aria-expanded="expandedKeys.has(keyFor(entry.row, entry.index))"
1947
+ :aria-label="expandedKeys.has(keyFor(entry.row, entry.index)) ? text.collapseRow : text.expandRow"
1948
+ @click="toggleExpanded(keyFor(entry.row, entry.index))"
1949
+ >
1950
+ <LpIcon name="lucide:chevron-right" :size="14" class="transition-transform duration-fast motion-reduce:transition-none" :class="expandedKeys.has(keyFor(entry.row, entry.index)) ? 'rotate-90' : ''" />
1951
+ </button>
1952
+ </td>
1953
+ <td
1954
+ v-if="selectable"
1955
+ class="w-px cursor-pointer p-0 select-none"
1956
+ :class="cellY"
1957
+ @click.stop
1958
+ @pointerdown.stop="paint.onPointerDown($event, keyFor(entry.row, entry.index))"
1959
+ >
1960
+ <div class="lp-tick lp-tick-open" :class="ticksShown ? '' : 'lp-tick-cue'">
1961
+ <LpCheckbox
1962
+ :model-value="isSelected(keyFor(entry.row, entry.index))"
1963
+ @update:model-value="(v) => toggleRow(keyFor(entry.row, entry.index), v)"
1964
+ />
1965
+ </div>
1966
+ </td>
1967
+ <td
1968
+ v-for="col in shownColumns"
1969
+ :key="col.key"
1970
+ class="px-4 text-ink"
1971
+ :class="[alignClass(col.align), cellY, col.nowrap ? 'whitespace-nowrap' : '', col.pin ? PIN_CELL : '']"
1972
+ :style="pinStyle(col)"
1973
+ >
1974
+ <slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">
1975
+ <!-- The field is its own world: its clicks and keys are not the row's. -->
1976
+ <span v-if="isEditable(col, entry.row)" class="inline-block" @click.stop @pointerdown.stop @keydown.stop>
1977
+ <LpEditable
1978
+ :model-value="String(entry.row[col.key] ?? '')"
1979
+ :label="col.label"
1980
+ size="sm"
1981
+ :validate="(value) => col.validateEdit?.(value, entry.row)"
1982
+ :save="(value) => saveCell(col, entry.row, value)"
1983
+ />
1984
+ </span>
1985
+ <template v-else>{{ entry.row[col.key] }}</template>
1986
+ </slot>
1987
+ </td>
1988
+ </tr>
1989
+ </template>
1990
+ <tr v-if="rowWindowed?.after" key="lp-virtual-after" aria-hidden="true">
1991
+ <td :colspan="colSpan" class="p-0" :style="{ height: `${rowWindowed.after}px` }" />
1476
1992
  </tr>
1477
1993
  </TransitionGroup>
1478
1994
  </table>