@leavepulse/ui 0.86.2 → 0.87.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/assets/{LpTable-CEFsRz5J.css → LpTable-BqZNpWgC.css} +51 -19
  2. package/dist/chunks/{ContextMenuItems-D15cGEw9.js → ContextMenuItems-D0jObgzN.js} +27 -3
  3. package/dist/chunks/{DropzoneLinkField-Dz7fIaLh.js → DropzoneLinkField-DhwYRnvd.js} +1 -1
  4. package/dist/chunks/{FieldLeading-DZZLK02U.js → FieldLeading-k9EJOKOI.js} +1 -1
  5. package/dist/chunks/{FieldWrapper-BDDw1F9H.js → FieldWrapper-CVTPnpoG.js} +1 -1
  6. package/dist/chunks/{IconCloseButton-Cn_w4aN3.js → IconCloseButton-CmWOJAXG.js} +1 -1
  7. package/dist/chunks/{LayoutCanvas-Dr5eSfAu.js → LayoutCanvas-j4TZqx1A.js} +1 -1
  8. package/dist/chunks/{LayoutNode-CViJnx6q.js → LayoutNode-4Eaf8ZWj.js} +1 -1
  9. package/dist/chunks/{LpActionBar-C0LK-Aqs.js → LpActionBar-Cx_MSZl9.js} +1 -1
  10. package/dist/chunks/{LpAddressInput-B9sSvodU.js → LpAddressInput-CfqQR5jE.js} +7 -7
  11. package/dist/chunks/{LpAlert-Bpd7122M.js → LpAlert-CYtbpRYW.js} +1 -1
  12. package/dist/chunks/{LpAppShell-DpsQtXvG.js → LpAppShell-BQG98odS.js} +4 -4
  13. package/dist/chunks/{LpAutocomplete-CQID7Zc-.js → LpAutocomplete-VNID5zdy.js} +4 -4
  14. package/dist/chunks/{LpAvatar-DLkiMGKx.js → LpAvatar-CfLBqlY1.js} +1 -1
  15. package/dist/chunks/{LpBadge-RFp8uHFt.js → LpBadge-Bh5roBr_.js} +1 -1
  16. package/dist/chunks/{LpBottomNav-BcJVkAhh.js → LpBottomNav-9AqvZlRW.js} +1 -1
  17. package/dist/chunks/{LpBreadcrumbs-DJ_DB45r.js → LpBreadcrumbs-Zv1fn9e_.js} +1 -1
  18. package/dist/chunks/{LpCalendar-D6tfCr2G.js → LpCalendar-aACjUhpM.js} +1 -1
  19. package/dist/chunks/{LpCheckbox-DcXC2qd5.js → LpCheckbox-P7tEw8GO.js} +2 -2
  20. package/dist/chunks/{LpCodeBlock-kGT15Zou.js → LpCodeBlock-BNThgUdQ.js} +2 -2
  21. package/dist/chunks/{LpCodeDisplay-DlwzZo9o.js → LpCodeDisplay-DsiE-Qy9.js} +4 -4
  22. package/dist/chunks/{LpColorPicker-Cjna_Bio.js → LpColorPicker-EXpwcX-t.js} +3 -3
  23. package/dist/chunks/{LpConfirmDialog-bfcX5F9d.js → LpConfirmDialog-DPiPVRn_.js} +2 -2
  24. package/dist/chunks/{LpConfirmHost-DIgloymi.js → LpConfirmHost-Ddiljlog.js} +1 -1
  25. package/dist/chunks/{LpContextMenu-CUFZIQX2.js → LpContextMenu-DYpDm6iI.js} +2 -2
  26. package/dist/chunks/{LpContextSwitcher-CMweLbv2.js → LpContextSwitcher-BPDSkKyA.js} +2 -2
  27. package/dist/chunks/{LpCopyButton-TGoE43mE.js → LpCopyButton-CxX682t4.js} +1 -1
  28. package/dist/chunks/{LpDataList-Due909Cy.js → LpDataList-DCtF7krm.js} +2 -2
  29. package/dist/chunks/{LpDatePicker-B5gzwI3T.js → LpDatePicker-BElEHVm6.js} +5 -5
  30. package/dist/chunks/{LpDateRangePicker-CwNnpyoL.js → LpDateRangePicker-vLt-drEu.js} +3 -3
  31. package/dist/chunks/{LpDisclosure-CsJ7nnFz.js → LpDisclosure-D4JZ-o8K.js} +1 -1
  32. package/dist/chunks/{LpDrawer-C1q2bl1r.js → LpDrawer-C97k-3L2.js} +1 -1
  33. package/dist/chunks/{LpDropdownMenu-DRABI-Bl.js → LpDropdownMenu-BPEsqZCN.js} +2 -2
  34. package/dist/chunks/{LpEditable-DCPdDDBb.js → LpEditable-CeRAzYMv.js} +2 -2
  35. package/dist/chunks/{LpEmptyState-DT8U4R8L.js → LpEmptyState-DrHBR3yh.js} +1 -1
  36. package/dist/chunks/{LpFieldNotice-mcmO-O-J.js → LpFieldNotice-DUPo1Yii.js} +1 -1
  37. package/dist/chunks/{LpFileTree-B365yOLv.js → LpFileTree-BcQgsw4e.js} +4 -4
  38. package/dist/chunks/{LpFileTreeNode-DfvIGmSz.js → LpFileTreeNode-Dk2WqkPF.js} +4 -4
  39. package/dist/chunks/{LpFormField-C5GOuOY-.js → LpFormField-jbM7K1Hg.js} +1 -1
  40. package/dist/chunks/{LpHintLayer-B0choo1r.js → LpHintLayer-CUSRLtLu.js} +1 -1
  41. package/dist/chunks/{LpHints-BEetUVTl.js → LpHints-BF40CP4_.js} +3 -3
  42. package/dist/chunks/{LpIcon-DCGhNsCU.js → LpIcon-CxHFPwKt.js} +1 -1
  43. package/dist/chunks/{LpInfraNode-BsExhdPE.js → LpInfraNode-BYiMFDnA.js} +2 -2
  44. package/dist/chunks/{LpInput-CKMblRtp.js → LpInput-BU5IsOlE.js} +3 -3
  45. package/dist/chunks/{LpInspector-BNxjuWMP.js → LpInspector-CEko6-pS.js} +4 -4
  46. package/dist/chunks/{LpLightbox-DVyiB4dw.js → LpLightbox-REei-y5e.js} +4 -4
  47. package/dist/chunks/{LpLogInspector-Yj310k4n.js → LpLogInspector-Bwz351LY.js} +2 -2
  48. package/dist/chunks/{LpLogViewer-Bki-IdNC.js → LpLogViewer-BCkqdH66.js} +6 -6
  49. package/dist/chunks/{LpMapPicker-CnpJhiY0.js → LpMapPicker-DVfiy3rR.js} +1 -1
  50. package/dist/chunks/{LpMediaGallery-OJemmbV7.js → LpMediaGallery-BF1Bch3v.js} +4 -4
  51. package/dist/chunks/{LpModal-B5y9_LgA.js → LpModal-BK2T5uIG.js} +1 -1
  52. package/dist/chunks/{LpModalNav-Er99S_AP.js → LpModalNav-Dk4eHpg6.js} +3 -3
  53. package/dist/chunks/{LpNotificationBell-DmR1xDd-.js → LpNotificationBell-CMwUXhFg.js} +4 -4
  54. package/dist/chunks/{LpNotificationFeed-DK7D1f-v.js → LpNotificationFeed-BXsRp9jm.js} +4 -4
  55. package/dist/chunks/{LpNumberField-BeYT7zzt.js → LpNumberField-QUMXvpqx.js} +3 -3
  56. package/dist/chunks/{LpOtpInput-DqwY-U5R.js → LpOtpInput-C14XagVB.js} +1 -1
  57. package/dist/chunks/{LpPagination-V1wEhu1K.js → LpPagination-DGLntZJN.js} +1 -1
  58. package/dist/chunks/{LpPaletteTheme-CWFz4H2D.js → LpPaletteTheme-wiJtP5hV.js} +2 -2
  59. package/dist/chunks/{LpPasswordInput-LaOXNYuV.js → LpPasswordInput-W9AZfzaL.js} +2 -2
  60. package/dist/chunks/{LpPhoneInput-D7Tq2iaz.js → LpPhoneInput-DsRv-Qla.js} +3 -3
  61. package/dist/chunks/{LpPie-CAsGO9jE.js → LpPie-pbrNFbRP.js} +2 -2
  62. package/dist/chunks/{LpPipeline-D-uPcbMN.js → LpPipeline-BHWmvOiv.js} +2 -2
  63. package/dist/chunks/{LpQuota-BR8ck2a9.js → LpQuota-BaCq5YOQ.js} +1 -1
  64. package/dist/chunks/{LpRadioGroup-DSOCL9MD.js → LpRadioGroup-D94KMStE.js} +2 -2
  65. package/dist/chunks/{LpRangeCalendar-QepHrmba.js → LpRangeCalendar-C9gkNMYw.js} +1 -1
  66. package/dist/chunks/{LpRangeSlider-BaAM7XIs.js → LpRangeSlider-CkP9hYUC.js} +1 -1
  67. package/dist/chunks/{LpRepeater-BKq5XPh4.js → LpRepeater-DLLcbQ6J.js} +2 -2
  68. package/dist/chunks/{LpSecretField-Dp-c7wWc.js → LpSecretField-Au7ZQTtm.js} +5 -5
  69. package/dist/chunks/{LpSection-BGzlKIWO.js → LpSection-RKCwB0xN.js} +1 -1
  70. package/dist/chunks/{LpSegmented-CiaF9DUK.js → LpSegmented-DD8MPj4j.js} +2 -2
  71. package/dist/chunks/{LpSelect-CB6Xbnfk.js → LpSelect-BeXz1TWh.js} +4 -4
  72. package/dist/chunks/{LpSidebar-CjE90o3s.js → LpSidebar-DUFlf6Uu.js} +4 -4
  73. package/dist/chunks/{LpSidebarNav-Bgz2tXSq.js → LpSidebarNav-C34YEZIR.js} +3 -3
  74. package/dist/chunks/{LpSidebarRow-CDBnvvd8.js → LpSidebarRow-BeEGgsZi.js} +3 -3
  75. package/dist/chunks/{LpSlider-DTb_Ivkm.js → LpSlider-BCy5rjwK.js} +1 -1
  76. package/dist/chunks/{LpStat-DM5Wfb6K.js → LpStat-eVz7iUfD.js} +2 -2
  77. package/dist/chunks/{LpStatus-_fIwWXQV.js → LpStatus-C6GqSHJj.js} +1 -1
  78. package/dist/chunks/{LpStepMark-iDZw1DYf.js → LpStepMark-CPGVj1XG.js} +1 -1
  79. package/dist/chunks/{LpStepper-CjC3cp1F.js → LpStepper-DBNRA6Q2.js} +1 -1
  80. package/dist/chunks/{LpSwitch-BNHyYDhX.js → LpSwitch-D7hmVsif.js} +1 -1
  81. package/dist/chunks/{LpTabFace-gATEe9pQ.js → LpTabFace-uoZgD_SN.js} +2 -2
  82. package/dist/chunks/{LpTableFilter-B_BFoBqS.js → LpTableFilter-BGFNKyUX.js} +2 -2
  83. package/dist/chunks/{LpTagsInput-By7LMb47.js → LpTagsInput-D9uOSlvf.js} +5 -5
  84. package/dist/chunks/{LpTextarea-Ck6RAfzu.js → LpTextarea-DwE-lQt4.js} +1 -1
  85. package/dist/chunks/{LpThemeStudio-RyI-SWiw.js → LpThemeStudio-COn2H76d.js} +5 -5
  86. package/dist/chunks/{LpThemeSwitcher-BZYJ0AXy.js → LpThemeSwitcher-B3R3nNid.js} +3 -3
  87. package/dist/chunks/{LpTimeField-BTKJVeNt.js → LpTimeField-Mi8H1g_9.js} +2 -2
  88. package/dist/chunks/{LpToaster-YcLJFSsH.js → LpToaster-DOJn6Vey.js} +2 -2
  89. package/dist/chunks/{LpTopologyCanvas-paC1Dup-.js → LpTopologyCanvas-5RFMP4gR.js} +4 -4
  90. package/dist/chunks/{LpTour-D_AN_SsZ.js → LpTour-DUI_hoPv.js} +1 -1
  91. package/dist/chunks/{LpTruncate-DCZjfcE-.js → LpTruncate-CynJhsEw.js} +1 -1
  92. package/dist/chunks/{LpVideoCompress-BfulQ8Rs.js → LpVideoCompress-CPUxXh-c.js} +1 -1
  93. package/dist/chunks/{LpVideoPlayer--87I5Kqu.js → LpVideoPlayer-BbCjEQAx.js} +4 -4
  94. package/dist/chunks/{LpWindow-Bo5qQgVY.js → LpWindow-Dp_0nNqg.js} +2 -2
  95. package/dist/chunks/{LpWorkspaceSwitcher-D50sHs9N.js → LpWorkspaceSwitcher-bjkncjSl.js} +3 -3
  96. package/dist/chunks/{MenuTitle-DCHcfyPH.js → MenuTitle-YcSjF8xo.js} +1 -1
  97. package/dist/chunks/{OptionContent-o58kS7Zh.js → OptionContent-DZb5eKRV.js} +1 -1
  98. package/dist/chunks/{PickerField-DNo1k89A.js → PickerField-CjOTUXDK.js} +1 -1
  99. package/dist/chunks/{RevealToggle-w7-BMcgC.js → RevealToggle-Bl1NcfxB.js} +1 -1
  100. package/dist/chunks/{TourCard-0yYxSdd4.js → TourCard-BovXtMbO.js} +1 -1
  101. package/dist/chunks/{VideoThumb-DL_QkAJY.js → VideoThumb-BfDkIXqE.js} +1 -1
  102. package/dist/chunks/{VideoVolume-D-2TmsVj.js → VideoVolume-DE3i1ree.js} +2 -2
  103. package/dist/chunks/{icons-V0k93Puc.js → icons-C8ccRCKw.js} +1 -1
  104. package/dist/components/ContextMenuItems.vue.js +1 -1
  105. package/dist/components/DropzoneLinkField.vue.js +1 -1
  106. package/dist/components/FieldLeading.vue.js +1 -1
  107. package/dist/components/FieldWrapper.vue.js +1 -1
  108. package/dist/components/IconCloseButton.vue.js +1 -1
  109. package/dist/components/LayoutCanvas.vue.js +1 -1
  110. package/dist/components/LayoutNode.vue.js +1 -1
  111. package/dist/components/LpActionBar.vue.js +1 -1
  112. package/dist/components/LpAddressInput.vue.js +1 -1
  113. package/dist/components/LpAlert.vue.js +1 -1
  114. package/dist/components/LpAppShell.vue.js +1 -1
  115. package/dist/components/LpAutocomplete.vue.js +1 -1
  116. package/dist/components/LpAvatar.vue.js +1 -1
  117. package/dist/components/LpBadge.vue.js +1 -1
  118. package/dist/components/LpBottomNav.vue.js +1 -1
  119. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  120. package/dist/components/LpCalendar.vue.js +1 -1
  121. package/dist/components/LpChart.vue.js +2 -2
  122. package/dist/components/LpCheckbox.vue.js +1 -1
  123. package/dist/components/LpCodeBlock.vue.js +1 -1
  124. package/dist/components/LpCodeDisplay.vue.js +1 -1
  125. package/dist/components/LpColorPicker.vue.js +1 -1
  126. package/dist/components/LpCommandPalette.vue.js +2 -2
  127. package/dist/components/LpConfirmDialog.vue.js +1 -1
  128. package/dist/components/LpConfirmHost.vue.js +1 -1
  129. package/dist/components/LpContextMenu.vue.d.ts +7 -0
  130. package/dist/components/LpContextMenu.vue.js +1 -1
  131. package/dist/components/LpContextSwitcher.vue.js +1 -1
  132. package/dist/components/LpCopyButton.vue.js +1 -1
  133. package/dist/components/LpDataList.vue.js +1 -1
  134. package/dist/components/LpDatePicker.vue.js +1 -1
  135. package/dist/components/LpDateRangePicker.vue.js +1 -1
  136. package/dist/components/LpDisclosure.vue.js +1 -1
  137. package/dist/components/LpDock.vue.js +1 -1
  138. package/dist/components/LpDrawer.vue.js +1 -1
  139. package/dist/components/LpDropdownMenu.vue.js +1 -1
  140. package/dist/components/LpDropzone.vue.js +2 -2
  141. package/dist/components/LpEditable.vue.js +1 -1
  142. package/dist/components/LpEmptyState.vue.js +1 -1
  143. package/dist/components/LpFieldNotice.vue.js +1 -1
  144. package/dist/components/LpFileTree.vue.js +1 -1
  145. package/dist/components/LpFileTreeNode.vue.js +1 -1
  146. package/dist/components/LpFormField.vue.js +1 -1
  147. package/dist/components/LpHintLayer.vue.js +1 -1
  148. package/dist/components/LpHints.vue.js +1 -1
  149. package/dist/components/LpIcon.vue.js +1 -1
  150. package/dist/components/LpImageEditor.vue.js +1 -1
  151. package/dist/components/LpInfraNode.vue.js +1 -1
  152. package/dist/components/LpInput.vue.js +1 -1
  153. package/dist/components/LpInspector.vue.js +1 -1
  154. package/dist/components/LpLightbox.vue.js +1 -1
  155. package/dist/components/LpLogInspector.vue.js +1 -1
  156. package/dist/components/LpLogViewer.vue.js +1 -1
  157. package/dist/components/LpMapPicker.vue.js +1 -1
  158. package/dist/components/LpMediaGallery.vue.js +1 -1
  159. package/dist/components/LpModal.vue.js +1 -1
  160. package/dist/components/LpModalNav.vue.js +1 -1
  161. package/dist/components/LpNotificationBell.vue.js +1 -1
  162. package/dist/components/LpNotificationFeed.vue.js +1 -1
  163. package/dist/components/LpNumberField.vue.js +1 -1
  164. package/dist/components/LpOtpInput.vue.js +1 -1
  165. package/dist/components/LpPagination.vue.js +1 -1
  166. package/dist/components/LpPaletteFacts.vue.js +1 -1
  167. package/dist/components/LpPaletteHeader.vue.js +2 -2
  168. package/dist/components/LpPaletteTheme.vue.js +1 -1
  169. package/dist/components/LpPasswordInput.vue.js +1 -1
  170. package/dist/components/LpPhoneInput.vue.js +1 -1
  171. package/dist/components/LpPie.vue.js +1 -1
  172. package/dist/components/LpPipeline.vue.js +1 -1
  173. package/dist/components/LpQuota.vue.js +1 -1
  174. package/dist/components/LpRadioGroup.vue.js +1 -1
  175. package/dist/components/LpRangeCalendar.vue.js +1 -1
  176. package/dist/components/LpRangeSlider.vue.js +1 -1
  177. package/dist/components/LpRepeater.vue.js +1 -1
  178. package/dist/components/LpSecretField.vue.js +1 -1
  179. package/dist/components/LpSection.vue.js +1 -1
  180. package/dist/components/LpSegmented.vue.js +1 -1
  181. package/dist/components/LpSelect.vue.js +1 -1
  182. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  183. package/dist/components/LpSidebar.vue.js +1 -1
  184. package/dist/components/LpSidebarNav.vue.js +1 -1
  185. package/dist/components/LpSidebarRow.vue.js +1 -1
  186. package/dist/components/LpSlider.vue.js +1 -1
  187. package/dist/components/LpStat.vue.js +1 -1
  188. package/dist/components/LpStatus.vue.js +1 -1
  189. package/dist/components/LpStepMark.vue.js +1 -1
  190. package/dist/components/LpStepper.vue.js +1 -1
  191. package/dist/components/LpSwitch.vue.js +1 -1
  192. package/dist/components/LpTabFace.vue.js +1 -1
  193. package/dist/components/LpTable.vue.d.ts +76 -1
  194. package/dist/components/LpTable.vue.js +516 -102
  195. package/dist/components/LpTableFilter.vue.js +1 -1
  196. package/dist/components/LpTabs.vue.js +2 -2
  197. package/dist/components/LpTagsInput.vue.js +1 -1
  198. package/dist/components/LpTextarea.vue.js +1 -1
  199. package/dist/components/LpThemeStudio.vue.js +1 -1
  200. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  201. package/dist/components/LpTimeField.vue.js +1 -1
  202. package/dist/components/LpToaster.vue.js +1 -1
  203. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  204. package/dist/components/LpTour.vue.js +1 -1
  205. package/dist/components/LpTruncate.vue.js +1 -1
  206. package/dist/components/LpVideoCompress.vue.js +1 -1
  207. package/dist/components/LpVideoPlayer.vue.js +1 -1
  208. package/dist/components/LpVideoTrim.vue.js +4 -4
  209. package/dist/components/LpWindow.vue.js +1 -1
  210. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  211. package/dist/components/MenuTitle.vue.js +1 -1
  212. package/dist/components/OptionContent.vue.js +1 -1
  213. package/dist/components/PickerField.vue.js +1 -1
  214. package/dist/components/RevealToggle.vue.js +1 -1
  215. package/dist/components/TourCard.vue.js +1 -1
  216. package/dist/components/VideoFlash.vue.js +1 -1
  217. package/dist/components/VideoThumb.vue.js +1 -1
  218. package/dist/components/VideoVolume.vue.js +1 -1
  219. package/dist/components/menuWhen.js +1 -1
  220. package/dist/components/tableFeatures.d.ts +9 -0
  221. package/dist/icons-all.js +1 -1
  222. package/dist/index.d.ts +1 -1
  223. package/dist/index.js +90 -85
  224. package/package.json +1 -1
  225. package/src/components/ContextMenuItems.vue +22 -0
  226. package/src/components/LpContextMenu.vue +7 -0
  227. package/src/components/LpTable.vue +401 -3
  228. package/src/components/tableFeatures.ts +10 -0
  229. package/src/index.ts +1 -1
  230. package/src/kit-icons.ts +1 -1
  231. 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,102 @@ 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
