@leavepulse/ui 0.86.1 → 0.87.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 (242) hide show
  1. package/dist/assets/{LpSkeleton-H3Z5dEIk.css → LpSkeleton-D6GSatJ-.css} +15 -0
  2. package/dist/assets/{LpTable-CEFsRz5J.css → LpTable-ClBFQeTd.css} +51 -19
  3. package/dist/chunks/{ContextMenuItems-D15cGEw9.js → ContextMenuItems-D0jObgzN.js} +27 -3
  4. package/dist/chunks/{DropzoneLinkField-Dz7fIaLh.js → DropzoneLinkField-DhwYRnvd.js} +1 -1
  5. package/dist/chunks/{FieldLeading-DZZLK02U.js → FieldLeading-k9EJOKOI.js} +1 -1
  6. package/dist/chunks/{FieldWrapper-BDDw1F9H.js → FieldWrapper-CVTPnpoG.js} +1 -1
  7. package/dist/chunks/{IconCloseButton-Cn_w4aN3.js → IconCloseButton-CmWOJAXG.js} +1 -1
  8. package/dist/chunks/{LayoutCanvas-Dr5eSfAu.js → LayoutCanvas-j4TZqx1A.js} +1 -1
  9. package/dist/chunks/{LayoutNode-CViJnx6q.js → LayoutNode-4Eaf8ZWj.js} +1 -1
  10. package/dist/chunks/{LpActionBar-C0LK-Aqs.js → LpActionBar-Cx_MSZl9.js} +1 -1
  11. package/dist/chunks/{LpAddressInput-B9sSvodU.js → LpAddressInput-CfqQR5jE.js} +7 -7
  12. package/dist/chunks/{LpAlert-Bpd7122M.js → LpAlert-CYtbpRYW.js} +1 -1
  13. package/dist/chunks/{LpAppShell-DW8JmY1o.js → LpAppShell-BQG98odS.js} +4 -4
  14. package/dist/chunks/{LpAutocomplete-CQID7Zc-.js → LpAutocomplete-VNID5zdy.js} +4 -4
  15. package/dist/chunks/{LpAvatar-DLkiMGKx.js → LpAvatar-CfLBqlY1.js} +1 -1
  16. package/dist/chunks/{LpBadge-RFp8uHFt.js → LpBadge-Bh5roBr_.js} +1 -1
  17. package/dist/chunks/{LpBottomNav-BcJVkAhh.js → LpBottomNav-9AqvZlRW.js} +1 -1
  18. package/dist/chunks/{LpBreadcrumbs-DJ_DB45r.js → LpBreadcrumbs-Zv1fn9e_.js} +1 -1
  19. package/dist/chunks/{LpCalendar-D6tfCr2G.js → LpCalendar-aACjUhpM.js} +1 -1
  20. package/dist/chunks/{LpCard-BPtYXwnT.js → LpCard-xXBqLVGo.js} +1 -1
  21. package/dist/chunks/{LpCheckbox-DcXC2qd5.js → LpCheckbox-P7tEw8GO.js} +2 -2
  22. package/dist/chunks/{LpCodeBlock-kGT15Zou.js → LpCodeBlock-BNThgUdQ.js} +2 -2
  23. package/dist/chunks/{LpCodeDisplay-DlwzZo9o.js → LpCodeDisplay-DsiE-Qy9.js} +4 -4
  24. package/dist/chunks/{LpColorPicker-Cjna_Bio.js → LpColorPicker-EXpwcX-t.js} +3 -3
  25. package/dist/chunks/{LpConfirmDialog-bfcX5F9d.js → LpConfirmDialog-DPiPVRn_.js} +2 -2
  26. package/dist/chunks/{LpConfirmHost-DIgloymi.js → LpConfirmHost-Ddiljlog.js} +1 -1
  27. package/dist/chunks/{LpContextMenu-CUFZIQX2.js → LpContextMenu-DYpDm6iI.js} +2 -2
  28. package/dist/chunks/{LpContextSwitcher-CMweLbv2.js → LpContextSwitcher-BPDSkKyA.js} +2 -2
  29. package/dist/chunks/{LpCopyButton-TGoE43mE.js → LpCopyButton-CxX682t4.js} +1 -1
  30. package/dist/chunks/{LpDataList-Due909Cy.js → LpDataList-DCtF7krm.js} +2 -2
  31. package/dist/chunks/{LpDatePicker-B5gzwI3T.js → LpDatePicker-BElEHVm6.js} +5 -5
  32. package/dist/chunks/{LpDateRangePicker-CwNnpyoL.js → LpDateRangePicker-vLt-drEu.js} +3 -3
  33. package/dist/chunks/{LpDisclosure-CsJ7nnFz.js → LpDisclosure-D4JZ-o8K.js} +1 -1
  34. package/dist/chunks/{LpDrawer-C1q2bl1r.js → LpDrawer-C97k-3L2.js} +1 -1
  35. package/dist/chunks/{LpDropdownMenu-DRABI-Bl.js → LpDropdownMenu-BPEsqZCN.js} +2 -2
  36. package/dist/chunks/{LpEditable-DCPdDDBb.js → LpEditable-CeRAzYMv.js} +2 -2
  37. package/dist/chunks/{LpEmptyState-DT8U4R8L.js → LpEmptyState-DrHBR3yh.js} +1 -1
  38. package/dist/chunks/{LpFieldNotice-mcmO-O-J.js → LpFieldNotice-DUPo1Yii.js} +1 -1
  39. package/dist/chunks/{LpFileTree-wiXExcZI.js → LpFileTree-BcQgsw4e.js} +5 -5
  40. package/dist/chunks/{LpFileTreeNode-DfvIGmSz.js → LpFileTreeNode-Dk2WqkPF.js} +4 -4
  41. package/dist/chunks/{LpFormField-C5GOuOY-.js → LpFormField-jbM7K1Hg.js} +1 -1
  42. package/dist/chunks/{LpHintLayer-B0choo1r.js → LpHintLayer-CUSRLtLu.js} +1 -1
  43. package/dist/chunks/{LpHints-BEetUVTl.js → LpHints-BF40CP4_.js} +3 -3
  44. package/dist/chunks/{LpIcon-DCGhNsCU.js → LpIcon-CxHFPwKt.js} +1 -1
  45. package/dist/chunks/{LpInfraNode-BsExhdPE.js → LpInfraNode-BYiMFDnA.js} +2 -2
  46. package/dist/chunks/{LpInput-CKMblRtp.js → LpInput-BU5IsOlE.js} +3 -3
  47. package/dist/chunks/{LpInspector-BNxjuWMP.js → LpInspector-CEko6-pS.js} +4 -4
  48. package/dist/chunks/{LpLightbox-DVyiB4dw.js → LpLightbox-REei-y5e.js} +4 -4
  49. package/dist/chunks/{LpLogInspector-Yj310k4n.js → LpLogInspector-Bwz351LY.js} +2 -2
  50. package/dist/chunks/{LpLogViewer-Bki-IdNC.js → LpLogViewer-BCkqdH66.js} +6 -6
  51. package/dist/chunks/{LpMapPicker-CnpJhiY0.js → LpMapPicker-DVfiy3rR.js} +1 -1
  52. package/dist/chunks/{LpMediaGallery-OJemmbV7.js → LpMediaGallery-BF1Bch3v.js} +4 -4
  53. package/dist/chunks/{LpModal-B5y9_LgA.js → LpModal-BK2T5uIG.js} +1 -1
  54. package/dist/chunks/{LpModalNav-BLvDVl2-.js → LpModalNav-Dk4eHpg6.js} +3 -3
  55. package/dist/chunks/{LpNotificationBell-Bf0u3t5q.js → LpNotificationBell-CMwUXhFg.js} +4 -4
  56. package/dist/chunks/{LpNotificationFeed-BPZM3zkS.js → LpNotificationFeed-BXsRp9jm.js} +5 -5
  57. package/dist/chunks/{LpNumberField-BeYT7zzt.js → LpNumberField-QUMXvpqx.js} +3 -3
  58. package/dist/chunks/{LpOtpInput-DqwY-U5R.js → LpOtpInput-C14XagVB.js} +1 -1
  59. package/dist/chunks/{LpPagination-V1wEhu1K.js → LpPagination-DGLntZJN.js} +1 -1
  60. package/dist/chunks/{LpPaletteTheme-CWFz4H2D.js → LpPaletteTheme-wiJtP5hV.js} +2 -2
  61. package/dist/chunks/{LpPasswordInput-LaOXNYuV.js → LpPasswordInput-W9AZfzaL.js} +2 -2
  62. package/dist/chunks/{LpPhoneInput-D7Tq2iaz.js → LpPhoneInput-DsRv-Qla.js} +3 -3
  63. package/dist/chunks/{LpPie-Bzv8mnVR.js → LpPie-pbrNFbRP.js} +3 -3
  64. package/dist/chunks/{LpPipeline-D-uPcbMN.js → LpPipeline-BHWmvOiv.js} +2 -2
  65. package/dist/chunks/{LpQuota-BR8ck2a9.js → LpQuota-BaCq5YOQ.js} +1 -1
  66. package/dist/chunks/{LpRadioGroup-DSOCL9MD.js → LpRadioGroup-D94KMStE.js} +2 -2
  67. package/dist/chunks/{LpRangeCalendar-QepHrmba.js → LpRangeCalendar-C9gkNMYw.js} +1 -1
  68. package/dist/chunks/{LpRangeSlider-BaAM7XIs.js → LpRangeSlider-CkP9hYUC.js} +1 -1
  69. package/dist/chunks/{LpRepeater-BKq5XPh4.js → LpRepeater-DLLcbQ6J.js} +2 -2
  70. package/dist/chunks/{LpSecretField-Dp-c7wWc.js → LpSecretField-Au7ZQTtm.js} +5 -5
  71. package/dist/chunks/{LpSection-BGzlKIWO.js → LpSection-RKCwB0xN.js} +1 -1
  72. package/dist/chunks/{LpSegmented-CiaF9DUK.js → LpSegmented-DD8MPj4j.js} +2 -2
  73. package/dist/chunks/{LpSelect-CB6Xbnfk.js → LpSelect-BeXz1TWh.js} +4 -4
  74. package/dist/chunks/{LpSidebar-DnqbKULj.js → LpSidebar-DUFlf6Uu.js} +4 -4
  75. package/dist/chunks/{LpSidebarNav-B3Eo_VIS.js → LpSidebarNav-C34YEZIR.js} +4 -4
  76. package/dist/chunks/{LpSidebarRow-CDBnvvd8.js → LpSidebarRow-BeEGgsZi.js} +3 -3
  77. package/dist/chunks/{LpSkeleton-BB6XIV-w.js → LpSkeleton-leTl53V9.js} +1 -1
  78. package/dist/chunks/{LpSlider-DTb_Ivkm.js → LpSlider-BCy5rjwK.js} +1 -1
  79. package/dist/chunks/{LpSparkline-CExIRDb3.js → LpSparkline-11ryFdPX.js} +1 -1
  80. package/dist/chunks/{LpStat-BtxSto7K.js → LpStat-eVz7iUfD.js} +3 -3
  81. package/dist/chunks/{LpStatus-_fIwWXQV.js → LpStatus-C6GqSHJj.js} +1 -1
  82. package/dist/chunks/{LpStepMark-iDZw1DYf.js → LpStepMark-CPGVj1XG.js} +1 -1
  83. package/dist/chunks/{LpStepper-CjC3cp1F.js → LpStepper-DBNRA6Q2.js} +1 -1
  84. package/dist/chunks/{LpSwitch-BNHyYDhX.js → LpSwitch-D7hmVsif.js} +1 -1
  85. package/dist/chunks/{LpTabFace-gATEe9pQ.js → LpTabFace-uoZgD_SN.js} +2 -2
  86. package/dist/chunks/{LpTableFilter-B_BFoBqS.js → LpTableFilter-BGFNKyUX.js} +2 -2
  87. package/dist/chunks/{LpTagsInput-By7LMb47.js → LpTagsInput-D9uOSlvf.js} +5 -5
  88. package/dist/chunks/{LpTextarea-Ck6RAfzu.js → LpTextarea-DwE-lQt4.js} +1 -1
  89. package/dist/chunks/{LpThemeStudio-RyI-SWiw.js → LpThemeStudio-COn2H76d.js} +5 -5
  90. package/dist/chunks/{LpThemeSwitcher-BZYJ0AXy.js → LpThemeSwitcher-B3R3nNid.js} +3 -3
  91. package/dist/chunks/{LpTimeField-BTKJVeNt.js → LpTimeField-Mi8H1g_9.js} +2 -2
  92. package/dist/chunks/{LpToaster-YcLJFSsH.js → LpToaster-DOJn6Vey.js} +2 -2
  93. package/dist/chunks/{LpTopologyCanvas-paC1Dup-.js → LpTopologyCanvas-5RFMP4gR.js} +4 -4
  94. package/dist/chunks/{LpTour-D_AN_SsZ.js → LpTour-DUI_hoPv.js} +1 -1
  95. package/dist/chunks/{LpTruncate-DCZjfcE-.js → LpTruncate-CynJhsEw.js} +1 -1
  96. package/dist/chunks/{LpUptimeBar-BoxcCDVY.js → LpUptimeBar-G7PfNJUH.js} +1 -1
  97. package/dist/chunks/{LpVideoCompress-BfulQ8Rs.js → LpVideoCompress-CPUxXh-c.js} +1 -1
  98. package/dist/chunks/{LpVideoPlayer--87I5Kqu.js → LpVideoPlayer-BbCjEQAx.js} +4 -4
  99. package/dist/chunks/{LpWindow-Bo5qQgVY.js → LpWindow-Dp_0nNqg.js} +2 -2
  100. package/dist/chunks/{LpWorkspaceSwitcher-D50sHs9N.js → LpWorkspaceSwitcher-bjkncjSl.js} +3 -3
  101. package/dist/chunks/{MenuTitle-DCHcfyPH.js → MenuTitle-YcSjF8xo.js} +1 -1
  102. package/dist/chunks/{OptionContent-o58kS7Zh.js → OptionContent-DZb5eKRV.js} +1 -1
  103. package/dist/chunks/{PickerField-DNo1k89A.js → PickerField-CjOTUXDK.js} +1 -1
  104. package/dist/chunks/{RevealToggle-w7-BMcgC.js → RevealToggle-Bl1NcfxB.js} +1 -1
  105. package/dist/chunks/{TourCard-0yYxSdd4.js → TourCard-BovXtMbO.js} +1 -1
  106. package/dist/chunks/{VideoThumb-DL_QkAJY.js → VideoThumb-BfDkIXqE.js} +1 -1
  107. package/dist/chunks/{VideoVolume-D-2TmsVj.js → VideoVolume-DE3i1ree.js} +2 -2
  108. package/dist/chunks/{icons-V0k93Puc.js → icons-C8ccRCKw.js} +1 -1
  109. package/dist/components/ContextMenuItems.vue.js +1 -1
  110. package/dist/components/DropzoneLinkField.vue.js +1 -1
  111. package/dist/components/FieldLeading.vue.js +1 -1
  112. package/dist/components/FieldWrapper.vue.js +1 -1
  113. package/dist/components/IconCloseButton.vue.js +1 -1
  114. package/dist/components/LayoutCanvas.vue.js +1 -1
  115. package/dist/components/LayoutNode.vue.js +1 -1
  116. package/dist/components/LpActionBar.vue.js +1 -1
  117. package/dist/components/LpAddressInput.vue.js +1 -1
  118. package/dist/components/LpAlert.vue.js +1 -1
  119. package/dist/components/LpAppShell.vue.js +1 -1
  120. package/dist/components/LpAutocomplete.vue.js +1 -1
  121. package/dist/components/LpAvatar.vue.js +1 -1
  122. package/dist/components/LpBadge.vue.js +1 -1
  123. package/dist/components/LpBottomNav.vue.js +1 -1
  124. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  125. package/dist/components/LpCalendar.vue.js +1 -1
  126. package/dist/components/LpCard.vue.js +1 -1
  127. package/dist/components/LpChart.vue.js +3 -3
  128. package/dist/components/LpCheckbox.vue.js +1 -1
  129. package/dist/components/LpCodeBlock.vue.js +1 -1
  130. package/dist/components/LpCodeDisplay.vue.js +1 -1
  131. package/dist/components/LpColorPicker.vue.js +1 -1
  132. package/dist/components/LpCommandPalette.vue.js +2 -2
  133. package/dist/components/LpConfirmDialog.vue.js +1 -1
  134. package/dist/components/LpConfirmHost.vue.js +1 -1
  135. package/dist/components/LpContextMenu.vue.d.ts +7 -0
  136. package/dist/components/LpContextMenu.vue.js +1 -1
  137. package/dist/components/LpContextSwitcher.vue.js +1 -1
  138. package/dist/components/LpCopyButton.vue.js +1 -1
  139. package/dist/components/LpDataList.vue.js +1 -1
  140. package/dist/components/LpDatePicker.vue.js +1 -1
  141. package/dist/components/LpDateRangePicker.vue.js +1 -1
  142. package/dist/components/LpDisclosure.vue.js +1 -1
  143. package/dist/components/LpDock.vue.js +1 -1
  144. package/dist/components/LpDrawer.vue.js +1 -1
  145. package/dist/components/LpDropdownMenu.vue.js +1 -1
  146. package/dist/components/LpDropzone.vue.js +2 -2
  147. package/dist/components/LpEditable.vue.js +1 -1
  148. package/dist/components/LpEmptyState.vue.js +1 -1
  149. package/dist/components/LpFieldNotice.vue.js +1 -1
  150. package/dist/components/LpFileTree.vue.js +1 -1
  151. package/dist/components/LpFileTreeNode.vue.js +1 -1
  152. package/dist/components/LpFormField.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 +1 -1
  157. package/dist/components/LpInfraNode.vue.js +1 -1
  158. package/dist/components/LpInput.vue.js +1 -1
  159. package/dist/components/LpInspector.vue.js +1 -1
  160. package/dist/components/LpLightbox.vue.js +1 -1
  161. package/dist/components/LpLogInspector.vue.js +1 -1
  162. package/dist/components/LpLogViewer.vue.js +1 -1
  163. package/dist/components/LpMapPicker.vue.js +1 -1
  164. package/dist/components/LpMediaGallery.vue.js +1 -1
  165. package/dist/components/LpModal.vue.js +1 -1
  166. package/dist/components/LpModalNav.vue.js +1 -1
  167. package/dist/components/LpNotificationBell.vue.js +1 -1
  168. package/dist/components/LpNotificationFeed.vue.js +1 -1
  169. package/dist/components/LpNumberField.vue.js +1 -1
  170. package/dist/components/LpOtpInput.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 +1 -1
  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/LpQuota.vue.js +1 -1
  181. package/dist/components/LpRadioGroup.vue.js +1 -1
  182. package/dist/components/LpRangeCalendar.vue.js +1 -1
  183. package/dist/components/LpRangeSlider.vue.js +1 -1
  184. package/dist/components/LpRepeater.vue.js +1 -1
  185. package/dist/components/LpSecretField.vue.js +1 -1
  186. package/dist/components/LpSection.vue.js +1 -1
  187. package/dist/components/LpSegmented.vue.js +1 -1
  188. package/dist/components/LpSelect.vue.js +1 -1
  189. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  190. package/dist/components/LpSidebar.vue.js +1 -1
  191. package/dist/components/LpSidebarNav.vue.js +1 -1
  192. package/dist/components/LpSidebarRow.vue.js +1 -1
  193. package/dist/components/LpSkeleton.vue.js +1 -1
  194. package/dist/components/LpSlider.vue.js +1 -1
  195. package/dist/components/LpSparkline.vue.js +1 -1
  196. package/dist/components/LpStat.vue.js +1 -1
  197. package/dist/components/LpStatus.vue.js +1 -1
  198. package/dist/components/LpStepMark.vue.js +1 -1
  199. package/dist/components/LpStepper.vue.js +1 -1
  200. package/dist/components/LpSwitch.vue.js +1 -1
  201. package/dist/components/LpTabFace.vue.js +1 -1
  202. package/dist/components/LpTable.vue.d.ts +76 -1
  203. package/dist/components/LpTable.vue.js +513 -102
  204. package/dist/components/LpTableFilter.vue.js +1 -1
  205. package/dist/components/LpTabs.vue.js +3 -3
  206. package/dist/components/LpTagsInput.vue.js +1 -1
  207. package/dist/components/LpTextarea.vue.js +1 -1
  208. package/dist/components/LpThemeStudio.vue.js +1 -1
  209. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  210. package/dist/components/LpTimeField.vue.js +1 -1
  211. package/dist/components/LpToaster.vue.js +1 -1
  212. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  213. package/dist/components/LpTour.vue.js +1 -1
  214. package/dist/components/LpTruncate.vue.js +1 -1
  215. package/dist/components/LpUptimeBar.vue.js +1 -1
  216. package/dist/components/LpVideoCompress.vue.js +1 -1
  217. package/dist/components/LpVideoPlayer.vue.js +1 -1
  218. package/dist/components/LpVideoTrim.vue.js +4 -4
  219. package/dist/components/LpWindow.vue.js +1 -1
  220. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  221. package/dist/components/MenuTitle.vue.js +1 -1
  222. package/dist/components/OptionContent.vue.js +1 -1
  223. package/dist/components/PickerField.vue.js +1 -1
  224. package/dist/components/RevealToggle.vue.js +1 -1
  225. package/dist/components/TourCard.vue.js +1 -1
  226. package/dist/components/VideoFlash.vue.js +1 -1
  227. package/dist/components/VideoThumb.vue.js +1 -1
  228. package/dist/components/VideoVolume.vue.js +1 -1
  229. package/dist/components/menuWhen.js +1 -1
  230. package/dist/components/tableFeatures.d.ts +9 -0
  231. package/dist/icons-all.js +1 -1
  232. package/dist/index.d.ts +1 -1
  233. package/dist/index.js +94 -89
  234. package/package.json +1 -1
  235. package/src/components/ContextMenuItems.vue +22 -0
  236. package/src/components/LpContextMenu.vue +7 -0
  237. package/src/components/LpSkeleton.vue +15 -0
  238. package/src/components/LpTable.vue +396 -3
  239. package/src/components/tableFeatures.ts +10 -0
  240. package/src/index.ts +1 -1
  241. package/src/kit-icons.ts +1 -1
  242. package/src/locales/uk.ts +5 -0
