@leavepulse/ui 0.43.4 → 0.45.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.
- package/README.md +121 -7
- package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
- package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
- package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
- package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
- package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
- package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
- package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-CPwWxTJS.css} +49 -9
- package/dist/chunks/ContextMenuItems-BtKU7074.js +124 -0
- package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-BkaGXTjx.js} +26 -9
- package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-B3_9E1Xa.js} +28 -12
- package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-CnfFVIwC.js} +26 -12
- package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BwX8uo4_.js} +54 -49
- package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-B6ilrM7T.js} +16 -9
- package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-C53_RaGF.js} +53 -26
- package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-C8IgSMsi.js} +52 -24
- package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-C1XStdOo.js} +19 -10
- package/dist/chunks/LpBreadcrumbs-CQJd73Gm.js +95 -0
- package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-CKSwC6Mu.js} +45 -15
- package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
- package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-C2IvucmX.js} +1 -1
- package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-Bkj13CbW.js} +8 -4
- package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-ca7ANFyW.js} +49 -17
- package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-zz4VGkG4.js} +75 -26
- package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-DCw5jR1t.js} +59 -17
- package/dist/chunks/LpConfirmHost-CbywMpMP.js +49 -0
- package/dist/chunks/LpContextMenu-BqkAYa9X.js +69 -0
- package/dist/chunks/LpDatePicker-DuusgT69.js +195 -0
- package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
- package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
- package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-Dt4aB4Fr.js} +65 -28
- package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
- package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
- package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
- package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CNurOvkV.js} +25 -10
- package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
- package/dist/chunks/LpFormField-CtVYj6tO.js +101 -0
- package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
- package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-D-YGQbEv.js} +58 -20
- package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-DSDahF23.js} +148 -61
- package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
- package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-KypbgOyo.js} +300 -72
- package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-Cc84LaM3.js} +39 -14
- package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-YKr-cQPN.js} +7 -4
- package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DMMDQHAu.js} +52 -29
- package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-BVPDyIup.js} +10 -7
- package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-CCwyJP5C.js} +79 -33
- package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-B77O-z28.js} +21 -8
- package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
- package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
- package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-BcDlo3r_.js} +31 -20
- package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-eKzQhbQP.js} +11 -8
- package/dist/chunks/LpPhoneInput-BCbrrhgE.js +336 -0
- package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BEJ2SfI6.js} +84 -28
- package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-DCKeqiVn.js} +5 -2
- package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
- package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
- package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
- package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
- package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
- package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
- package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-C09EEam2.js} +8 -4
- package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DNIL6MB9.js} +50 -31
- package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-CQNB0-yU.js} +30 -22
- package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-kx-LdqNU.js} +16 -11
- package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
- package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
- package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
- package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-B-7udBRP.js} +2 -2
- package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
- package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-OrcOJX9m.js} +41 -27
- package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
- package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
- package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-29iW1XOx.js} +20 -6
- package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
- package/dist/chunks/LpTextarea-uuScWo6-.js +77 -0
- package/dist/chunks/LpThemeProvider-Be4r2TQX.js +35 -0
- package/dist/chunks/LpThemeSwitcher-B4aVPbNC.js +166 -0
- package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
- package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-Cqe7fXnh.js} +14 -8
- package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster-DpJvoCOh.js} +94 -31
- package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CXVKcOXT.js} +14 -6
- package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
- package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-6ozNrQKh.js} +8 -2
- package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-ugcv25xj.js} +44 -15
- package/dist/chunks/icons-CkA0njzm.js +43 -0
- package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-DJm1JSsY.js} +215 -260
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.d.ts +8 -0
- package/dist/components/ContextMenuItems.vue.js +4 -0
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.d.ts +4 -0
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.d.ts +3 -0
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.d.ts +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.d.ts +9 -0
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.d.ts +10 -0
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.d.ts +31 -6
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.d.ts +3 -3
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBadge.vue.d.ts +2 -2
- package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.d.ts +35 -98
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.d.ts +47 -3
- package/dist/components/LpChart.vue.js +377 -97
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.d.ts +2 -0
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +2 -2
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +14 -3
- package/dist/components/LpCommandPalette.vue.js +142 -77
- package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.d.ts +9 -0
- package/dist/components/LpConfirmHost.vue.js +4 -0
- package/dist/components/LpContextMenu.vue.d.ts +3 -5
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.d.ts +24 -6
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
- package/dist/components/LpDateRangePicker.vue.js +4 -0
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDrawer.vue.d.ts +10 -0
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +23 -7
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.d.ts +8 -0
- package/dist/components/LpFieldNotice.vue.js +4 -0
- package/dist/components/LpFileTree.vue.d.ts +11 -2
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.d.ts +13 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpIcon.vue.d.ts +0 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +93 -58
- package/dist/components/LpInput.vue.d.ts +22 -3
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.d.ts +17 -0
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLink.vue.d.ts +11 -11
- package/dist/components/LpLink.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +72 -3
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.d.ts +4 -4
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +16 -6
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +0 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.d.ts +11 -2
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.d.ts +2 -0
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpNumberFlow.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPagination.vue.d.ts +2 -0
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.d.ts +13 -0
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.d.ts +21 -2
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPopover.vue.d.ts +2 -0
- package/dist/components/LpPopover.vue.js +1 -1
- package/dist/components/LpProgress.vue.d.ts +1 -1
- package/dist/components/LpProgress.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
- package/dist/components/LpRangeCalendar.vue.js +4 -0
- package/dist/components/LpRollingText.vue.d.ts +1 -1
- package/dist/components/LpRollingText.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSection.vue.d.ts +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.d.ts +5 -0
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.d.ts +1 -0
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +7 -1
- package/dist/components/LpShortcutsDialog.vue.js +60 -15
- package/dist/components/LpSidebar.vue.d.ts +6 -2
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSkeleton.vue.js +1 -1
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpSparkline.vue.d.ts +1 -0
- package/dist/components/LpSparkline.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +107 -3
- package/dist/components/LpTable.vue.js +540 -128
- package/dist/components/LpTableFilter.vue.d.ts +31 -0
- package/dist/components/LpTableFilter.vue.js +4 -0
- package/dist/components/LpTableOfContents.vue.d.ts +4 -2
- package/dist/components/LpTableOfContents.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +2 -0
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.d.ts +77 -0
- package/dist/components/LpTagsInput.vue.js +4 -0
- package/dist/components/LpTextarea.vue.d.ts +19 -6
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeProvider.vue.d.ts +22 -0
- package/dist/components/LpThemeProvider.vue.js +4 -0
- package/dist/components/LpThemeSwitcher.vue.d.ts +15 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTilt.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.d.ts +6 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.d.ts +1 -2
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
- package/dist/components/LpTooltipProvider.vue.js +4 -0
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.d.ts +4 -0
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/chart.d.ts +32 -1
- package/dist/components/chart.js +42 -6
- package/dist/components/codeHighlight.d.ts +6 -0
- package/dist/components/codeHighlight.js +92 -20
- package/dist/components/color.d.ts +1 -1
- package/dist/components/color.js +20 -9
- package/dist/components/commandSearch.js +3 -0
- package/dist/components/confirm.d.ts +25 -0
- package/dist/components/confirm.js +26 -0
- package/dist/components/countries.d.ts +6 -0
- package/dist/components/countries.js +7 -0
- package/dist/components/dateRange.d.ts +43 -0
- package/dist/components/dateRange.js +76 -0
- package/dist/components/exportData.d.ts +58 -0
- package/dist/components/exportData.js +165 -0
- package/dist/components/fileTree.js +16 -7
- package/dist/components/imageCrop.d.ts +42 -0
- package/dist/components/imageCrop.js +47 -0
- package/dist/components/link.d.ts +12 -0
- package/dist/components/link.js +19 -0
- package/dist/components/logSearch.d.ts +18 -0
- package/dist/components/logSearch.js +36 -0
- package/dist/components/modalDepth.d.ts +18 -2
- package/dist/components/modalDepth.js +17 -5
- package/dist/components/otp.d.ts +5 -0
- package/dist/components/otp.js +10 -0
- package/dist/components/phone.d.ts +41 -0
- package/dist/components/phone.js +80 -0
- package/dist/components/sidebar.d.ts +7 -1
- package/dist/components/tableFilter.d.ts +36 -0
- package/dist/components/tableFilter.js +84 -0
- package/dist/components/tableSort.d.ts +8 -0
- package/dist/components/tableSort.js +31 -0
- package/dist/components/tags.d.ts +20 -0
- package/dist/components/tags.js +37 -0
- package/dist/composables/useField.d.ts +38 -0
- package/dist/composables/useField.js +45 -0
- package/dist/composables/useFieldNotice.d.ts +17 -0
- package/dist/composables/useFieldNotice.js +28 -0
- package/dist/composables/useForm.d.ts +53 -0
- package/dist/composables/useForm.js +207 -0
- package/dist/composables/useFormDraft.d.ts +10 -3
- package/dist/composables/useFormDraft.js +9 -14
- package/dist/composables/useHotkeys.d.ts +81 -5
- package/dist/composables/useHotkeys.js +223 -25
- package/dist/composables/useInputFilter.d.ts +17 -0
- package/dist/composables/useInputFilter.js +47 -10
- package/dist/composables/useInputMask.d.ts +59 -0
- package/dist/composables/useInputMask.js +170 -0
- package/dist/composables/useLpConfig.d.ts +24 -0
- package/dist/composables/useLpConfig.js +44 -0
- package/dist/composables/usePaintSelect.d.ts +22 -0
- package/dist/composables/usePaintSelect.js +169 -0
- package/dist/composables/useRovingFocus.js +7 -0
- package/dist/composables/useShift.js +7 -2
- package/dist/composables/useTilt.d.ts +2 -2
- package/dist/composables/useTilt.js +5 -6
- package/dist/composables/useToast.d.ts +20 -0
- package/dist/composables/useToast.js +28 -4
- package/dist/composables/useZoomPan.d.ts +3 -3
- package/dist/composables/useZoomPan.js +4 -4
- package/dist/icons-all.d.ts +1 -0
- package/dist/icons-all.js +3 -0
- package/dist/icons.d.ts +14 -1
- package/dist/index.d.ts +60 -4
- package/dist/index.js +245 -141
- package/dist/kit-icons.d.ts +2 -0
- package/dist/layout/tree.d.ts +2 -0
- package/dist/layout/tree.js +16 -1
- package/dist/layout/useLayout.d.ts +15 -0
- package/dist/layout/useLayout.js +41 -14
- package/dist/theme/backdrop.d.ts +12 -0
- package/dist/theme/presets.d.ts +0 -2
- package/dist/theme/useTheme.d.ts +32 -9
- package/package.json +18 -8
- package/src/component-names.ts +9 -7
- package/src/components/ContextMenuItems.vue +88 -0
- package/src/components/LayoutCanvas.vue +26 -6
- package/src/components/LayoutNode.vue +30 -4
- package/src/components/LpActionBar.vue +31 -8
- package/src/components/LpAddressInput.vue +57 -41
- package/src/components/LpAlert.vue +16 -3
- package/src/components/LpAppShell.vue +45 -12
- package/src/components/LpAutocomplete.vue +47 -10
- package/src/components/LpAvatar.vue +20 -6
- package/src/components/LpBreadcrumbs.vue +65 -41
- package/src/components/LpButton.vue +60 -14
- package/src/components/LpChart.vue +351 -28
- package/src/components/LpCheckbox.vue +7 -3
- package/src/components/LpCodeBlock.vue +50 -14
- package/src/components/LpColorPicker.vue +73 -10
- package/src/components/LpCommandPalette.vue +115 -20
- package/src/components/LpConfirmDialog.vue +68 -17
- package/src/components/LpConfirmHost.vue +59 -0
- package/src/components/LpContextMenu.vue +15 -116
- package/src/components/LpDatePicker.vue +104 -68
- package/src/components/LpDateRangePicker.vue +224 -0
- package/src/components/LpDrawer.vue +56 -19
- package/src/components/LpDropdownMenu.vue +1 -0
- package/src/components/LpDropzone.vue +21 -4
- package/src/components/LpFieldNotice.vue +65 -0
- package/src/components/LpFileTree.vue +33 -7
- package/src/components/LpFormField.vue +59 -7
- package/src/components/LpIcon.vue +22 -4
- package/src/components/LpImageEditor.vue +96 -57
- package/src/components/LpInput.vue +62 -11
- package/src/components/LpLightbox.vue +140 -29
- package/src/components/LpLink.vue +24 -13
- package/src/components/LpLogViewer.vue +311 -39
- package/src/components/LpMapPicker.vue +36 -13
- package/src/components/LpMediaGallery.vue +8 -2
- package/src/components/LpModal.vue +54 -28
- package/src/components/LpModalNav.vue +5 -2
- package/src/components/LpNotificationBell.vue +77 -23
- package/src/components/LpNumberField.vue +21 -6
- package/src/components/LpNumberFlow.vue +26 -10
- package/src/components/LpOtpInput.vue +43 -8
- package/src/components/LpPagination.vue +23 -7
- package/src/components/LpPasswordInput.vue +4 -1
- package/src/components/LpPhoneInput.vue +224 -39
- package/src/components/LpPie.vue +102 -20
- package/src/components/LpPopover.vue +4 -0
- package/src/components/LpProgress.vue +5 -3
- package/src/components/LpRadioGroup.vue +9 -2
- package/src/components/LpRangeCalendar.vue +365 -0
- package/src/components/LpRollingText.vue +3 -0
- package/src/components/LpScrollArea.vue +22 -2
- package/src/components/LpSegmented.vue +12 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +83 -9
- package/src/components/LpSidebar.vue +14 -9
- package/src/components/LpSidebarNav.vue +17 -5
- package/src/components/LpSkeleton.vue +4 -1
- package/src/components/LpSlider.vue +7 -1
- package/src/components/LpSparkline.vue +11 -3
- package/src/components/LpStepper.vue +23 -10
- package/src/components/LpSwitch.vue +31 -8
- package/src/components/LpTable.vue +668 -90
- package/src/components/LpTableFilter.vue +175 -0
- package/src/components/LpTableOfContents.vue +50 -32
- package/src/components/LpTabs.vue +23 -6
- package/src/components/LpTagsInput.vue +364 -0
- package/src/components/LpTextarea.vue +44 -6
- package/src/components/LpThemeProvider.vue +41 -0
- package/src/components/LpThemeSwitcher.vue +76 -33
- package/src/components/LpTilt.vue +2 -2
- package/src/components/LpTimeField.vue +14 -3
- package/src/components/LpToaster.vue +115 -27
- package/src/components/LpTooltip.vue +29 -5
- package/src/components/LpTooltipProvider.vue +24 -0
- package/src/components/LpTopologyCanvas.vue +19 -2
- package/src/components/LpUptimeBar.vue +46 -8
- package/src/components/chart.ts +82 -8
- package/src/components/codeHighlight.ts +111 -15
- package/src/components/color.ts +30 -9
- package/src/components/commandSearch.ts +6 -0
- package/src/components/confirm.ts +58 -0
- package/src/components/countries.ts +12 -0
- package/src/components/dateRange.ts +134 -0
- package/src/components/exportData.ts +228 -0
- package/src/components/fileTree.ts +19 -7
- package/src/components/imageCrop.ts +107 -0
- package/src/components/link.ts +34 -0
- package/src/components/logSearch.ts +46 -0
- package/src/components/modalDepth.ts +48 -6
- package/src/components/otp.ts +16 -0
- package/src/components/phone.ts +140 -0
- package/src/components/sidebar.ts +7 -1
- package/src/components/tableFilter.ts +122 -0
- package/src/components/tableSort.ts +37 -0
- package/src/components/tags.ts +62 -0
- package/src/composables/useField.ts +83 -0
- package/src/composables/useFieldNotice.ts +59 -0
- package/src/composables/useForm.ts +314 -0
- package/src/composables/useFormDraft.ts +25 -20
- package/src/composables/useHotkeys.ts +355 -28
- package/src/composables/useInputFilter.ts +77 -14
- package/src/composables/useInputMask.ts +254 -0
- package/src/composables/useLpConfig.ts +87 -0
- package/src/composables/usePaintSelect.ts +243 -0
- package/src/composables/useRovingFocus.ts +11 -0
- package/src/composables/useShift.ts +13 -2
- package/src/composables/useTilt.ts +5 -6
- package/src/composables/useToast.ts +56 -4
- package/src/composables/useZoomPan.ts +6 -6
- package/src/iconSubset.d.mts +10 -0
- package/src/iconSubset.mjs +51 -0
- package/src/icons-all.ts +10 -0
- package/src/icons.ts +59 -8
- package/src/index.ts +73 -4
- package/src/kit-icons.ts +5 -0
- package/src/layout/tree.ts +30 -0
- package/src/layout/useLayout.ts +68 -18
- package/src/nuxt.ts +87 -17
- package/src/runtime/config.plugin.ts +14 -0
- package/src/runtime/theme.plugin.ts +43 -23
- package/src/theme/backdrop.ts +69 -0
- package/src/theme/presets.ts +6 -64
- package/src/theme/useTheme.ts +206 -100
- package/src/tokens/tokens.css +46 -0
- package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
- package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
- package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
- package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
- package/dist/chunks/LpPhoneInput-BDSKVl_z.js +0 -209
- package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
|
@@ -5,22 +5,43 @@ export default { inheritAttrs: false }
|
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<script setup lang="ts" generic="T extends Record<string, unknown>">
|
|
8
|
-
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, useAttrs, watch } from "vue"
|
|
8
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useAttrs, watch } from "vue"
|
|
9
9
|
import LpCheckbox from "./LpCheckbox.vue"
|
|
10
10
|
import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
|
|
11
11
|
import LpIcon from "./LpIcon.vue"
|
|
12
12
|
import LpPagination from "./LpPagination.vue"
|
|
13
|
+
import LpPopover from "./LpPopover.vue"
|
|
13
14
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
15
|
+
import LpRollingText from "./LpRollingText.vue"
|
|
16
|
+
import LpShift from "./LpShift.vue"
|
|
17
|
+
import LpTableFilter, { type TableFilterOption } from "./LpTableFilter.vue"
|
|
14
18
|
import { useAsyncTask } from "../composables/useAsyncTask"
|
|
19
|
+
import { useHotkeys } from "../composables/useHotkeys"
|
|
20
|
+
import { usePaintSelect } from "../composables/usePaintSelect"
|
|
15
21
|
import { useRovingFocus } from "../composables/useRovingFocus"
|
|
22
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
23
|
+
import { compareValues } from "./tableSort"
|
|
24
|
+
import LpButton from "./LpButton.vue"
|
|
25
|
+
import { downloadFile, fileStem, toCsv, type CsvOptions } from "./exportData"
|
|
26
|
+
import { countValues, parseTableState, serializeTableState, summarizeShown } from "./tableFilter"
|
|
16
27
|
|
|
17
28
|
export interface TableColumn<Row> {
|
|
18
29
|
key: string
|
|
19
30
|
label: string
|
|
20
31
|
align?: "left" | "right" | "center"
|
|
21
32
|
width?: string
|
|
33
|
+
/**
|
|
34
|
+
* Keep the cell on one line. For ids, codes, dates and amounts, which a
|
|
35
|
+
* narrow screen otherwise breaks at a hyphen or a space ("ORD-" / "1040");
|
|
36
|
+
* the table scrolls sideways instead.
|
|
37
|
+
*/
|
|
38
|
+
nowrap?: boolean
|
|
22
39
|
/** Allow clicking the header to sort by this column. */
|
|
23
40
|
sortable?: boolean
|
|
41
|
+
/** The value this column sorts by, when the cell holds something else. */
|
|
42
|
+
sortValue?: (row: Row) => unknown
|
|
43
|
+
/** Full control over the order; wins over `sortValue`. Ascending order. */
|
|
44
|
+
compare?: (a: Row, b: Row) => number
|
|
24
45
|
/**
|
|
25
46
|
* Offer "show only these values" in the header's menu, built from the values
|
|
26
47
|
* actually present in the rows. For a column that holds a small vocabulary —
|
|
@@ -33,11 +54,62 @@ export interface TableColumn<Row> {
|
|
|
33
54
|
* vocabulary; the menu should say what the column itself says.
|
|
34
55
|
*/
|
|
35
56
|
valueLabel?: (value: unknown) => string
|
|
57
|
+
/** The value a CSV export writes for this column; the raw cell by default. */
|
|
58
|
+
exportValue?: (row: Row) => unknown
|
|
36
59
|
/** Keep this column out of the show/hide list — an actions column has no
|
|
37
60
|
* name to offer and nothing to gain from being hidden. */
|
|
38
61
|
alwaysVisible?: boolean
|
|
39
62
|
}
|
|
40
63
|
|
|
64
|
+
export interface TableLabels {
|
|
65
|
+
[key: string]: string
|
|
66
|
+
loading: string
|
|
67
|
+
error: string
|
|
68
|
+
empty: string
|
|
69
|
+
sortAsc: string
|
|
70
|
+
sortDesc: string
|
|
71
|
+
sortClear: string
|
|
72
|
+
filter: string
|
|
73
|
+
showAll: string
|
|
74
|
+
columns: string
|
|
75
|
+
exportCsv: string
|
|
76
|
+
selectAll: string
|
|
77
|
+
shortcutsGroup: string
|
|
78
|
+
filterColumn: string
|
|
79
|
+
filterSearch: string
|
|
80
|
+
filterAll: string
|
|
81
|
+
filterNone: string
|
|
82
|
+
filterInvert: string
|
|
83
|
+
filterOnly: string
|
|
84
|
+
filterOnlyShown: string
|
|
85
|
+
filterAddShown: string
|
|
86
|
+
filterNoValues: string
|
|
87
|
+
filterNothingShown: string
|
|
88
|
+
clearFilter: string
|
|
89
|
+
clearFilters: string
|
|
90
|
+
noMatches: string
|
|
91
|
+
resetFilters: string
|
|
92
|
+
rowCount: string
|
|
93
|
+
rowsTotal: string
|
|
94
|
+
selectedCount: string
|
|
95
|
+
clearSelection: string
|
|
96
|
+
pageSelected: string
|
|
97
|
+
selectAllMatching: string
|
|
98
|
+
allMatchingSelected: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TableExportOptions extends CsvOptions {
|
|
102
|
+
/**
|
|
103
|
+
* Which rows: every row in the current order, the rows the header filters
|
|
104
|
+
* leave, or the selected ones. With `load` only the loaded page is known.
|
|
105
|
+
*/
|
|
106
|
+
scope?: "all" | "filtered" | "selected"
|
|
107
|
+
/** File name without extension. */
|
|
108
|
+
filename?: string
|
|
109
|
+
/** Return the CSV without offering a download. */
|
|
110
|
+
download?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
41
113
|
export interface SortState {
|
|
42
114
|
key: string
|
|
43
115
|
dir: "asc" | "desc"
|
|
@@ -68,6 +140,7 @@ export interface TablePage<Row> {
|
|
|
68
140
|
|
|
69
141
|
type RowKey = string | number
|
|
70
142
|
|
|
143
|
+
|
|
71
144
|
const props = withDefaults(
|
|
72
145
|
defineProps<{
|
|
73
146
|
columns: TableColumn<T>[]
|
|
@@ -86,12 +159,23 @@ const props = withDefaults(
|
|
|
86
159
|
debounce?: number
|
|
87
160
|
loadingLabel?: string
|
|
88
161
|
errorLabel?: string
|
|
89
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* Stable id per row. Without it a row is keyed by its position in `rows`,
|
|
164
|
+
* which survives sorting and filtering but not a new array; set it whenever
|
|
165
|
+
* rows are replaced (`load`, polling) and selection must carry over.
|
|
166
|
+
*/
|
|
90
167
|
rowKey?: keyof T | ((row: T) => RowKey)
|
|
91
168
|
emptyLabel?: string
|
|
92
169
|
emptyIcon?: string
|
|
93
|
-
/**
|
|
94
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Rows can be selected (v-model:selected). `true` shows a tick column,
|
|
172
|
+
* where pressing a tick and sweeping ticks the run. `"hold"` keeps the
|
|
173
|
+
* column out of sight until it is needed: a long press on a row selects it
|
|
174
|
+
* (and the same press goes on selecting the rows it is dragged across), the
|
|
175
|
+
* column slides in, and while anything is selected a click on a row toggles
|
|
176
|
+
* it. Emptying the selection, or Escape, slides the column away again.
|
|
177
|
+
*/
|
|
178
|
+
selectable?: boolean | "hold"
|
|
95
179
|
/** Selected row keys (v-model:selected). */
|
|
96
180
|
selected?: RowKey[]
|
|
97
181
|
/** Active sort (v-model:sort). When set, the parent owns ordering (server
|
|
@@ -111,26 +195,98 @@ const props = withDefaults(
|
|
|
111
195
|
filterLabel?: string
|
|
112
196
|
showAllLabel?: string
|
|
113
197
|
columnsLabel?: string
|
|
198
|
+
/** Offer a CSV download in a toolbar above the table and in its menu. */
|
|
199
|
+
exportable?: boolean
|
|
200
|
+
/** File name for that download, without extension. */
|
|
201
|
+
exportFilename?: string
|
|
202
|
+
/** All of the wording above in one object, and the config-level scope "table". */
|
|
203
|
+
labels?: Partial<TableLabels>
|
|
204
|
+
/** Row height: `compact` for dense data, `comfortable` (default) otherwise. */
|
|
205
|
+
density?: "comfortable" | "compact"
|
|
206
|
+
/**
|
|
207
|
+
* Always show the bar above the table with the row count. It shows on its
|
|
208
|
+
* own while filters are active or rows are selected.
|
|
209
|
+
*/
|
|
210
|
+
summary?: boolean
|
|
211
|
+
/**
|
|
212
|
+
* Remember sort, value filters and hidden columns across visits under this
|
|
213
|
+
* key (localStorage). Only plain filter values (text, numbers) are kept.
|
|
214
|
+
*/
|
|
215
|
+
persist?: string
|
|
216
|
+
/**
|
|
217
|
+
* With `load`: the user chose "select all N" beyond the loaded page
|
|
218
|
+
* (v-model:allMatching). The keys in `selected` are then only the loaded
|
|
219
|
+
* ones; acting on everything that matches is the consumer's to do.
|
|
220
|
+
*/
|
|
221
|
+
allMatching?: boolean
|
|
114
222
|
}>(),
|
|
115
223
|
{
|
|
116
224
|
rows: () => [],
|
|
117
225
|
pageSize: 25,
|
|
118
226
|
debounce: 150,
|
|
119
|
-
loadingLabel: "Loading…",
|
|
120
|
-
errorLabel: "Could not load",
|
|
121
|
-
emptyLabel: "Nothing here yet",
|
|
122
227
|
emptyIcon: "lucide:inbox",
|
|
123
228
|
selectable: false,
|
|
124
229
|
selected: () => [],
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
230
|
+
density: "comfortable",
|
|
231
|
+
summary: false,
|
|
232
|
+
allMatching: false,
|
|
233
|
+
// undefined, not null: null is a parent saying "unsorted", undefined is no
|
|
234
|
+
// parent at all, and only the latter lets the table keep its own sort.
|
|
235
|
+
sort: undefined,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const labelSet = useLabels<TableLabels>(
|
|
240
|
+
"table",
|
|
241
|
+
{
|
|
242
|
+
loading: "Loading…",
|
|
243
|
+
error: "Could not load",
|
|
244
|
+
empty: "Nothing here yet",
|
|
245
|
+
sortAsc: "Sort ascending",
|
|
246
|
+
sortDesc: "Sort descending",
|
|
247
|
+
sortClear: "Clear sorting",
|
|
248
|
+
filter: "Show only",
|
|
249
|
+
showAll: "Show all",
|
|
250
|
+
columns: "Columns",
|
|
251
|
+
exportCsv: "Export CSV",
|
|
252
|
+
selectAll: "Select every row",
|
|
253
|
+
shortcutsGroup: "Table",
|
|
254
|
+
filterColumn: "Filter {column}",
|
|
255
|
+
filterSearch: "Search values",
|
|
256
|
+
filterAll: "All",
|
|
257
|
+
filterNone: "None",
|
|
258
|
+
filterInvert: "Invert",
|
|
259
|
+
filterOnly: "Only",
|
|
260
|
+
filterOnlyShown: "Only these {count}",
|
|
261
|
+
filterAddShown: "Add these {count}",
|
|
262
|
+
filterNoValues: "No values match",
|
|
263
|
+
filterNothingShown: "none",
|
|
264
|
+
clearFilter: "Clear the {column} filter",
|
|
265
|
+
clearFilters: "Clear all",
|
|
266
|
+
noMatches: "No rows match the filters",
|
|
267
|
+
resetFilters: "Reset filters",
|
|
268
|
+
rowCount: "{shown} of {total}",
|
|
269
|
+
rowsTotal: "{total} rows",
|
|
270
|
+
selectedCount: "{count} selected",
|
|
271
|
+
clearSelection: "Clear selection",
|
|
272
|
+
pageSelected: "All {count} on this page are selected.",
|
|
273
|
+
selectAllMatching: "Select all {total}",
|
|
274
|
+
allMatchingSelected: "All {total} are selected.",
|
|
132
275
|
},
|
|
276
|
+
() => props.labels,
|
|
133
277
|
)
|
|
278
|
+
const text = computed<TableLabels>(() => ({
|
|
279
|
+
...labelSet.value,
|
|
280
|
+
loading: props.loadingLabel ?? labelSet.value.loading,
|
|
281
|
+
error: props.errorLabel ?? labelSet.value.error,
|
|
282
|
+
empty: props.emptyLabel ?? labelSet.value.empty,
|
|
283
|
+
sortAsc: props.sortAscLabel ?? labelSet.value.sortAsc,
|
|
284
|
+
sortDesc: props.sortDescLabel ?? labelSet.value.sortDesc,
|
|
285
|
+
sortClear: props.sortClearLabel ?? labelSet.value.sortClear,
|
|
286
|
+
filter: props.filterLabel ?? labelSet.value.filter,
|
|
287
|
+
showAll: props.showAllLabel ?? labelSet.value.showAll,
|
|
288
|
+
columns: props.columnsLabel ?? labelSet.value.columns,
|
|
289
|
+
}))
|
|
134
290
|
|
|
135
291
|
/*
|
|
136
292
|
* Layout classes belong on the root, not on the scroll area. Since the pager
|
|
@@ -150,13 +306,16 @@ const scrollAttrs = computed(() => {
|
|
|
150
306
|
const emit = defineEmits<{
|
|
151
307
|
(e: "update:selected", value: RowKey[]): void
|
|
152
308
|
(e: "update:sort", value: SortState | null): void
|
|
309
|
+
(e: "update:allMatching", value: boolean): void
|
|
153
310
|
(e: "rowClick", row: T): void
|
|
154
311
|
}>()
|
|
155
312
|
|
|
156
313
|
function keyFor(row: T, index: number): RowKey {
|
|
157
314
|
if (typeof props.rowKey === "function") return props.rowKey(row)
|
|
158
315
|
if (props.rowKey) return row[props.rowKey] as RowKey
|
|
159
|
-
|
|
316
|
+
// Position in the source, not in the sorted view: a sort must not move a
|
|
317
|
+
// tick from one row to another.
|
|
318
|
+
return sourceIndex.value.get(row) ?? index
|
|
160
319
|
}
|
|
161
320
|
|
|
162
321
|
function alignClass(align?: "left" | "right" | "center"): string {
|
|
@@ -192,6 +351,8 @@ function onRowContextMenu(row: T) {
|
|
|
192
351
|
// Row items first — they are what the pointer is actually on — then the
|
|
193
352
|
// table-wide ones under a rule, when the row has any of its own.
|
|
194
353
|
const contextMenu = computed<ContextMenuItemDef[]>(() => {
|
|
354
|
+
// A finger's long press opens this menu too; while it is selecting rows, no.
|
|
355
|
+
if (paint.painting.value) return []
|
|
195
356
|
const table = tableMenu()
|
|
196
357
|
const row = menuRow.value ? (props.rowMenu?.(menuRow.value) ?? []) : []
|
|
197
358
|
if (!row.length) return table
|
|
@@ -207,7 +368,7 @@ const contextMenu = computed<ContextMenuItemDef[]>(() => {
|
|
|
207
368
|
// emitted an event nobody was listening for, and left the rows exactly as they
|
|
208
369
|
// were: a control that answers a click by doing nothing.
|
|
209
370
|
const ownSort = ref<SortState | null>(null)
|
|
210
|
-
const activeSort = computed(() => props.sort
|
|
371
|
+
const activeSort = computed(() => (props.sort !== undefined ? props.sort : ownSort.value))
|
|
211
372
|
|
|
212
373
|
// ── server-backed rows ───────────────────────────────────────
|
|
213
374
|
const loaded = shallowRef<T[]>([])
|
|
@@ -219,14 +380,20 @@ const task = useAsyncTask<TablePage<T>>()
|
|
|
219
380
|
const loading = task.pending
|
|
220
381
|
|
|
221
382
|
const sourceRows = computed<T[]>(() => (props.load ? loaded.value : props.rows))
|
|
383
|
+
const sourceIndex = computed(() => new Map(sourceRows.value.map((row, i) => [row, i])))
|
|
222
384
|
|
|
223
385
|
// The tallest page shown so far, held as a floor: the last page is usually
|
|
224
386
|
// short, and without it the pager below jumps up the moment you reach it.
|
|
225
|
-
|
|
387
|
+
/*
|
|
388
|
+
* The table sits in LpContextMenu's as-child trigger, which re-clones its child
|
|
389
|
+
* on every render and leaves a template ref on it undefined after the first
|
|
390
|
+
* update. The component's own root is stable, so the table is found from it.
|
|
391
|
+
*/
|
|
392
|
+
const frameEl = ref<HTMLElement | null>(null)
|
|
226
393
|
const floor = ref(0)
|
|
227
394
|
async function holdHeight() {
|
|
228
395
|
await nextTick()
|
|
229
|
-
const height =
|
|
396
|
+
const height = frameEl.value?.querySelector("table")?.offsetHeight ?? 0
|
|
230
397
|
if (height > floor.value) floor.value = height
|
|
231
398
|
}
|
|
232
399
|
|
|
@@ -297,6 +464,60 @@ function showAllValues(key: string) {
|
|
|
297
464
|
withMotion(() => (hiddenValues.value = { ...hiddenValues.value, [key]: new Set() }))
|
|
298
465
|
}
|
|
299
466
|
|
|
467
|
+
function setHidden(key: string, next: Set<unknown>) {
|
|
468
|
+
withMotion(() => (hiddenValues.value = { ...hiddenValues.value, [key]: next }))
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function labelOf(col: TableColumn<T>, value: unknown): string {
|
|
472
|
+
return col.valueLabel?.(value) ?? String(value ?? "—")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/*
|
|
476
|
+
* What the filter of one column offers. Counts follow the OTHER filters, the
|
|
477
|
+
* way a faceted search does: with "Status: open" set, the "Owner" filter
|
|
478
|
+
* counts open rows, which is what the user is about to narrow further.
|
|
479
|
+
*/
|
|
480
|
+
function filterOptions(col: TableColumn<T>): TableFilterOption[] {
|
|
481
|
+
let counts: Map<unknown, number> | undefined
|
|
482
|
+
if (!props.load) {
|
|
483
|
+
const others = Object.entries(hiddenValues.value).filter(([key, set]) => key !== col.key && set.size)
|
|
484
|
+
const rows = others.length
|
|
485
|
+
? props.rows.filter((row) => others.every(([key, hidden]) => !hidden.has(row[key])))
|
|
486
|
+
: props.rows
|
|
487
|
+
counts = countValues(rows, col.key)
|
|
488
|
+
}
|
|
489
|
+
return valuesOf(col).map((value) => ({ value, label: labelOf(col, value), count: counts?.get(value) ?? (counts ? 0 : undefined) }))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Every column with something hidden, summarised for the chips above the table. */
|
|
493
|
+
const activeFilters = computed(() =>
|
|
494
|
+
props.columns
|
|
495
|
+
.filter((col) => col.filterable && hiddenValues.value[col.key]?.size)
|
|
496
|
+
.map((col) => {
|
|
497
|
+
const hidden = hiddenValues.value[col.key]!
|
|
498
|
+
const shown = valuesOf(col).filter((v) => !hidden.has(v)).map((v) => labelOf(col, v))
|
|
499
|
+
return { col, text: shown.length ? summarizeShown(shown) : text.value.filterNothingShown }
|
|
500
|
+
}),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
function clearAllFilters() {
|
|
504
|
+
withMotion(() => (hiddenValues.value = {}))
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function filterLabels() {
|
|
508
|
+
const t = text.value
|
|
509
|
+
return {
|
|
510
|
+
search: t.filterSearch,
|
|
511
|
+
all: t.filterAll,
|
|
512
|
+
none: t.filterNone,
|
|
513
|
+
invert: t.filterInvert,
|
|
514
|
+
only: t.filterOnly,
|
|
515
|
+
onlyShown: t.filterOnlyShown,
|
|
516
|
+
addShown: t.filterAddShown,
|
|
517
|
+
noValues: t.filterNoValues,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
300
521
|
function toggleColumn(key: string) {
|
|
301
522
|
const next = new Set(hiddenColumns.value)
|
|
302
523
|
if (next.has(key)) next.delete(key)
|
|
@@ -315,23 +536,19 @@ const filteredRows = computed<T[]>(() => {
|
|
|
315
536
|
})
|
|
316
537
|
|
|
317
538
|
// We compare by raw cell value with a stable-ish coerce.
|
|
318
|
-
|
|
539
|
+
function sortRows(rows: T[]): T[] {
|
|
319
540
|
const s = activeSort.value
|
|
320
|
-
if (!s || props.load) return
|
|
541
|
+
if (!s || props.load) return rows
|
|
321
542
|
const col = props.columns.find((c) => c.key === s.key)
|
|
322
|
-
if (!col?.sortable) return
|
|
543
|
+
if (!col?.sortable) return rows
|
|
323
544
|
const factor = s.dir === "asc" ? 1 : -1
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
function compare(a: unknown, b: unknown): number {
|
|
328
|
-
if (a == null && b == null) return 0
|
|
329
|
-
if (a == null) return -1
|
|
330
|
-
if (b == null) return 1
|
|
331
|
-
if (typeof a === "number" && typeof b === "number") return a - b
|
|
332
|
-
return String(a).localeCompare(String(b), undefined, { numeric: true })
|
|
545
|
+
const own = col.compare
|
|
546
|
+
const value = col.sortValue ?? ((row: T) => row[s.key])
|
|
547
|
+
return [...rows].sort((a, b) => (own ? own(a, b) : compareValues(value(a), value(b))) * factor)
|
|
333
548
|
}
|
|
334
549
|
|
|
550
|
+
const displayRows = computed<T[]>(() => sortRows(filteredRows.value))
|
|
551
|
+
|
|
335
552
|
function toggleSort(col: TableColumn<T>) {
|
|
336
553
|
if (!col.sortable) return
|
|
337
554
|
const s = activeSort.value
|
|
@@ -364,6 +581,20 @@ function sortIcon(col: TableColumn<T>): string {
|
|
|
364
581
|
* Empty for a column that does not sort, and LpContextMenu passes an empty
|
|
365
582
|
* list straight through to the browser's own menu.
|
|
366
583
|
*/
|
|
584
|
+
// Opened from the header menu: the menu has to finish closing and hand focus
|
|
585
|
+
// back before the popover can take it, or the popover reads that focus move as
|
|
586
|
+
// an outside interaction and dismisses itself. So the trigger is clicked once
|
|
587
|
+
// the menu is gone.
|
|
588
|
+
function openFilter(key: string) {
|
|
589
|
+
const open = () => frameEl.value?.querySelector<HTMLElement>(`[data-lp-filter="${CSS.escape(key)}"]`)?.click()
|
|
590
|
+
const started = performance.now()
|
|
591
|
+
const wait = () => {
|
|
592
|
+
if (!document.querySelector("[data-reka-menu-content]") || performance.now() - started > 600) setTimeout(open, 30)
|
|
593
|
+
else requestAnimationFrame(wait)
|
|
594
|
+
}
|
|
595
|
+
requestAnimationFrame(wait)
|
|
596
|
+
}
|
|
597
|
+
|
|
367
598
|
function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
368
599
|
const s = activeSort.value
|
|
369
600
|
const set = (next: SortState | null) => () =>
|
|
@@ -377,19 +608,19 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
|
377
608
|
if (col.sortable) {
|
|
378
609
|
items.push(
|
|
379
610
|
{
|
|
380
|
-
label:
|
|
611
|
+
label: text.value.sortAsc,
|
|
381
612
|
icon: "lucide:arrow-up",
|
|
382
613
|
disabled: s?.key === col.key && s.dir === "asc",
|
|
383
614
|
onSelect: set({ key: col.key, dir: "asc" }),
|
|
384
615
|
},
|
|
385
616
|
{
|
|
386
|
-
label:
|
|
617
|
+
label: text.value.sortDesc,
|
|
387
618
|
icon: "lucide:arrow-down",
|
|
388
619
|
disabled: s?.key === col.key && s.dir === "desc",
|
|
389
620
|
onSelect: set({ key: col.key, dir: "desc" }),
|
|
390
621
|
},
|
|
391
622
|
{
|
|
392
|
-
label:
|
|
623
|
+
label: text.value.sortClear,
|
|
393
624
|
icon: "lucide:x",
|
|
394
625
|
disabled: !s,
|
|
395
626
|
onSelect: set(null),
|
|
@@ -397,29 +628,26 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
|
397
628
|
)
|
|
398
629
|
}
|
|
399
630
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
631
|
+
// The values themselves live in the funnel's popover, where they can be
|
|
632
|
+
// searched, painted and narrowed to one; the menu only leads there, and
|
|
633
|
+
// offers the one-step way back. A second, plainer list here meant two
|
|
634
|
+
// places to filter that behaved differently.
|
|
402
635
|
if (col.filterable) {
|
|
403
636
|
const hidden = hiddenValues.value[col.key]
|
|
404
|
-
items.push(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
disabled: !hidden?.size,
|
|
419
|
-
onSelect: () => showAllValues(col.key),
|
|
420
|
-
},
|
|
421
|
-
],
|
|
422
|
-
})
|
|
637
|
+
items.push(
|
|
638
|
+
{
|
|
639
|
+
label: text.value.filterColumn.replace("{column}", col.label),
|
|
640
|
+
icon: "lucide:filter",
|
|
641
|
+
separatorBefore: items.length > 0,
|
|
642
|
+
onSelect: () => openFilter(col.key),
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
label: text.value.showAll,
|
|
646
|
+
icon: "lucide:list",
|
|
647
|
+
disabled: !hidden?.size,
|
|
648
|
+
onSelect: () => showAllValues(col.key),
|
|
649
|
+
},
|
|
650
|
+
)
|
|
423
651
|
}
|
|
424
652
|
|
|
425
653
|
return items
|
|
@@ -432,10 +660,14 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
|
432
660
|
*/
|
|
433
661
|
function tableMenu(): ContextMenuItemDef[] {
|
|
434
662
|
const offered = props.columns.filter((c) => !c.alwaysVisible && c.label)
|
|
435
|
-
|
|
663
|
+
const exportItem: ContextMenuItemDef[] = props.exportable
|
|
664
|
+
? [{ label: text.value.exportCsv, icon: "lucide:download", onSelect: () => exportCsv() }]
|
|
665
|
+
: []
|
|
666
|
+
if (!offered.length) return exportItem
|
|
436
667
|
return [
|
|
668
|
+
...exportItem,
|
|
437
669
|
{
|
|
438
|
-
label:
|
|
670
|
+
label: text.value.columns,
|
|
439
671
|
icon: "lucide:columns-3",
|
|
440
672
|
children: offered.map((col) => ({
|
|
441
673
|
label: col.label,
|
|
@@ -462,6 +694,13 @@ function fetchPage(delay = 0) {
|
|
|
462
694
|
task.run((signal) => load(query, signal), {
|
|
463
695
|
delay,
|
|
464
696
|
apply: (result) => {
|
|
697
|
+
// Rows deleted from the last page leave it past the end; step back to
|
|
698
|
+
// the page that now exists rather than strand the user on an empty one.
|
|
699
|
+
const last = Math.max(1, Math.ceil(result.total / props.pageSize))
|
|
700
|
+
if (page.value > last) {
|
|
701
|
+
page.value = last
|
|
702
|
+
return
|
|
703
|
+
}
|
|
465
704
|
loaded.value = result.rows
|
|
466
705
|
total.value = result.total
|
|
467
706
|
facets.value = result.facets ?? {}
|
|
@@ -482,13 +721,76 @@ watch([activeSort, hiddenValues], () => {
|
|
|
482
721
|
else fetchPage(props.debounce)
|
|
483
722
|
})
|
|
484
723
|
watch(page, () => fetchPage())
|
|
724
|
+
|
|
725
|
+
// ── remembered state ─────────────────────────────────────────
|
|
726
|
+
// Read after mount, not during setup: a server render knows nothing of this
|
|
727
|
+
// browser's storage, and restoring earlier would make the first client render
|
|
728
|
+
// disagree with it.
|
|
729
|
+
const storageKey = computed(() => (props.persist ? `lp-table:${props.persist}` : ""))
|
|
730
|
+
const restored = ref(false)
|
|
731
|
+
onMounted(() => {
|
|
732
|
+
if (storageKey.value) {
|
|
733
|
+
let raw: string | null = null
|
|
734
|
+
try {
|
|
735
|
+
raw = localStorage.getItem(storageKey.value)
|
|
736
|
+
} catch {}
|
|
737
|
+
const state = parseTableState(raw, props.columns.map((c) => c.key))
|
|
738
|
+
if (state) {
|
|
739
|
+
if (state.sort !== undefined) {
|
|
740
|
+
ownSort.value = state.sort
|
|
741
|
+
emit("update:sort", state.sort)
|
|
742
|
+
}
|
|
743
|
+
if (state.hidden) {
|
|
744
|
+
hiddenValues.value = Object.fromEntries(Object.entries(state.hidden).map(([k, v]) => [k, new Set(v)]))
|
|
745
|
+
}
|
|
746
|
+
if (state.columns) hiddenColumns.value = new Set(state.columns)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
restored.value = true
|
|
750
|
+
})
|
|
751
|
+
watch([activeSort, hiddenValues, hiddenColumns], () => {
|
|
752
|
+
if (!storageKey.value || !restored.value) return
|
|
753
|
+
const state = serializeTableState({
|
|
754
|
+
sort: activeSort.value,
|
|
755
|
+
hidden: Object.fromEntries(Object.entries(hiddenValues.value).map(([k, v]) => [k, [...v]])),
|
|
756
|
+
columns: [...hiddenColumns.value],
|
|
757
|
+
})
|
|
758
|
+
try {
|
|
759
|
+
localStorage.setItem(storageKey.value, state)
|
|
760
|
+
} catch {}
|
|
761
|
+
})
|
|
485
762
|
watch(() => props.load, () => fetchPage(), { immediate: true })
|
|
486
763
|
|
|
487
|
-
|
|
764
|
+
// ── export ───────────────────────────────────────────────────
|
|
765
|
+
// The columns on screen, minus the unnamed ones (actions), in the order shown.
|
|
766
|
+
function exportCsv(options: TableExportOptions = {}): string {
|
|
767
|
+
const scope = options.scope ?? "filtered"
|
|
768
|
+
let rows: T[]
|
|
769
|
+
if (scope === "all") rows = sortRows(sourceRows.value)
|
|
770
|
+
else if (scope === "selected") {
|
|
771
|
+
const keys = new Set(props.selected)
|
|
772
|
+
rows = sortRows(sourceRows.value).filter((row, i) => keys.has(keyFor(row, i)))
|
|
773
|
+
} else rows = displayRows.value
|
|
774
|
+
const columns = visibleColumns.value
|
|
775
|
+
.filter((col) => col.label)
|
|
776
|
+
.map((col) => ({
|
|
777
|
+
key: col.key,
|
|
778
|
+
label: col.label,
|
|
779
|
+
value: col.exportValue ?? ((row: T) => row[col.key]),
|
|
780
|
+
}))
|
|
781
|
+
const csv = toCsv(rows, columns, options)
|
|
782
|
+
if (options.download !== false) {
|
|
783
|
+
downloadFile(`${fileStem(options.filename ?? props.exportFilename, "table")}.csv`, csv, "text/csv;charset=utf-8")
|
|
784
|
+
}
|
|
785
|
+
return csv
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
defineExpose({ reload: () => fetchPage(), exportCsv })
|
|
488
789
|
|
|
489
790
|
// ── selection ────────────────────────────────────────────────
|
|
490
791
|
const selectedSet = computed(() => new Set(props.selected))
|
|
491
|
-
|
|
792
|
+
// "All" is what the user can see: rows a header filter hides are left alone.
|
|
793
|
+
const allKeys = computed(() => filteredRows.value.map((r, i) => keyFor(r, i)))
|
|
492
794
|
const allChecked = computed(
|
|
493
795
|
() => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k)),
|
|
494
796
|
)
|
|
@@ -497,7 +799,12 @@ const someChecked = computed(
|
|
|
497
799
|
)
|
|
498
800
|
|
|
499
801
|
function toggleAll(checked: boolean) {
|
|
500
|
-
|
|
802
|
+
const next = new Set(selectedSet.value)
|
|
803
|
+
for (const key of allKeys.value) {
|
|
804
|
+
if (checked) next.add(key)
|
|
805
|
+
else next.delete(key)
|
|
806
|
+
}
|
|
807
|
+
emit("update:selected", [...next])
|
|
501
808
|
}
|
|
502
809
|
|
|
503
810
|
function toggleRow(key: RowKey, checked: boolean) {
|
|
@@ -507,7 +814,111 @@ function toggleRow(key: RowKey, checked: boolean) {
|
|
|
507
814
|
emit("update:selected", [...next])
|
|
508
815
|
}
|
|
509
816
|
|
|
510
|
-
|
|
817
|
+
// Press a row's tick and sweep down the column to tick (or clear) the run,
|
|
818
|
+
// as in a file manager; Ctrl+A inside the table ticks every row shown.
|
|
819
|
+
const keyByText = computed(
|
|
820
|
+
() => new Map(displayRows.value.map((row, index) => [String(keyFor(row, index)), keyFor(row, index)])),
|
|
821
|
+
)
|
|
822
|
+
const paint = usePaintSelect<RowKey>({
|
|
823
|
+
root: frameEl,
|
|
824
|
+
enabled: () => !!props.selectable,
|
|
825
|
+
selection: () => props.selected,
|
|
826
|
+
set: (keys) => emit("update:selected", keys),
|
|
827
|
+
resolve: (raw) => keyByText.value.get(raw),
|
|
828
|
+
selectAll: () => [...new Set([...props.selected, ...allKeys.value])],
|
|
829
|
+
help: () => ({ group: text.value.shortcutsGroup, label: text.value.selectAll }),
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
const holdMode = computed(() => props.selectable === "hold")
|
|
833
|
+
/*
|
|
834
|
+
* In hold mode the tick column is there all along, collapsed to nothing, and
|
|
835
|
+
* opens by easing its width: the other columns then slide over instead of
|
|
836
|
+
* jumping when it appears.
|
|
837
|
+
*/
|
|
838
|
+
const ticksShown = computed(
|
|
839
|
+
() => props.selectable === true || props.selected.length > 0 || paint.painting.value,
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
function clearSelection() {
|
|
843
|
+
emit("update:selected", [])
|
|
844
|
+
if (props.allMatching) emit("update:allMatching", false)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/*
|
|
848
|
+
* A long press on a row starts selection in hold mode. Presses that begin on
|
|
849
|
+
* something the row holds that answers clicks of its own (a link, a button, a
|
|
850
|
+
* field) are left to it.
|
|
851
|
+
*/
|
|
852
|
+
const OWN_CLICKS = "a,button,input,select,textarea,label,[role=button],[role=checkbox],[contenteditable=true]"
|
|
853
|
+
function onRowPointerDown(event: PointerEvent, key: RowKey) {
|
|
854
|
+
if (!holdMode.value) return
|
|
855
|
+
if ((event.target as Element | null)?.closest(OWN_CLICKS)) return
|
|
856
|
+
paint.onPointerDown(event, key, { hold: 420 })
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Once something is selected, a click on a row adds or removes it, as in a
|
|
860
|
+
// phone's selection mode; with nothing selected it is the row's click again.
|
|
861
|
+
function onRowClick(row: T, key: RowKey) {
|
|
862
|
+
if (holdMode.value && props.selected.length) toggleRow(key, !selectedSet.value.has(key))
|
|
863
|
+
else emit("rowClick", row)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
useHotkeys(
|
|
867
|
+
() =>
|
|
868
|
+
props.selectable && props.selected.length
|
|
869
|
+
? [
|
|
870
|
+
{
|
|
871
|
+
key: "escape",
|
|
872
|
+
help: { group: text.value.shortcutsGroup, label: text.value.clearSelection },
|
|
873
|
+
handler: () => clearSelection(),
|
|
874
|
+
},
|
|
875
|
+
]
|
|
876
|
+
: [],
|
|
877
|
+
{ scope: frameEl },
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
// ── the bar above the table ──────────────────────────────────
|
|
881
|
+
const pageKeys = computed(() => displayRows.value.map((row, i) => keyFor(row, i)))
|
|
882
|
+
const pageAllSelected = computed(
|
|
883
|
+
() => pageKeys.value.length > 0 && pageKeys.value.every((k) => selectedSet.value.has(k)),
|
|
884
|
+
)
|
|
885
|
+
// "Select all" beyond the page is a separate, explicit step: the header tick
|
|
886
|
+
// only ever means the rows the user can see.
|
|
887
|
+
watch(pageAllSelected, (all) => {
|
|
888
|
+
if (!all && props.allMatching) emit("update:allMatching", false)
|
|
889
|
+
})
|
|
890
|
+
/*
|
|
891
|
+
* One status line that rolls between its states ("60 rows", "3 selected",
|
|
892
|
+
* "All 40 are selected") instead of swapping text in a frame; the controls
|
|
893
|
+
* beside it fade in and out, and the group eases its width so they slide.
|
|
894
|
+
*/
|
|
895
|
+
const statusText = computed(() => {
|
|
896
|
+
if (!(props.selectable && props.selected.length)) return countText.value
|
|
897
|
+
return props.allMatching
|
|
898
|
+
? text.value.allMatchingSelected.replace("{total}", String(total.value))
|
|
899
|
+
: text.value.selectedCount.replace("{count}", String(props.selected.length))
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
const countText = computed(() => {
|
|
903
|
+
const t = text.value
|
|
904
|
+
if (props.load) return t.rowsTotal.replace("{total}", String(total.value))
|
|
905
|
+
if (displayRows.value.length !== props.rows.length) {
|
|
906
|
+
return t.rowCount.replace("{shown}", String(displayRows.value.length)).replace("{total}", String(props.rows.length))
|
|
907
|
+
}
|
|
908
|
+
return t.rowsTotal.replace("{total}", String(props.rows.length))
|
|
909
|
+
})
|
|
910
|
+
// A selectable table keeps the bar at all times (the row count until something
|
|
911
|
+
// is selected): ticking the first row must not push the table down.
|
|
912
|
+
const showBar = computed(() => props.summary || !!props.selectable || activeFilters.value.length > 0)
|
|
913
|
+
const filteredOut = computed(() => {
|
|
914
|
+
if (loading.value || failed.value || displayRows.value.length) return false
|
|
915
|
+
if (props.load) return Object.values(hiddenValues.value).some((set) => set.size)
|
|
916
|
+
return props.rows.length > 0
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
const cellY = computed(() => (props.density === "compact" ? "py-1.5" : "py-3"))
|
|
920
|
+
|
|
921
|
+
const colSpan = computed(() => visibleColumns.value.length + (props.selectable ? 1 : 0))
|
|
511
922
|
|
|
512
923
|
/*
|
|
513
924
|
* Keyboard navigation over the rows. A table is one Tab stop, not one per row:
|
|
@@ -592,7 +1003,98 @@ const barInsetTop = computed(() =>
|
|
|
592
1003
|
<template>
|
|
593
1004
|
<!-- No top fade: a sticky header sits in the masked band and would be dimmed
|
|
594
1005
|
by it. The bottom edge is left crisp with it for symmetry. -->
|
|
595
|
-
<div class="flex flex-col gap-3" :class="rootClass">
|
|
1006
|
+
<div ref="frameEl" class="flex flex-col gap-3" :class="rootClass">
|
|
1007
|
+
<div v-if="exportable" class="flex justify-end">
|
|
1008
|
+
<LpButton variant="ghost" size="sm" @click="exportCsv()">
|
|
1009
|
+
<LpIcon name="lucide:download" :size="14" />
|
|
1010
|
+
{{ text.exportCsv }}
|
|
1011
|
+
</LpButton>
|
|
1012
|
+
</div>
|
|
1013
|
+
<!-- What the table is showing right now: the filters in force, each one
|
|
1014
|
+
a chip that clears it, the row count, and what is selected with room
|
|
1015
|
+
for the consumer's bulk actions. Eased in and out (LpShift), so the
|
|
1016
|
+
table below glides rather than jumps when the first filter is set. -->
|
|
1017
|
+
<!-- The gap below the bar lives inside the eased box (pb-3), cancelled
|
|
1018
|
+
outside it (-mb-3): an empty box must not leave the root's gap behind. -->
|
|
1019
|
+
<LpShift class="-mb-3">
|
|
1020
|
+
<div v-if="showBar" class="flex min-h-8 items-center gap-x-3 pb-3 text-xs">
|
|
1021
|
+
<div v-if="activeFilters.length" class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
|
|
1022
|
+
<span
|
|
1023
|
+
v-for="f in activeFilters"
|
|
1024
|
+
:key="f.col.key"
|
|
1025
|
+
class="inline-flex max-w-72 animate-[chip-in_var(--duration-fast)_var(--ease-emphasized)] items-center gap-1 rounded-pill bg-brand-soft py-0.5 pr-1 pl-2.5 text-ink motion-reduce:animate-none"
|
|
1026
|
+
>
|
|
1027
|
+
<span class="truncate"><span class="text-muted">{{ f.col.label }}:</span> {{ f.text }}</span>
|
|
1028
|
+
<button
|
|
1029
|
+
type="button"
|
|
1030
|
+
class="grid size-4 shrink-0 place-items-center rounded-pill text-muted outline-none transition-colors duration-fast hover:bg-ink/10 hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1031
|
+
:aria-label="text.clearFilter.replace('{column}', f.col.label)"
|
|
1032
|
+
@click="showAllValues(f.col.key)"
|
|
1033
|
+
>
|
|
1034
|
+
<LpIcon name="lucide:x" :size="12" />
|
|
1035
|
+
</button>
|
|
1036
|
+
</span>
|
|
1037
|
+
<button
|
|
1038
|
+
v-if="activeFilters.length > 1"
|
|
1039
|
+
type="button"
|
|
1040
|
+
class="rounded-control px-1.5 py-0.5 font-medium text-muted outline-none transition-colors duration-fast hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1041
|
+
@click="clearAllFilters"
|
|
1042
|
+
>
|
|
1043
|
+
{{ text.clearFilters }}
|
|
1044
|
+
</button>
|
|
1045
|
+
</div>
|
|
1046
|
+
<!-- One line, always: the status and its controls never wrap, so the bar
|
|
1047
|
+
keeps its height and the table under it stays put; the group eases
|
|
1048
|
+
its width as controls come and go. -->
|
|
1049
|
+
<LpShift axis="width" class="ml-auto shrink-0">
|
|
1050
|
+
<div class="flex items-center gap-2 whitespace-nowrap text-muted">
|
|
1051
|
+
<LpRollingText
|
|
1052
|
+
:value="statusText"
|
|
1053
|
+
align="end"
|
|
1054
|
+
class="tabular-nums"
|
|
1055
|
+
:class="selectable && selected.length ? 'font-medium text-ink' : ''"
|
|
1056
|
+
aria-live="polite"
|
|
1057
|
+
/>
|
|
1058
|
+
<Transition
|
|
1059
|
+
enter-active-class="transition-opacity duration-fast motion-reduce:transition-none"
|
|
1060
|
+
leave-active-class="transition-opacity duration-fast motion-reduce:transition-none"
|
|
1061
|
+
enter-from-class="opacity-0"
|
|
1062
|
+
leave-to-class="opacity-0"
|
|
1063
|
+
>
|
|
1064
|
+
<span v-if="selectable && selected.length" class="flex items-center gap-2">
|
|
1065
|
+
<Transition
|
|
1066
|
+
enter-active-class="transition-opacity duration-fast motion-reduce:transition-none"
|
|
1067
|
+
leave-active-class="transition-opacity duration-fast motion-reduce:transition-none"
|
|
1068
|
+
enter-from-class="opacity-0"
|
|
1069
|
+
leave-to-class="opacity-0"
|
|
1070
|
+
>
|
|
1071
|
+
<span v-if="load && pageAllSelected && !allMatching && total > pageKeys.length" class="flex items-center gap-2">
|
|
1072
|
+
<!-- The sentence is context, the button is the action: on a narrow
|
|
1073
|
+
screen only the action stays, so the line still fits. -->
|
|
1074
|
+
<span class="hidden sm:inline">{{ text.pageSelected.replace("{count}", String(pageKeys.length)) }}</span>
|
|
1075
|
+
<button
|
|
1076
|
+
type="button"
|
|
1077
|
+
class="rounded-control px-1.5 py-0.5 font-medium text-brand outline-none transition-colors duration-fast hover:bg-brand-soft focus-visible:ring-2 focus-visible:ring-ring"
|
|
1078
|
+
@click="emit('update:allMatching', true)"
|
|
1079
|
+
>
|
|
1080
|
+
{{ text.selectAllMatching.replace("{total}", String(total)) }}
|
|
1081
|
+
</button>
|
|
1082
|
+
</span>
|
|
1083
|
+
</Transition>
|
|
1084
|
+
<button
|
|
1085
|
+
type="button"
|
|
1086
|
+
class="rounded-control px-1.5 py-0.5 font-medium outline-none transition-colors duration-fast hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1087
|
+
@click="clearSelection"
|
|
1088
|
+
>
|
|
1089
|
+
{{ text.clearSelection }}
|
|
1090
|
+
</button>
|
|
1091
|
+
<slot name="bulk" :selected="selected" :all-matching="allMatching" :clear="clearSelection" />
|
|
1092
|
+
</span>
|
|
1093
|
+
</Transition>
|
|
1094
|
+
</div>
|
|
1095
|
+
</LpShift>
|
|
1096
|
+
</div>
|
|
1097
|
+
</LpShift>
|
|
596
1098
|
<LpScrollArea
|
|
597
1099
|
v-bind="scrollAttrs"
|
|
598
1100
|
:fade="false"
|
|
@@ -604,7 +1106,7 @@ const barInsetTop = computed(() =>
|
|
|
604
1106
|
reaches: rows and headers carry their own and take the event first, so
|
|
605
1107
|
this is the fallback rather than a competitor. -->
|
|
606
1108
|
<LpContextMenu :items="contextMenu" always>
|
|
607
|
-
<table
|
|
1109
|
+
<table class="w-full border-collapse text-sm">
|
|
608
1110
|
<!-- The background belongs on `thead` itself, not only on its row:
|
|
609
1111
|
`border-collapse` collapses the borders, and with a sticky header the
|
|
610
1112
|
rows show through that seam — a band of body text rides across the
|
|
@@ -614,19 +1116,21 @@ const barInsetTop = computed(() =>
|
|
|
614
1116
|
:class="stickyHeader ? 'sticky top-0 z-10 bg-surface-soft' : ''"
|
|
615
1117
|
>
|
|
616
1118
|
<tr class="border-b border-line bg-surface-soft">
|
|
617
|
-
<th v-if="selectable" class="w-px
|
|
618
|
-
<
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1119
|
+
<th v-if="selectable" class="w-px p-0" :class="cellY">
|
|
1120
|
+
<div class="lp-tick lp-tick-open" :class="ticksShown ? '' : 'lp-tick-hidden'" :inert="!ticksShown">
|
|
1121
|
+
<LpCheckbox
|
|
1122
|
+
:model-value="allChecked"
|
|
1123
|
+
:indeterminate="someChecked"
|
|
1124
|
+
@update:model-value="toggleAll"
|
|
1125
|
+
/>
|
|
1126
|
+
</div>
|
|
623
1127
|
</th>
|
|
624
1128
|
<th
|
|
625
1129
|
v-for="col in visibleColumns"
|
|
626
1130
|
:key="col.key"
|
|
627
1131
|
:style="col.width ? { width: col.width } : undefined"
|
|
628
|
-
class="px-4
|
|
629
|
-
:class="alignClass(col.align)"
|
|
1132
|
+
class="px-4 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
|
|
1133
|
+
:class="[alignClass(col.align), cellY]"
|
|
630
1134
|
:aria-sort="
|
|
631
1135
|
activeSort?.key === col.key
|
|
632
1136
|
? activeSort.dir === 'asc'
|
|
@@ -654,14 +1158,7 @@ const barInsetTop = computed(() =>
|
|
|
654
1158
|
>
|
|
655
1159
|
{{ col.label }}
|
|
656
1160
|
<LpIcon
|
|
657
|
-
v-if="
|
|
658
|
-
name="lucide:filter"
|
|
659
|
-
:size="12"
|
|
660
|
-
class="shrink-0"
|
|
661
|
-
:class="hiddenValues[col.key]?.size ? 'text-brand opacity-100' : 'opacity-40 group-hover/sort:opacity-70'"
|
|
662
|
-
/>
|
|
663
|
-
<LpIcon
|
|
664
|
-
v-else
|
|
1161
|
+
v-if="col.sortable"
|
|
665
1162
|
:name="sortIcon(col)"
|
|
666
1163
|
:size="13"
|
|
667
1164
|
class="shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]"
|
|
@@ -674,6 +1171,30 @@ const barInsetTop = computed(() =>
|
|
|
674
1171
|
</button>
|
|
675
1172
|
</LpContextMenu>
|
|
676
1173
|
<template v-else>{{ col.label }}</template>
|
|
1174
|
+
<LpPopover
|
|
1175
|
+
v-if="col.filterable"
|
|
1176
|
+
align="start"
|
|
1177
|
+
panel-class="w-72"
|
|
1178
|
+
:aria-label="text.filterColumn.replace('{column}', col.label)"
|
|
1179
|
+
>
|
|
1180
|
+
<template #trigger>
|
|
1181
|
+
<button
|
|
1182
|
+
type="button"
|
|
1183
|
+
class="ml-0.5 inline-grid size-5 place-items-center rounded align-middle outline-none transition-colors duration-fast hover:bg-surface-raised hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
1184
|
+
:class="hiddenValues[col.key]?.size ? 'text-brand' : 'opacity-60'"
|
|
1185
|
+
:aria-label="text.filterColumn.replace('{column}', col.label)"
|
|
1186
|
+
:data-lp-filter="col.key"
|
|
1187
|
+
>
|
|
1188
|
+
<LpIcon :name="hiddenValues[col.key]?.size ? 'lucide:filter-x' : 'lucide:filter'" :size="12" />
|
|
1189
|
+
</button>
|
|
1190
|
+
</template>
|
|
1191
|
+
<LpTableFilter
|
|
1192
|
+
:options="filterOptions(col)"
|
|
1193
|
+
:hidden="hiddenValues[col.key] ?? new Set()"
|
|
1194
|
+
:labels="filterLabels()"
|
|
1195
|
+
@update:hidden="(next) => setHidden(col.key, next)"
|
|
1196
|
+
/>
|
|
1197
|
+
</LpPopover>
|
|
677
1198
|
</th>
|
|
678
1199
|
</tr>
|
|
679
1200
|
</thead>
|
|
@@ -698,12 +1219,15 @@ const barInsetTop = computed(() =>
|
|
|
698
1219
|
<tr v-if="displayRows.length === 0" key="lp-table-empty" class="lp-row-empty">
|
|
699
1220
|
<td :colspan="colSpan" class="px-4 py-10 text-center text-muted">
|
|
700
1221
|
<LpIcon
|
|
701
|
-
:name="loading ? 'lucide:loader-circle' : failed ? 'lucide:circle-alert' : emptyIcon"
|
|
1222
|
+
:name="loading ? 'lucide:loader-circle' : failed ? 'lucide:circle-alert' : filteredOut ? 'lucide:filter-x' : emptyIcon"
|
|
702
1223
|
:size="22"
|
|
703
1224
|
class="mx-auto mb-2 opacity-60"
|
|
704
1225
|
:class="loading ? 'animate-spin' : ''"
|
|
705
1226
|
/>
|
|
706
|
-
<div>{{ loading ?
|
|
1227
|
+
<div>{{ loading ? text.loading : failed ? text.error : filteredOut ? text.noMatches : text.empty }}</div>
|
|
1228
|
+
<LpButton v-if="filteredOut" variant="outline" size="sm" class="mt-3" @click="clearAllFilters">
|
|
1229
|
+
{{ text.resetFilters }}
|
|
1230
|
+
</LpButton>
|
|
707
1231
|
</td>
|
|
708
1232
|
</tr>
|
|
709
1233
|
<!-- A plain <tr>, because TransitionGroup animates element children and
|
|
@@ -716,20 +1240,30 @@ const barInsetTop = computed(() =>
|
|
|
716
1240
|
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"
|
|
717
1241
|
:class="selectedSet.has(keyFor(row, index)) ? 'bg-brand-soft/40' : ''"
|
|
718
1242
|
v-bind="roving.itemProps(String(keyFor(row, index)))"
|
|
719
|
-
|
|
1243
|
+
:data-paint-key="selectable ? String(keyFor(row, index)) : undefined"
|
|
1244
|
+
@click="onRowClick(row, keyFor(row, index))"
|
|
1245
|
+
@pointerdown="onRowPointerDown($event, keyFor(row, index))"
|
|
720
1246
|
@keydown="onRowKeydown"
|
|
721
1247
|
>
|
|
722
|
-
<td
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1248
|
+
<td
|
|
1249
|
+
v-if="selectable"
|
|
1250
|
+
class="w-px cursor-pointer p-0 select-none"
|
|
1251
|
+
:class="cellY"
|
|
1252
|
+
@click.stop
|
|
1253
|
+
@pointerdown.stop="paint.onPointerDown($event, keyFor(row, index))"
|
|
1254
|
+
>
|
|
1255
|
+
<div class="lp-tick lp-tick-open" :class="ticksShown ? '' : 'lp-tick-cue'">
|
|
1256
|
+
<LpCheckbox
|
|
1257
|
+
:model-value="selectedSet.has(keyFor(row, index))"
|
|
1258
|
+
@update:model-value="(v) => toggleRow(keyFor(row, index), v)"
|
|
1259
|
+
/>
|
|
1260
|
+
</div>
|
|
727
1261
|
</td>
|
|
728
1262
|
<td
|
|
729
|
-
v-for="col in visibleColumns"
|
|
1263
|
+
v-for="(col, ci) in visibleColumns"
|
|
730
1264
|
:key="col.key"
|
|
731
|
-
class="px-4
|
|
732
|
-
:class="alignClass(col.align)"
|
|
1265
|
+
class="px-4 text-ink"
|
|
1266
|
+
:class="[alignClass(col.align), cellY, col.nowrap ? 'whitespace-nowrap' : '']"
|
|
733
1267
|
>
|
|
734
1268
|
<slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]">
|
|
735
1269
|
{{ row[col.key] }}
|
|
@@ -801,12 +1335,56 @@ const barInsetTop = computed(() =>
|
|
|
801
1335
|
* which is a moment slower and stays a table the whole time.
|
|
802
1336
|
*/
|
|
803
1337
|
|
|
1338
|
+
/*
|
|
1339
|
+
* The tick column of a hold-to-select table keeps its width and only hides its
|
|
1340
|
+
* boxes until they mean something, which is the desktop pattern Material
|
|
1341
|
+
* describes: a row's box shows while the row is hovered, and every box shows
|
|
1342
|
+
* once anything is selected. An earlier version collapsed the column to nothing
|
|
1343
|
+
* and floated the hover box over the first cell, where it sat on the text.
|
|
1344
|
+
*/
|
|
1345
|
+
.lp-tick {
|
|
1346
|
+
display: flex;
|
|
1347
|
+
align-items: center;
|
|
1348
|
+
max-width: 0;
|
|
1349
|
+
padding-inline: 0;
|
|
1350
|
+
overflow: hidden;
|
|
1351
|
+
opacity: 0;
|
|
1352
|
+
transition:
|
|
1353
|
+
max-width var(--duration-medium) var(--ease-emphasized),
|
|
1354
|
+
padding var(--duration-medium) var(--ease-emphasized),
|
|
1355
|
+
opacity var(--duration-fast) ease;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
.lp-tick-open {
|
|
1359
|
+
max-width: 3.25rem;
|
|
1360
|
+
padding-inline: 1rem;
|
|
1361
|
+
opacity: 1;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.lp-tick-hidden,
|
|
1365
|
+
.lp-tick-cue {
|
|
1366
|
+
opacity: 0;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
tr:hover .lp-tick-cue,
|
|
1370
|
+
.lp-tick-cue:focus-within {
|
|
1371
|
+
opacity: 1;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
804
1374
|
/* A filter that redraws the list is information, not decoration: with motion
|
|
805
1375
|
turned down the rows change without animating rather than not changing. */
|
|
1376
|
+
/* Only where there is a hover to reveal it; a finger long-presses instead. */
|
|
1377
|
+
@media (hover: none) {
|
|
1378
|
+
tr:hover .lp-tick-cue {
|
|
1379
|
+
opacity: 0;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
806
1383
|
@media (prefers-reduced-motion: reduce) {
|
|
807
1384
|
.lp-row-enter-active,
|
|
808
1385
|
.lp-row-leave-active,
|
|
809
|
-
.lp-row-move
|
|
1386
|
+
.lp-row-move,
|
|
1387
|
+
.lp-tick {
|
|
810
1388
|
transition: none;
|
|
811
1389
|
}
|
|
812
1390
|
}
|