+ // While the width forces a list the switch would change nothing on screen, so
1477
+ // it steps aside until there is room for the choice again.
1478
+ const forcedList = computed(() => chosenLayout.value === "table" && activeLayout.value === "list")
1479
+ const layoutChoices = computed(() =>
1480
+ props.layouts && props.layouts.length > 1 && !forcedList.value ? props.layouts : [],
1481
+ )
1482
+ const LAYOUT_ICONS: Record<TableLayout, string> = {
1483
+ table: "lucide:table-2",
1484
+ cards: "lucide:layout-grid",
1485
+ list: "lucide:list",
1486
+ }
1487
+ const layoutOptions = computed(() =>
1488
+ layoutChoices.value.map((value) => {
1489
+ const label = text.value[`layout${value[0]!.toUpperCase()}${value.slice(1)}`] ?? value
1490
+ return { value, icon: LAYOUT_ICONS[value], ariaLabel: label }
1491
+ }),
1492
+ )
1493
+
1494
+ function chooseLayout(next: string) {
1495
+ const layout = next as TableLayout
1496
+ if (layout === chosenLayout.value) return
1497
+ ownLayout.value = layout
1498
+ emit("update:layout", layout)
1499
+ }
1500
+
1501
+ // Without a header the sort and the column filters need a home: the bar.
1502
+ const headerless = computed(() => activeLayout.value !== "table")
1503
+ const sortableColumns = computed(() => props.columns.filter((col) => col.sortable && col.label))
1504
+ const filterableColumns = computed(() => props.columns.filter((col) => col.filterable && col.label))
1505
+
1506
+ function sortMenu(): ContextMenuItemDef[] {
1507
+ const current = activeSort.value
1508
+ const dir = current?.dir ?? "asc"
1509
+ return [
1510
+ // The columns, the one sorted by ticked; picking one keeps the direction.
1511
+ ...sortableColumns.value.map((col): ContextMenuItemDef => ({
1512
+ label: col.label,
1513
+ selected: current?.key === col.key,
1514
+ onSelect: () => writeSorts([{ key: col.key, dir }]),
1515
+ })),
1516
+ {
1517
+ label: text.value.sortAsc,
1518
+ icon: "lucide:arrow-up",
1519
+ separatorBefore: true,
1520
+ disabled: !current,
1521
+ selected: current?.dir === "asc",
1522
+ onSelect: () => current && writeSorts([{ key: current.key, dir: "asc" }]),
1523
+ },
1524
+ {
1525
+ label: text.value.sortDesc,
1526
+ icon: "lucide:arrow-down",
1527
+ disabled: !current,
1528
+ selected: current?.dir === "desc",
1529
+ onSelect: () => current && writeSorts([{ key: current.key, dir: "desc" }]),
1530
+ },
1531
+ { label: text.value.sortClear, icon: "lucide:x", separatorBefore: true, disabled: !current, onSelect: () => writeSorts([]) },
1532
+ ]
1533
+ }
1534
+
1535
+ const sortSummary = computed(() => {
1536
+ const current = activeSort.value
1537
+ const col = current && props.columns.find((c) => c.key === current.key)
1538
+ return col ? col.label : text.value.sortBy
1539
+ })
1540
+
1541
+ // A card shows the columns the user keeps, not the ones a narrow table drops:
1542
+ // the card has its own room, and `hideBelow` was a decision about a row.
1543
+ const cardColumns = computed(() =>
1544
+ orderPinned(props.columns.filter((col) => !hiddenColumns.value.has(col.key))),
1545
+ )
1546
+ const titleColumn = computed(() => cardColumns.value[0])
1547
+ const fieldColumns = computed(() => cardColumns.value.slice(1).filter((col) => col.label))
1548
+
1549
+ watch(chosenLayout, (layout) => {
1550
+ if (storageKey.value && restored.value) store.set(`${storageKey.value}:layout`, layout)
1551
+ })
1552
+
1404
1553
  /*
1405
1554
  * Skeleton bars of uneven width, the way real cells are: a column of identical
1406
1555
  * bars reads as a striped background rather than as rows of text. Fixed per
@@ -1484,7 +1633,7 @@ watch(viewportEl, (el, _, onCleanup) => {
1484
1633
  useResizeObserver(viewportEl, (_, el) => (viewportHeight.value = el.clientHeight), { immediate: true })
1485
1634
 
1486
1635
  const rowWindowed = computed(() =>
1487
- props.virtualRowHeight
1636
+ props.virtualRowHeight && !headerless.value
1488
1637
  ? rowWindow(scrollTop.value, viewportHeight.value, props.virtualRowHeight, allEntries.value.length, props.virtualOverscan)
1489
1638
  : null,
1490
1639
  )
@@ -1627,6 +1776,47 @@ usePaletteCommands(() => {
1627
1776
  <div v-if="$slots.toolbar" class="flex min-w-0 flex-wrap items-center gap-2 text-sm">
1628
1777
  <slot name="toolbar" />
1629
1778
  </div>
1779
+ <!-- Without a header the sort and the column funnels live here: the same
1780
+ menu and the same filter, reached from the bar instead. -->
1781
+ <div v-if="headerless && (sortableColumns.length || filterableColumns.length)" class="flex flex-wrap items-center gap-1 text-sm">
1782
+ <LpDropdownMenu v-if="sortableColumns.length" :items="sortMenu()">
1783
+ <template #trigger>
1784
+ <LpButton variant="ghost" size="sm" :aria-label="text.sortBy">
1785
+ <LpIcon
1786
+ :name="activeSort ? (activeSort.dir === 'asc' ? 'lucide:arrow-up' : 'lucide:arrow-down') : 'lucide:arrow-up-down'"
1787
+ :size="14"
1788
+ />
1789
+ {{ sortSummary }}
1790
+ </LpButton>
1791
+ </template>
1792
+ </LpDropdownMenu>
1793
+ <LpPopover
1794
+ v-for="col in filterableColumns"
1795
+ :key="col.key"
1796
+ align="start"
1797
+ panel-class="w-72"
1798
+ :aria-label="text.filterColumn.replace('{column}', col.label)"
1799
+ >
1800
+ <template #trigger>
1801
+ <LpButton
1802
+ variant="ghost"
1803
+ size="sm"
1804
+ :class="hiddenValues[col.key]?.size ? 'text-brand' : ''"
1805
+ :aria-label="text.filterColumn.replace('{column}', col.label)"
1806
+ :data-lp-filter="col.key"
1807
+ >
1808
+ <LpIcon :name="hiddenValues[col.key]?.size ? 'lucide:filter-x' : 'lucide:filter'" :size="14" />
1809
+ {{ col.label }}
1810
+ </LpButton>
1811
+ </template>
1812
+ <LpTableFilter
1813
+ :options="facetOptions.get(col.key)?.value ?? []"
1814
+ :hidden="hiddenValues[col.key] ?? new Set()"
1815
+ :labels="filterLabels()"
1816
+ @update:hidden="(next) => setHidden(col.key, next)"
1817
+ />
1818
+ </LpPopover>
1819
+ </div>
1630
1820
  <div v-if="activeFilters.length" class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
1631
1821
  <LpBadge
1632
1822
  v-for="f in activeFilters"
@@ -1696,6 +1886,14 @@ usePaletteCommands(() => {
1696
1886
  <slot name="bulk" :selected="selected" :all-matching="allMatching" :clear="clearSelection" />
1697
1887
  </span>
1698
1888
  </Transition>
1889
+ <LpSegmented
1890
+ v-if="layoutOptions.length"
1891
+ :model-value="chosenLayout"
1892
+ :options="layoutOptions"
1893
+ size="sm"
1894
+ :aria-label="text.layoutSwitch"
1895
+ @update:model-value="chooseLayout"
1896
+ />
1699
1897
  <LpButton v-if="exportable" variant="ghost" size="sm" @click="exportCsv()">
1700
1898
  <LpIcon name="lucide:download" :size="14" />
1701
1899
  {{ text.exportCsv }}
@@ -1718,7 +1916,9 @@ usePaletteCommands(() => {
1718
1916
  <LpScrollArea
1719
1917
  v-bind="scrollAttrs"
1720
1918
  :fade="false"
1721
- class="min-h-0 flex-1 rounded-card border border-line bg-surface"
1919
+ v-if="!headerless"
1920
+ class="min-h-0 flex-1"
1921
+ :class="variant === 'framed' ? 'rounded-card border border-line bg-surface' : ''"
1722
1922
  :style="load && floor ? { minHeight: `${floor + 2}px` } : undefined"
1723
1923
  :bar-inset-top="barInsetTop"
1724
1924
  >
@@ -1790,7 +1990,7 @@ usePaletteCommands(() => {
1790
1990
  <LpContextMenu v-if="col.sortable || col.filterable" :items="headerMenu(col)">
1791
1991
  <button
1792
1992
  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"
1993
+ 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
1994
  :class="[
1795
1995
  col.align === 'right' ? 'flex-row-reverse' : '',
1796
1996
  sortOf(col) ? 'text-ink' : '',
@@ -2016,6 +2216,169 @@ usePaletteCommands(() => {
2016
2216
  </table>
2017
2217
  </LpContextMenu>
2018
2218
  </LpScrollArea>
2219
+ <!-- Cards and list: the same rows, menu, selection and keyboard, drawn
2220
+ without a header. The one menu wraps the lot, as it does the table. -->
2221
+ <LpContextMenu
2222
+ v-else
2223
+ :items="contextMenu"
2224
+ always
2225
+ :no-hint="!rowMenu"
2226
+ :title="menuHead?.title"
2227
+ :subtitle="menuHead?.subtitle"
2228
+ :title-icon="menuHead?.icon"
2229
+ :title-image="menuHead?.image"
2230
+ >
2231
+ <TransitionGroup
2232
+ :key="activeLayout"
2233
+ tag="div"
2234
+ name="lp-row"
2235
+ :css="animating"
2236
+ v-bind="scrollAttrs"
2237
+ class="min-h-0 transition-opacity"
2238
+ :class="[
2239
+ busy && displayRows.length ? 'opacity-60' : '',
2240
+ activeLayout === 'cards'
2241
+ ? 'grid gap-3'
2242
+ : variant === 'framed' ? 'overflow-hidden rounded-card border border-line bg-surface' : '',
2243
+ ]"
2244
+ :style="activeLayout === 'cards' ? { gridTemplateColumns: `repeat(auto-fill, minmax(min(${cardWidth}, 100%), 1fr))` } : undefined"
2245
+ :aria-busy="busy || undefined"
2246
+ :ref="(el: unknown) => roving.setContainer((el as { $el?: HTMLElement })?.$el ?? null)"
2247
+ >
2248
+ <template v-if="displayRows.length === 0 && busy">
2249
+ <div
2250
+ v-for="n in skeletonRows"
2251
+ :key="`lp-table-skeleton-${n}`"
2252
+ class="lp-row-empty"
2253
+ :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]"
2254
+ >
2255
+ <LpSkeleton class="h-3.5" :style="{ width: skeletonWidth(n, 0) }" />
2256
+ <LpSkeleton class="h-3" :style="{ width: skeletonWidth(n, 1) }" />
2257
+ <LpSkeleton v-if="activeLayout === 'cards'" class="h-3" :style="{ width: skeletonWidth(n, 2) }" />
2258
+ <span v-if="n === 1" class="sr-only">{{ text.loading }}</span>
2259
+ </div>
2260
+ </template>
2261
+ <div
2262
+ v-else-if="displayRows.length === 0"
2263
+ key="lp-table-empty"
2264
+ class="lp-row-empty"
2265
+ :class="activeLayout === 'cards' ? 'col-span-full rounded-card border border-line bg-surface' : ''"
2266
+ >
2267
+ <LpEmptyState
2268
+ compact
2269
+ :icon="failed ? 'lucide:circle-alert' : filteredOut ? 'lucide:filter-x' : emptyIcon"
2270
+ :title="failed ? text.error : filteredOut ? text.noMatches : text.empty"
2271
+ >
2272
+ <LpButton v-if="filteredOut" variant="outline" size="sm" @click="resetFilters">
2273
+ {{ text.resetFilters }}
2274
+ </LpButton>
2275
+ </LpEmptyState>
2276
+ </div>
2277
+ <template v-for="entry in allEntries" :key="entry.id">
2278
+ <div
2279
+ v-if="entry.kind === 'group'"
2280
+ class="text-xs font-medium text-muted"
2281
+ :class="activeLayout === 'cards' ? 'col-span-full pt-2' : ['border-b border-line/60 bg-surface-soft/60 px-4', cellY]"
2282
+ >
2283
+ <slot name="group" :value="entry.value" :rows="entry.rows" :label="entry.label" :collapsed="entry.collapsed" :toggle="() => toggleGroup(entry.group)">
2284
+ <button
2285
+ type="button"
2286
+ 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"
2287
+ :aria-expanded="!entry.collapsed"
2288
+ :aria-label="(entry.collapsed ? text.expandGroup : text.collapseGroup).replace('{label}', entry.label)"
2289
+ @click="toggleGroup(entry.group)"
2290
+ >
2291
+ <LpIcon name="lucide:chevron-right" :size="14" class="transition-transform duration-fast motion-reduce:transition-none" :class="entry.collapsed ? '' : 'rotate-90'" />
2292
+ {{ entry.label }}
2293
+ </button>
2294
+ </slot>
2295
+ </div>
2296
+ <div
2297
+ v-else-if="entry.kind === 'detail'"
2298
+ class="px-4 py-3"
2299
+ :class="activeLayout === 'cards' ? 'col-span-full' : 'border-b border-line/60 last:border-0'"
2300
+ data-lp-row-detail
2301
+ >
2302
+ <slot name="expanded" :row="entry.row" :close="() => toggleExpanded(keyFor(entry.row, entry.index))" />
2303
+ </div>
2304
+ <div
2305
+ v-else
2306
+ class="lp-item group relative outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
2307
+ :class="[
2308
+ activeLayout === 'cards'
2309
+ ? ['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']
2310
+ : ['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' : ''],
2311
+ ]"
2312
+ v-bind="roving.itemProps(String(keyFor(entry.row, entry.index)))"
2313
+ :data-paint-key="selectable ? String(keyFor(entry.row, entry.index)) : undefined"
2314
+ :data-lp-context-row="rowMenu ? '' : undefined"
2315
+ @contextmenu="onRowContextMenu(entry.row)"
2316
+ @click="onRowClick(entry.row, keyFor(entry.row, entry.index))"
2317
+ @pointerdown="onRowPointerDown($event, keyFor(entry.row, entry.index))"
2318
+ @keydown="onRowKeydown"
2319
+ >
2320
+ <div
2321
+ v-if="selectable"
2322
+ class="cursor-pointer select-none"
2323
+ :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']"
2324
+ @click.stop
2325
+ @pointerdown.stop="paint.onPointerDown($event, keyFor(entry.row, entry.index))"
2326
+ >
2327
+ <div
2328
+ class="lp-tick"
2329
+ :class="activeLayout === 'cards' ? 'lp-tick-card' : ticksShown ? 'lp-tick-line' : ''"
2330
+ :data-cue="activeLayout === 'cards' && !ticksShown ? '' : undefined"
2331
+ :inert="activeLayout !== 'cards' && !ticksShown"
2332
+ >
2333
+ <LpCheckbox
2334
+ :model-value="isSelected(keyFor(entry.row, entry.index))"
2335
+ @update:model-value="(v) => toggleRow(keyFor(entry.row, entry.index), v)"
2336
+ />
2337
+ </div>
2338
+ </div>
2339
+ <!-- `#card` draws the inside of a card; the frame, selection, focus
2340
+ and menu stay the table's, so every card answers alike. Without
2341
+ it the card is built from the columns and their cell slots: the
2342
+ first column names the row, the rest are its facts. -->
2343
+ <slot
2344
+ v-if="activeLayout === 'cards'"
2345
+ name="card"
2346
+ :row="entry.row"
2347
+ :selected="isSelected(keyFor(entry.row, entry.index))"
2348
+ :toggle="() => toggleRow(keyFor(entry.row, entry.index), !isSelected(keyFor(entry.row, entry.index)))"
2349
+ :open-menu="openRowMenu"
2350
+ >
2351
+ <div class="flex min-w-0 flex-col gap-2 p-4">
2352
+ <div v-if="titleColumn" class="min-w-0 truncate pr-6 font-medium text-ink">
2353
+ <slot :name="`cell-${titleColumn.key}`" :row="entry.row" :value="entry.row[titleColumn.key]" :open-menu="openRowMenu">{{ entry.row[titleColumn.key] }}</slot>
2354
+ </div>
2355
+ <dl v-if="fieldColumns.length" class="grid grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-3 gap-y-1 text-sm">
2356
+ <template v-for="col in fieldColumns" :key="col.key">
2357
+ <dt class="text-xs text-muted">{{ col.label }}</dt>
2358
+ <dd class="min-w-0 truncate text-ink">
2359
+ <slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">{{ entry.row[col.key] }}</slot>
2360
+ </dd>
2361
+ </template>
2362
+ </dl>
2363
+ </div>
2364
+ </slot>
2365
+ <div v-else class="min-w-0 flex-1">
2366
+ <div v-if="titleColumn" class="min-w-0 truncate font-medium text-ink">
2367
+ <slot :name="`cell-${titleColumn.key}`" :row="entry.row" :value="entry.row[titleColumn.key]" :open-menu="openRowMenu">{{ entry.row[titleColumn.key] }}</slot>
2368
+ </div>
2369
+ <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">
2370
+ <template v-for="(col, ci) in fieldColumns" :key="col.key">
2371
+ <span v-if="ci > 0" aria-hidden="true">·</span>
2372
+ <span class="min-w-0 truncate" :title="col.label">
2373
+ <slot :name="`cell-${col.key}`" :row="entry.row" :value="entry.row[col.key]" :open-menu="openRowMenu">{{ entry.row[col.key] }}</slot>
2374
+ </span>
2375
+ </template>
2376
+ </div>
2377
+ </div>
2378
+ </div>
2379
+ </template>
2380
+ </TransitionGroup>
2381
+ </LpContextMenu>
2019
2382
  </LpShift>
2020
2383
  <LpPagination
2021
2384
  v-if="load && total > pageSize"
@@ -2132,6 +2495,41 @@ tr:hover .lp-tick-cue,
2132
2495
  }
2133
2496
  }
2134
2497
 
2498
+ /*
2499
+ * A list line's tick opens from nothing, as the table's column does, so lines
2500
+ * do not carry an empty gap until selection starts.
2501
+ */
2502
+ .lp-tick-line {
2503
+ max-width: 2rem;
2504
+ opacity: 1;
2505
+ }
2506
+
2507
+ /*
2508
+ * A card's tick sits in its corner at its own size, with no column to hold
2509
+ * open; a cue shows it only while
2510
+ * the card is pointed at or holds focus, the same rule as a row's.
2511
+ */
2512
+ .lp-tick-card {
2513
+ max-width: none;
2514
+ padding-inline: 0;
2515
+ opacity: 1;
2516
+ }
2517
+
2518
+ .lp-tick[data-cue] {
2519
+ opacity: 0;
2520
+ }
2521
+
2522
+ .lp-item:hover .lp-tick[data-cue],
2523
+ .lp-tick[data-cue]:focus-within {
2524
+ opacity: 1;
2525
+ }
2526
+
2527
+ @media (hover: none) {
2528
+ .lp-item:hover .lp-tick[data-cue] {
2529
+ opacity: 0;
2530
+ }
2531
+ }
2532
+
2135
2533
  @media (prefers-reduced-motion: reduce) {
2136
2534
  .lp-row-enter-active,
2137
2535
  .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"