@@ -22,6 +22,8 @@ import LpScrollArea from "./LpScrollArea.vue"
22
22
  import LpRollingText from "./LpRollingText.vue"
23
23
  import LpShift from "./LpShift.vue"
24
24
  import LpTableFilter, { type TableFilterOption } from "./LpTableFilter.vue"
25
+ import LpDropdownMenu from "./LpDropdownMenu.vue"
26
+ import LpSegmented from "./LpSegmented.vue"
25
27
  import { useKitStorage } from "../composables/usePersisted"
26
28
  import { useAsyncTask } from "../composables/useAsyncTask"
27
29
  import { useTableFacets } from "./tableFacets"
@@ -41,6 +43,7 @@ import {
41
43
  sortByKeys,
42
44
  type ColumnPin,
43
45
  type ColumnPreset,
46
+ type TableLayout,
44
47
  } from "./tableFeatures"
45
48
  import { devWarn } from "../composables/devWarn"
46
49
  import { startPointerDrag } from "../composables/pointerDrag"
@@ -194,6 +197,11 @@ export interface TableLabels {
194
197
  groupCount: string
195
198
  expandGroup: string
196
199
  collapseGroup: string
200
+ layoutTable: string
201
+ layoutCards: string
202
+ layoutList: string
203
+ layoutSwitch: string
204
+ sortBy: string
197
205
  }
198
206
 
199
207
  export interface TableExportOptions extends CsvOptions {
@@ -241,6 +249,7 @@ export interface TablePage<Row> {
241
249
  type RowKey = string | number
242
250
 
243
251
 
252
+
244
253
  const props = withDefaults(
245
254
  defineProps<{
246
255
  /** Publish loaded rows and existing row actions to the enclosing command scope. */
@@ -411,6 +420,30 @@ const props = withDefaults(
411
420
  virtualRowHeight?: number
412
421
  /** Rows drawn beyond each edge of the view. */
413
422
  virtualOverscan?: number
423
+ /**
424
+ * How the rows are drawn (v-model:layout): `table`, `cards` or `list`. Left
425
+ * unset the table keeps the choice itself, remembered with `persist`.
426
+ */
427
+ layout?: TableLayout
428
+ /**
429
+ * The layouts the user can switch between, offered as a switch in the bar
430
+ * above the rows. One layout, or none given, means no switch.
431
+ */
432
+ layouts?: readonly TableLayout[]
433
+ /**
434
+ * Below this width in px (the table's own, not the window's) a `table`
435
+ * layout is drawn as a `list`: a phone gets one line per row instead of a
436
+ * table that loses its columns to `hideBelow` or scrolls sideways.
437
+ */
438
+ layoutBelow?: number
439
+ /**
440
+ * `framed` (default) draws the rows on a surface with a border; `plain`
441
+ * leaves the frame out, for a table that already sits in a card or a
442
+ * section of its own.
443
+ */
444
+ variant?: "framed" | "plain"
445
+ /** The narrowest a card may get in the `cards` layout; the grid fits as many as fit. */
446
+ cardWidth?: string
414
447
  }>(),
415
448
  { palette: undefined,
416
449
  rows: () => [],
@@ -436,6 +469,11 @@ const props = withDefaults(
436
469
  expanded: undefined,
437
470
  virtualRowHeight: undefined,
438
471
  virtualOverscan: 6,
472
+ layout: undefined,
473
+ layouts: undefined,
474
+ layoutBelow: undefined,
475
+ variant: "framed",
476
+ cardWidth: "16rem",
439
477
  },
440
478
  )
441
479
 
@@ -482,6 +520,11 @@ const labelSet = useLabels<TableLabels>(
482
520
  groupCount: "{label} ({count})",
483
521
  expandGroup: "Expand {label}",
484
522
  collapseGroup: "Collapse {label}",
523
+ layoutTable: "Table",
524
+ layoutCards: "Cards",
525
+ layoutList: "List",
526
+ layoutSwitch: "View",
527
+ sortBy: "Sort",
485
528
  },
486
529
  () => props.labels,
487
530
  )
@@ -528,6 +571,7 @@ const emit = defineEmits<{
528
571
  (e: "update:expanded", value: RowKey[]): void
529
572
  (e: "cellEdit", edit: TableCellEdit<T>): void
530
573
  (e: "rowClick", row: T): void
574
+ (e: "update:layout", value: TableLayout): void
531
575
  }>()
532
576
 
533
577
  function keyFor(row: T, index: number): RowKey {
@@ -1174,6 +1218,14 @@ onMounted(() => {
1174
1218
  if (state.hidden) writeHidden(mergeRestored(hiddenValues.value, state.hidden))
1175
1219
  if (state.columns) hiddenColumns.value = new Set(state.columns)
1176
1220
  }
1221
+ // Kept beside the table state rather than in it: the layout is a choice
1222
+ // about how to look, and an older table state must not lose its sort and
1223
+ // filters to a field it never had.
1224
+ const layout = store.get(`${storageKey.value}:layout`)
1225
+ if ((layout === "table" || layout === "cards" || layout === "list") && (!props.layouts || props.layouts.includes(layout))) {
1226
+ ownLayout.value = layout
1227
+ if (props.layout !== undefined && props.layout !== layout) emit("update:layout", layout)
1228
+ }
1177
1229
  }
1178
1230
  restored.value = true
1179
1231
  })
@@ -1389,6 +1441,7 @@ const showBar = computed(
1389
1441
  props.summary ||
1390
1442
  props.exportable ||
1391
1443
  !!slots.toolbar ||
1444
+ layoutChoices.value.length > 0 ||
1392
1445
  (!!props.selectable && props.selected.length > 0) ||
1393
1446
  activeFilters.value.length > 0,
1394
1447
  )
@@ -1401,6 +1454,97 @@ const filteredOut = computed(() => {
1401
1454
 
1402
1455
  const cellY = computed(() => (props.density === "compact" ? "py-1.5" : "py-3"))
1403
1456
 
1457
+ // ── layouts ──────────────────────────────────────────────────
1458
+ //
1459
+ // One set of rows and tools, drawn three ways. Everything above this line
1460
+ // (sort, filters, selection, the menu, export, the palette) reads
1461
+ // `displayRows` and knows nothing of how they are drawn, so a card grid or a
1462
+ // list gets all of it for free instead of every page building its own.
1463
+ const ownLayout = ref<TableLayout>("table")
1464
+ const chosenLayout = computed<TableLayout>(() => props.layout ?? ownLayout.value)
1465
+
1466
+ // The table's own width, not the window's: the same table sits in a wide page
1467
+ // and in a narrow side panel.
1468
+ const frameWidth = ref(Number.POSITIVE_INFINITY)
1469
+ useResizeObserver(frameEl, (_, el) => (frameWidth.value = el.clientWidth), { immediate: true })
1470
+
1471
+ const activeLayout = computed<TableLayout>(() =>
1472
+ chosenLayout.value === "table" && props.layoutBelow && frameWidth.value < props.layoutBelow
1473
+ ? "list"
1474
+ : chosenLayout.value,
1475
+ )
1476
+ const layoutChoices = computed(() => (props.layouts && props.layouts.length > 1 ? props.layouts : []))
1477
+ const LAYOUT_ICONS: Record<TableLayout, string> = {
1478
+ table: "lucide:table-2",
1479
+ cards: "lucide:layout-grid",
1480
+ list: "lucide:list",
1481
+ }
1482
+ const layoutOptions = computed(() =>
1483
+ layoutChoices.value.map((value) => {
1484
+ const label = text.value[`layout${value[0]!.toUpperCase()}${value.slice(1)}`] ?? value
1485
+ return { value, icon: LAYOUT_ICONS[value], ariaLabel: label }
1486
+ }),
1487
+ )
1488
+
1489
+ function chooseLayout(next: string) {
1490
+ const layout = next as TableLayout
1491
+ if (layout === chosenLayout.value) return
1492
+ ownLayout.value = layout
1493
+ emit("update:layout", layout)
1494
+ }
1495
+
1496
+ // Without a header the sort and the column filters need a home: the bar.
1497
+ const headerless = computed(() => activeLayout.value !== "table")
1498
+ const sortableColumns = computed(() => props.columns.filter((col) => col.sortable && col.label))
1499
+ const filterableColumns = computed(() => props.columns.filter((col) => col.filterable && col.label))
1500
+
1501
+ function sortMenu(): ContextMenuItemDef[] {
1502
+ const current = activeSort.value
1503
+ const dir = current?.dir ?? "asc"
1504
+ return [
1505
+ // The columns, the one sorted by ticked; picking one keeps the direction.
1506
+ ...sortableColumns.value.map((col): ContextMenuItemDef => ({
1507
+ label: col.label,
1508
+ selected: current?.key === col.key,
1509
+ onSelect: () => writeSorts([{ key: col.key, dir }]),
1510
+ })),
1511
+ {
1512
+ label: text.value.sortAsc,
1513
+ icon: "lucide:arrow-up",
1514
+ separatorBefore: true,
1515
+ disabled: !current,
1516
+ selected: current?.dir === "asc",
1517
+ onSelect: () => current && writeSorts([{ key: current.key, dir: "asc" }]),
1518
+ },
1519
+ {
1520
+ label: text.value.sortDesc,
1521
+ icon: "lucide:arrow-down",
1522
+ disabled: !current,
1523
+ selected: current?.dir === "desc",
1524
+ onSelect: () => current && writeSorts([{ key: current.key, dir: "desc" }]),
1525
+ },
1526
+ { label: text.value.sortClear, icon: "lucide:x", separatorBefore: true, disabled: !current, onSelect: () => writeSorts([]) },
1527
+ ]
1528
+ }
1529
+
1530
+ const sortSummary = computed(() => {
1531
+ const current = activeSort.value
1532
+ const col = current && props.columns.find((c) => c.key === current.key)
1533
+ return col ? col.label : text.value.sortBy
1534
+ })
1535
+
1536
+ // A card shows the columns the user keeps, not the ones a narrow table drops:
1537
+ // the card has its own room, and `hideBelow` was a decision about a row.
1538
+ const cardColumns = computed(() =>
1539
+ orderPinned(props.columns.filter((col) => !hiddenColumns.value.has(col.key))),
1540
+ )
1541
+ const titleColumn = computed(() => cardColumns.value[0])
1542
+ const fieldColumns = computed(() => cardColumns.value.slice(1).filter((col) => col.label))
1543
+
1544
+ watch(chosenLayout, (layout) => {
1545
+ if (storageKey.value && restored.value) store.set(`${storageKey.value}:layout`, layout)
1546
+ })
1547
+
1404
1548
  /*
1405
1549
  * Skeleton bars of uneven width, the way real cells are: a column of identical
1406
1550
  * bars reads as a striped background rather than as rows of text. Fixed per
@@ -1484,7 +1628,7 @@ watch(viewportEl, (el, _, onCleanup) => {
1484
1628
  useResizeObserver(viewportEl, (_, el) => (viewportHeight.value = el.clientHeight), { immediate: true })
1485
1629
 
1486
1630
  const rowWindowed = computed(() =>
1487
- props.virtualRowHeight
1631
+ props.virtualRowHeight && !headerless.value
1488
1632
  ? rowWindow(scrollTop.value, viewportHeight.value, props.virtualRowHeight, allEntries.value.length, props.virtualOverscan)
1489
1633
  : null,
1490
1634
  )
@@ -1627,6 +1771,47 @@ usePaletteCommands(() => {
1627
1771
  <div v-if="$slots.toolbar" class="flex min-w-0 flex-wrap items-center gap-2 text-sm">
1628
1772
  <slot name="toolbar" />
1629
1773
  </div>
1774
+ <!-- Without a header the sort and the column funnels live here: the same
1775
+ menu and the same filter, reached from the bar instead. -->
1776
+ <div v-if="headerless && (sortableColumns.length || filterableColumns.length)" class="flex flex-wrap items-center gap-1 text-sm">
1777
+ <LpDropdownMenu v-if="sortableColumns.length" :items="sortMenu()">
1778
+ <template #trigger>
1779
+ <LpButton variant="ghost" size="sm" :aria-label="text.sortBy">
1780
+ <LpIcon
1781
+ :name="activeSort ? (activeSort.dir === 'asc' ? 'lucide:arrow-up' : 'lucide:arrow-down') : 'lucide:arrow-up-down'"
1782
+ :size="14"
1783
+ />
1784
+ {{ sortSummary }}
1785
+ </LpButton>
1786
+ </template>
1787
+ </LpDropdownMenu>
1788
+ <LpPopover
1789
+ v-for="col in filterableColumns"
1790
+ :key="col.key"
1791
+ align="start"
1792
+ panel-class="w-72"
1793
+ :aria-label="text.filterColumn.replace('{column}', col.label)"
1794
+ >
1795
+ <template #trigger>
1796
+ <LpButton
1797
+ variant="ghost"
1798
+ size="sm"
1799
+ :class="hiddenValues[col.key]?.size ? 'text-brand' : ''"
1800
+ :aria-label="text.filterColumn.replace('{column}', col.label)"
1801
+ :data-lp-filter="col.key"
1802
+ >
1803
+ <LpIcon :name="hiddenValues[col.key]?.size ? 'lucide:filter-x' : 'lucide:filter'" :size="14" />
1804
+ {{ col.label }}
1805
+ </LpButton>
1806
+ </template>
1807
+ <LpTableFilter
1808
+ :options="facetOptions.get(col.key)?.value ?? []"
1809
+ :hidden="hiddenValues[col.key] ?? new Set()"
1810
+ :labels="filterLabels()"
1811
+ @update:hidden="(next) => setHidden(col.key, next)"
1812
+ />
1813
+ </LpPopover>
1814
+ </div>
1630
1815
  <div v-if="activeFilters.length" class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
1631
1816
  <LpBadge
1632
1817
  v-for="f in activeFilters"
@@ -1696,6 +1881,14 @@ usePaletteCommands(() => {
1696
1881
  <slot name="bulk" :selected="selected" :all-matching="allMatching" :clear="clearSelection" />
1697
1882
  </span>
1698
1883
  </Transition>
1884
+ <LpSegmented
1885
+ v-if="layoutOptions.length"
1886
+ :model-value="chosenLayout"
1887
+ :options="layoutOptions"
1888
+ size="sm"
1889
+ :aria-label="text.layoutSwitch"
1890
+ @update:model-value="chooseLayout"
1891
+ />
1699
1892
  <LpButton v-if="exportable" variant="ghost" size="sm" @click="exportCsv()">
1700
1893
  <LpIcon name="lucide:download" :size="14" />
1701
1894
  {{ text.exportCsv }}
@@ -1718,7 +1911,9 @@ usePaletteCommands(() => {
1718
1911
  <LpScrollArea
1719
1912
  v-bind="scrollAttrs"
1720
1913
  :fade="false"
1721
- class="min-h-0 flex-1 rounded-card border border-line bg-surface"
1914
+ v-if="!headerless"
1915
+ class="min-h-0 flex-1"
1916
+ :class="variant === 'framed' ? 'rounded-card border border-line bg-surface' : ''"
1722
1917
  :style="load && floor ? { minHeight: `${floor + 2}px` } : undefined"
1723
1918
  :bar-inset-top="barInsetTop"
1724
1919
  >
@@ -1790,7 +1985,7 @@ usePaletteCommands(() => {
1790
1985
  <LpContextMenu v-if="col.sortable || col.filterable" :items="headerMenu(col)">
1791
1986
  <button
1792
1987
  type="button"
1793
- 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"
1988
+ class="group/sort -mx-1 inline-flex items-center gap-1 rounded-small px-1 uppercase outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
1794
1989
  :class="[
1795
1990
  col.align === 'right' ? 'flex-row-reverse' : '',
1796
1991
  sortOf(col) ? 'text-ink' : '',
@@ -2016,6 +2211,169 @@ usePaletteCommands(() => {
2016
2211
  </table>
2017
2212
  </LpContextMenu>
2018
2213
  </LpScrollArea>
2214
+ <!-- Cards and list: the same rows, menu, selection and keyboard, drawn
2215
+ without a header. The one menu wraps the lot, as it does the table. -->
2216
+ <LpContextMenu
2217
+ v-else
2218
+ :items="contextMenu"
2219
+ always
2220
+ :no-hint="!rowMenu"
2221
+ :title="menuHead?.title"
2222
+ :subtitle="menuHead?.subtitle"
2223
+ :title-icon="menuHead?.icon"
2224
+ :title-image="menuHead?.image"
2225
+ >
2226
+ <TransitionGroup
2227
+ :key="activeLayout"
2228
+ tag="div"
2229
+ name="lp-row"
2230
+ :css="animating"
2231
+ v-bind="scrollAttrs"
2232
+ class="min-h-0 transition-opacity"
2233
+ :class="[
2234
+ busy && displayRows.length ? 'opacity-60' : '',
2235
+ activeLayout === 'cards'
2236
+ ? 'grid gap-3'
2237
+ : variant === 'framed' ? 'overflow-hidden rounded-card border border-line bg-surface' : '',
2238
+ ]"
2239
+ :style="activeLayout === 'cards' ? { gridTemplateColumns: `repeat(auto-fill, minmax(min(${cardWidth}, 100%), 1fr))` } : undefined"
2240
+ :aria-busy="busy || undefined"
2241
+ :ref="(el: unknown) => roving.setContainer((el as { $el?: HTMLElement })?.$el ?? null)"
2242
+ >
2243
+ <template v-if="displayRows.length === 0 && busy">
2244
+ <div
2245
+ v-for="n in skeletonRows"
2246
+ :key="`lp-table-skeleton-${n}`"
2247
+ class="lp-row-empty"
2248
+ :class="activeLayout === 'cards' ? 'flex flex-col gap-3 rounded-card border border-line bg-surface p-4' : ['flex flex-col gap-2 border-b border-line/60 px-4 last:border-0', cellY]"
2249
+ >
2250
+ <LpSkeleton class="h-3.5" :style="{ width: skeletonWidth(n, 0) }" />
2251
+ <LpSkeleton class="h-3" :style="{ width: skeletonWidth(n, 1) }" />
2252
+ <LpSkeleton v-if="activeLayout === 'cards'" class="h-3" :style="{ width: skeletonWidth(n, 2) }" />
2253
+ <span v-if="n === 1" class="sr-only">{{ text.loading }}</span>
2254
+ </div>
2255
+ </template>
2256
+ <div
2257
+ v-else-if="displayRows.length === 0"
2258
+ key="lp-table-empty"
2259
+ class="lp-row-empty"
2260
+ :class="activeLayout === 'cards' ? 'col-span-full rounded-card border border-line bg-surface' : ''"
2261
+ >
2262
+ <LpEmptyState
2263
+ compact
2264
+ :icon="failed ? 'lucide:circle-alert' : filteredOut ? 'lucide:filter-x' : emptyIcon"
2265
+ :title="failed ? text.error : filteredOut ? text.noMatches : text.empty"
2266
+ >
2267
+ <LpButton v-if="filteredOut" variant="outline" size="sm" @click="resetFilters">
2268
+ {{ text.resetFilters }}
2269
+ </LpButton>
2270
+ </LpEmptyState>
2271
+ </div>
2272
+ <template v-for="entry in allEntries" :key="entry.id">
2273
+ <div
2274
+ v-if="entry.kind === 'group'"
2275
+ class="text-xs font-medium text-muted"
2276
+ :class="activeLayout === 'cards' ? 'col-span-full pt-2' : ['border-b border-line/60 bg-surface-soft/60 px-4', cellY]"
2277
+ >
2278
+ <slot name="group" :value="entry.value" :rows="entry.rows" :label="entry.label" :collapsed="entry.collapsed" :toggle="() => toggleGroup(entry.group)">
2279
+ <button
2280
+ type="button"
2281
+ 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"
2282
+ :aria-expanded="!entry.collapsed"
2283
+ :aria-label="(entry.collapsed ? text.expandGroup : text.collapseGroup).replace('{label}', entry.label)"
2284
+ @click="toggleGroup(entry.group)"
2285
+ >
2286
+ <LpIcon name="lucide:chevron-right" :size="14" class="transition-transform duration-fast motion-reduce:transition-none" :class="entry.collapsed ? '' : 'rotate-90'" />
2287
+ {{ entry.label }}
2288
+ </button>
2289
+ </slot>
2290
+ </div>
2291
+ <div
2292
+ v-else-if="entry.kind === 'detail'"
2293
+ class="px-4 py-3"
2294
+ :class="activeLayout === 'cards' ? 'col-span-full' : 'border-b border-line/60 last:border-0'"
2295
+ data-lp-row-detail
2296
+ >
2297
+ <slot name="expanded" :row="entry.row" :close="() => toggleExpanded(keyFor(entry.row, entry.index))" />
2298
+ </div>
2299
+ <div
2300
+ v-else
2301
+ class="lp-item group relative outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
2302
+ :class="[
2303
+ activeLayout === 'cards'
2304
+ ? ['flex min-w-0 flex-col overflow-hidden rounded-card border bg-surface hover:border-line-strong focus-visible:ring-offset-0', isSelected(keyFor(entry.row, entry.index)) ? 'border-brand bg-brand-soft/30' : 'border-line']
2305
+ : ['flex min-w-0 items-center gap-3 border-b border-line/60 px-4 last:border-0 hover:bg-surface-soft/60 focus-visible:ring-inset data-[active]:bg-surface-soft/40', cellY, isSelected(keyFor(entry.row, entry.index)) ? 'bg-brand-soft/40' : ''],
2306
+ ]"
2307
+ v-bind="roving.itemProps(String(keyFor(entry.row, entry.index)))"
2308
+ :data-paint-key="selectable ? String(keyFor(entry.row, entry.index)) : undefined"
2309
+ :data-lp-context-row="rowMenu ? '' : undefined"
2310
+ @contextmenu="onRowContextMenu(entry.row)"
2311
+ @click="onRowClick(entry.row, keyFor(entry.row, entry.index))"
2312
+ @pointerdown="onRowPointerDown($event, keyFor(entry.row, entry.index))"
2313
+ @keydown="onRowKeydown"
2314
+ >
2315
+ <div
2316
+ v-if="selectable"
2317
+ class="cursor-pointer select-none"
2318
+ :class="activeLayout === 'cards' ? 'absolute top-2 right-2 z-[1]' : ['shrink-0 transition-[margin] duration-medium ease-[var(--ease-emphasized)] motion-reduce:transition-none', ticksShown ? '' : '-mr-3']"
2319
+ @click.stop
2320
+ @pointerdown.stop="paint.onPointerDown($event, keyFor(entry.row, entry.index))"
2321
+ >
2322
+ <div
2323
+ class="lp-tick"
2324
+ :class="activeLayout === 'cards' ? 'lp-tick-card' : ticksShown ? 'lp-tick-line' : ''"
2325
+ :data-cue="activeLayout === 'cards' && !ticksShown ? '' : undefined"
2326
+ :inert="activeLayout !== 'cards' && !ticksShown"
2327
+ >
2328
+ <LpCheckbox
2329
+ :model-value="isSelected(keyFor(entry.row, entry.index))"
2330
+ @update:model-value="(v) => toggleRow(keyFor(entry.row, entry.index), v)"
2331
+ />
2332
+ </div>
2333
+ </div>
2334
+ <!-- `#card` draws the inside of a card; the frame, selection, focus
2335
+ and menu stay the table's, so every card answers alike. Without
2336
+ it the card is built from the columns and their cell slots: the
2337
+ first column names the row, the rest are its facts. -->
2338
+ <slot
2339
+ v-if="activeLayout === 'cards'"
2340
+ name="card"
2341
+ :row="entry.row"
2342
+ :selected="isSelected(keyFor(entry.row, entry.index))"
2343
+ :toggle="() => toggleRow(keyFor(entry.row, entry.index), !isSelected(keyFor(entry.row, entry.index)))"
2344
+ :open-menu="openRowMenu"
2345
+ >
2346
+ <div class="flex min-w-0 flex-col gap-2 p-4">
2347
+ <div v-if="titleColumn" class="min-w-0 truncate pr-6 font-medium text-ink">
2348
+ <slot :name="`cell-${titleColumn.key}`" :row="entry.row" :value="entry.row[titleColumn.key]" :open-menu="openRowMenu">{{ entry.row[titleColumn.key] }}</slot>
2349
+ </div>
2350
+ <dl v-if="fieldColumns.length" class="grid grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-3 gap-y-1 text-sm">
2351
+ <template v-for="col in fieldColumns" :key="col.key">
2352
+ <dt class="text-xs text-muted">{{ col.label }}</dt>
2353
+ <dd class="min-w-0 truncate text-ink">
2354
+ <slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">{{ entry.row[col.key] }}</slot>
2355
+ </dd>
2356
+ </template>
2357
+ </dl>
2358
+ </div>
2359
+ </slot>
2360
+ <div v-else class="min-w-0 flex-1">
2361
+ <div v-if="titleColumn" class="min-w-0 truncate font-medium text-ink">
2362
+ <slot :name="`cell-${titleColumn.key}`" :row="entry.row" :value="entry.row[titleColumn.key]" :open-menu="openRowMenu">{{ entry.row[titleColumn.key] }}</slot>
2363
+ </div>
2364
+ <div v-if="fieldColumns.length" class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted">
2365
+ <template v-for="(col, ci) in fieldColumns" :key="col.key">
2366
+ <span v-if="ci > 0" aria-hidden="true">·</span>
2367
+ <span class="min-w-0 truncate" :title="col.label">
2368
+ <slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">{{ entry.row[col.key] }}</slot>
2369
+ </span>
2370
+ </template>
2371
+ </div>
2372
+ </div>
2373
+ </div>
2374
+ </template>
2375
+ </TransitionGroup>
2376
+ </LpContextMenu>
2019
2377
  </LpShift>
2020
2378
  <LpPagination
2021
2379
  v-if="load && total > pageSize"
@@ -2132,6 +2490,41 @@ tr:hover .lp-tick-cue,
2132
2490
  }
2133
2491
  }
2134
2492
 
2493
+ /*
2494
+ * A list line's tick opens from nothing, as the table's column does, so lines
2495
+ * do not carry an empty gap until selection starts.
2496
+ */
2497
+ .lp-tick-line {
2498
+ max-width: 2rem;
2499
+ opacity: 1;
2500
+ }
2501
+
2502
+ /*
2503
+ * A card's tick sits in its corner at its own size, with no column to hold
2504
+ * open; a cue shows it only while
2505
+ * the card is pointed at or holds focus, the same rule as a row's.
2506
+ */
2507
+ .lp-tick-card {
2508
+ max-width: none;
2509
+ padding-inline: 0;
2510
+ opacity: 1;
2511
+ }
2512
+
2513
+ .lp-tick[data-cue] {
2514
+ opacity: 0;
2515
+ }
2516
+
2517
+ .lp-item:hover .lp-tick[data-cue],
2518
+ .lp-tick[data-cue]:focus-within {
2519
+ opacity: 1;
2520
+ }
2521
+
2522
+ @media (hover: none) {
2523
+ .lp-item:hover .lp-tick[data-cue] {
2524
+ opacity: 0;
2525
+ }
2526
+ }
2527
+
2135
2528
  @media (prefers-reduced-motion: reduce) {
2136
2529
  .lp-row-enter-active,
2137
2530
  .lp-row-leave-active,
@@ -147,3 +147,13 @@ export function presetHidden(
147
147
  // A preset naming nothing that exists must not leave a table with no columns.
148
148
  return hidden.length === columns.length ? new Set() : new Set(hidden)
149
149
  }
150
+
151
+ /**
152
+ * How the rows are drawn. The table's tools (sort, filters, search, selection,
153
+ * the row menu, export, persistence, the palette) are the same in each; only
154
+ * the drawing changes.
155
+ * - `table`: rows under a header.
156
+ * - `cards`: a grid of cards, for rows that are pictures or read best as a whole.
157
+ * - `list`: one compact line per row with no header, for narrow places.
158
+ */
159
+ export type TableLayout = "table" | "cards" | "list"
package/src/index.ts CHANGED
@@ -270,7 +270,7 @@ export { default as LpTable } from "./components/LpTable.vue"
270
270
  export type { SortState, TableColumn, TablePage, TableQuery } from "./components/LpTable.vue"
271
271
  export type { TableLabels } from "./components/LpTable.vue"
272
272
  export type { TableCellEdit, TableExportOptions, TableSelectionToken } from "./components/LpTable.vue"
273
- export type { ColumnPin, ColumnPreset } from "./components/tableFeatures"
273
+ export type { ColumnPin, ColumnPreset, TableLayout } from "./components/tableFeatures"
274
274
  export { compareValues } from "./components/tableSort"
275
275
  export { filterRows, showOnly, shownValues } from "./components/tableFilter"
276
276
  export type { TableFilters } from "./components/tableFilter"