@leavepulse/ui 0.43.3 → 0.44.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 +75 -7
- package/dist/assets/{LpCommandPalette-DH7sPcKP.css → LpCommandPalette-DCP5g2ik.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/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-ChUyU7oV.css} +7 -7
- package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
- package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-DDQNdf86.css} +9 -9
- package/dist/chunks/ContextMenuItems-CrfeNPWT.js +123 -0
- package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-D-jPOAvg.js} +26 -9
- package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-D8Xy9N-o.js} +20 -8
- package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-BbwCF6l5.js} +24 -10
- package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BrOqkZqg.js} +53 -48
- package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-DkOIjsbq.js} +15 -8
- package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-BUKPcHco.js} +51 -24
- package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-DxV3Y9h4.js} +50 -23
- package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-ClBbEYZN.js} +17 -8
- package/dist/chunks/LpBreadcrumbs-B8IJgFIc.js +95 -0
- package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-kqJBHIIb.js} +44 -14
- package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-CpRByR0q.js} +1 -1
- package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-DpqchzH4.js} +7 -3
- package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-DeHJPq7p.js} +48 -16
- package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-Bhf1qxhB.js} +32 -9
- package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-CIUOSnWo.js} +58 -16
- package/dist/chunks/LpConfirmHost-CDm0Wtpv.js +49 -0
- package/dist/chunks/LpContextMenu-DQw5dGfG.js +68 -0
- package/dist/chunks/LpDatePicker-Drirx1AK.js +191 -0
- package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-bScx-KMO.js} +63 -26
- package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CSI_5SK2.js} +23 -8
- package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-B4MiSQX2.js} +2 -2
- package/dist/chunks/LpFormField-CIk5qosh.js +87 -0
- package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-C0e8y8_d.js} +26 -11
- package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-Mr0W_6HK.js} +147 -60
- package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
- package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-YBOaFnc_.js} +68 -34
- package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-NUzS9e37.js} +38 -13
- package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-DfJmMW0I.js} +6 -3
- package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DPwGKBPv.js} +49 -26
- package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-DPg5H9f3.js} +9 -6
- package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-t5yn-siX.js} +58 -24
- package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-DWnQKktS.js} +20 -7
- 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-DFngX7Xo.js} +30 -19
- package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-CWVDuBJ5.js} +10 -7
- package/dist/chunks/{LpPhoneInput-BDSKVl_z.js → LpPhoneInput-CD-ZTeSd.js} +57 -33
- package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BLco31Nx.js} +78 -21
- package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-p_e_shN3.js} +4 -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/{LpSegmented-fV0Sk92p.js → LpSegmented-Dejd7cIx.js} +7 -3
- package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DrRD9I7L.js} +40 -18
- package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-BgwUNWpJ.js} +30 -22
- package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-Bqxz8aWT.js} +14 -9
- 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-dXtyrdX9.js} +1 -1
- package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-B1aLKulj.js} +40 -26
- package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
- package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-_qMVaZJ6.js} +19 -5
- package/dist/chunks/LpTextarea-yP8hGedR.js +77 -0
- package/dist/chunks/LpThemeProvider-Wjm7Tuit.js +35 -0
- package/dist/chunks/LpThemeSwitcher-CKLc10KZ.js +169 -0
- package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
- package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-C5HRgS5H.js} +13 -7
- package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster--oT6lfhL.js} +57 -28
- package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CBdzeKNf.js} +13 -6
- package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
- package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-DocsLal3.js} +6 -1
- package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-DLtxOMRy.js} +43 -14
- package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-C8JHUlPM.js} +173 -215
- 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/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.d.ts +31 -2
- package/dist/components/LpChart.vue.js +317 -95
- 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.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +8 -10
- package/dist/components/LpCommandPalette.vue.js +88 -78
- 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/LpDrawer.vue.d.ts +10 -0
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +22 -6
- 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 +9 -0
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +92 -57
- package/dist/components/LpInput.vue.d.ts +3 -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 +20 -2
- 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.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.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpSection.vue.d.ts +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 +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +11 -8
- 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/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +23 -1
- package/dist/components/LpTable.vue.js +74 -36
- 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/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 +13 -0
- 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 +39 -4
- 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.d.ts +0 -1
- package/dist/components/commandSearch.js +1 -17
- 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/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/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/sidebar.d.ts +7 -1
- package/dist/components/tableSort.d.ts +8 -0
- package/dist/components/tableSort.js +31 -0
- package/dist/composables/useField.d.ts +34 -0
- package/dist/composables/useField.js +45 -0
- package/dist/composables/useFormDraft.d.ts +10 -3
- package/dist/composables/useFormDraft.js +9 -14
- package/dist/composables/useHotkeys.d.ts +17 -1
- package/dist/composables/useHotkeys.js +55 -18
- package/dist/composables/useInputFilter.d.ts +17 -0
- package/dist/composables/useInputFilter.js +47 -10
- package/dist/composables/useLpConfig.d.ts +24 -0
- package/dist/composables/useLpConfig.js +44 -0
- package/dist/composables/useRovingFocus.js +7 -0
- 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/index.d.ts +33 -3
- package/dist/index.js +185 -139
- 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/useTheme.d.ts +32 -9
- package/package.json +8 -7
- package/src/component-names.ts +5 -7
- package/src/components/ContextMenuItems.vue +88 -0
- package/src/components/LayoutCanvas.vue +26 -6
- package/src/components/LayoutNode.vue +19 -2
- package/src/components/LpActionBar.vue +30 -7
- 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 +46 -10
- package/src/components/LpAvatar.vue +19 -5
- package/src/components/LpBreadcrumbs.vue +65 -41
- package/src/components/LpButton.vue +59 -13
- package/src/components/LpChart.vue +272 -28
- package/src/components/LpCheckbox.vue +7 -3
- package/src/components/LpCodeBlock.vue +50 -14
- package/src/components/LpColorPicker.vue +39 -9
- package/src/components/LpCommandPalette.vue +36 -25
- package/src/components/LpConfirmDialog.vue +68 -17
- package/src/components/LpConfirmHost.vue +59 -0
- package/src/components/LpContextMenu.vue +14 -115
- package/src/components/LpDatePicker.vue +99 -68
- package/src/components/LpDrawer.vue +56 -19
- package/src/components/LpDropzone.vue +21 -4
- package/src/components/LpFileTree.vue +33 -7
- package/src/components/LpFormField.vue +30 -7
- package/src/components/LpImageEditor.vue +96 -57
- package/src/components/LpInput.vue +18 -7
- package/src/components/LpLightbox.vue +140 -29
- package/src/components/LpLink.vue +24 -13
- package/src/components/LpLogViewer.vue +86 -17
- package/src/components/LpMapPicker.vue +36 -13
- package/src/components/LpMediaGallery.vue +8 -2
- package/src/components/LpModal.vue +53 -27
- package/src/components/LpModalNav.vue +5 -2
- package/src/components/LpNotificationBell.vue +63 -17
- 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 +53 -27
- package/src/components/LpPie.vue +94 -15
- package/src/components/LpPopover.vue +3 -0
- package/src/components/LpProgress.vue +5 -3
- package/src/components/LpRadioGroup.vue +9 -2
- package/src/components/LpSegmented.vue +12 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +8 -3
- 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/LpSwitch.vue +30 -7
- package/src/components/LpTable.vue +91 -32
- package/src/components/LpTableOfContents.vue +50 -32
- package/src/components/LpTabs.vue +23 -6
- package/src/components/LpTextarea.vue +44 -6
- package/src/components/LpThemeProvider.vue +41 -0
- package/src/components/LpThemeSwitcher.vue +64 -17
- package/src/components/LpTilt.vue +2 -2
- package/src/components/LpTimeField.vue +14 -3
- package/src/components/LpToaster.vue +66 -26
- package/src/components/LpTooltip.vue +22 -5
- package/src/components/LpTooltipProvider.vue +24 -0
- package/src/components/LpTopologyCanvas.vue +12 -1
- package/src/components/LpUptimeBar.vue +46 -8
- package/src/components/chart.ts +77 -5
- package/src/components/codeHighlight.ts +111 -15
- package/src/components/color.ts +30 -9
- package/src/components/commandSearch.ts +0 -17
- package/src/components/confirm.ts +58 -0
- package/src/components/countries.ts +12 -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/modalDepth.ts +48 -6
- package/src/components/otp.ts +16 -0
- package/src/components/sidebar.ts +7 -1
- package/src/components/tableSort.ts +37 -0
- package/src/composables/useField.ts +79 -0
- package/src/composables/useFormDraft.ts +25 -20
- package/src/composables/useHotkeys.ts +89 -18
- package/src/composables/useInputFilter.ts +77 -14
- package/src/composables/useLpConfig.ts +87 -0
- package/src/composables/useRovingFocus.ts +11 -0
- package/src/composables/useTilt.ts +5 -6
- package/src/composables/useToast.ts +56 -4
- package/src/composables/useZoomPan.ts +6 -6
- package/src/index.ts +38 -3
- package/src/layout/tree.ts +30 -0
- package/src/layout/useLayout.ts +68 -18
- package/src/nuxt.ts +59 -17
- package/src/runtime/config.plugin.ts +14 -0
- package/src/runtime/theme.plugin.ts +42 -22
- package/src/theme/useTheme.ts +206 -100
- 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/LpTextarea-t7F6gv8E.js +0 -47
|
@@ -16,6 +16,7 @@ export default { inheritAttrs: false }
|
|
|
16
16
|
import { Motion } from "motion-v"
|
|
17
17
|
import { ToggleGroupItem, ToggleGroupRoot } from "reka-ui"
|
|
18
18
|
import { useId } from "vue"
|
|
19
|
+
import { useFieldControl } from "../composables/useField"
|
|
19
20
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
20
21
|
import { usePillTransition } from "../composables/usePillTransition"
|
|
21
22
|
import LpIcon from "./LpIcon.vue"
|
|
@@ -25,6 +26,11 @@ export interface SegmentedOption {
|
|
|
25
26
|
label?: string
|
|
26
27
|
/** Optional iconify name shown before the label (or alone, icon-only). */
|
|
27
28
|
icon?: string
|
|
29
|
+
/**
|
|
30
|
+
* Accessible name for an icon-only segment. Without it the raw `value` is
|
|
31
|
+
* announced, which is an identifier rather than words.
|
|
32
|
+
*/
|
|
33
|
+
ariaLabel?: string
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
const props = withDefaults(
|
|
@@ -66,6 +72,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
66
72
|
].join(" "),
|
|
67
73
|
)
|
|
68
74
|
|
|
75
|
+
const field = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }))
|
|
76
|
+
|
|
69
77
|
const pillId = `lp-segmented-${useId()}`
|
|
70
78
|
const pillTransition = usePillTransition()
|
|
71
79
|
</script>
|
|
@@ -74,7 +82,9 @@ const pillTransition = usePillTransition()
|
|
|
74
82
|
<ToggleGroupRoot
|
|
75
83
|
:model-value="modelValue"
|
|
76
84
|
type="single"
|
|
77
|
-
:disabled="disabled"
|
|
85
|
+
:disabled="field.disabled"
|
|
86
|
+
:aria-labelledby="field.labelId"
|
|
87
|
+
:aria-describedby="field.describedBy"
|
|
78
88
|
:rovingFocus="true"
|
|
79
89
|
:class="rootClass"
|
|
80
90
|
v-bind="rest"
|
|
@@ -84,7 +94,7 @@ const pillTransition = usePillTransition()
|
|
|
84
94
|
v-for="opt in options"
|
|
85
95
|
:key="opt.value"
|
|
86
96
|
:value="opt.value"
|
|
87
|
-
:aria-label="opt.label ? undefined : opt.value"
|
|
97
|
+
:aria-label="opt.ariaLabel ?? (opt.label ? undefined : opt.value)"
|
|
88
98
|
class="relative inline-flex h-full items-center justify-center gap-1.5 rounded-md px-3 font-medium outline-none transition-colors duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] focus-visible:ring-2 focus-visible:ring-ring data-[state=on]:text-ink data-[state=off]:text-muted data-[state=off]:hover:text-ink disabled:cursor-not-allowed disabled:opacity-55"
|
|
89
99
|
:class="block ? 'flex-1' : ''"
|
|
90
100
|
>
|
|
Binary file
|
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
activeShortcuts,
|
|
19
19
|
appleModifierLabel,
|
|
20
20
|
mergeShortcutGroups,
|
|
21
|
+
useAppleKeyboard,
|
|
21
22
|
type ShortcutGroup,
|
|
22
23
|
} from "../composables/useHotkeys"
|
|
24
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
23
25
|
|
|
24
26
|
const props = withDefaults(
|
|
25
27
|
defineProps<{
|
|
@@ -32,9 +34,12 @@ const props = withDefaults(
|
|
|
32
34
|
/** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
|
|
33
35
|
glass?: boolean
|
|
34
36
|
}>(),
|
|
35
|
-
{ open: false, groups: () => [], live: false, title:
|
|
37
|
+
{ open: false, groups: () => [], live: false, title: undefined, glass: undefined },
|
|
36
38
|
)
|
|
37
39
|
|
|
40
|
+
const text = useLabels("shortcuts", { title: "Keyboard shortcuts" })
|
|
41
|
+
const apple = useAppleKeyboard()
|
|
42
|
+
|
|
38
43
|
/*
|
|
39
44
|
* One key per "+", except a "+" that is the key itself: "Ctrl++" is Ctrl and
|
|
40
45
|
* plus, not Ctrl and two blanks.
|
|
@@ -53,7 +58,7 @@ const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
|
53
58
|
:glass="props.glass"
|
|
54
59
|
:open="props.open"
|
|
55
60
|
size="lg"
|
|
56
|
-
:title="props.title"
|
|
61
|
+
:title="props.title ?? text.title"
|
|
57
62
|
:description="props.description"
|
|
58
63
|
@update:open="(value: boolean) => emit('update:open', value)"
|
|
59
64
|
>
|
|
@@ -65,7 +70,7 @@ const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
|
65
70
|
<dt>{{ entry.label }}</dt>
|
|
66
71
|
<dd>
|
|
67
72
|
<kbd v-for="(key, index) in keysOf(entry.combo)" :key="`${key}-${index}`">
|
|
68
|
-
{{ appleModifierLabel(key.trim()) }}
|
|
73
|
+
{{ appleModifierLabel(key.trim(), apple) }}
|
|
69
74
|
</kbd>
|
|
70
75
|
</dd>
|
|
71
76
|
</div>
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* the static <nav> it always has — existing call sites are unaffected.
|
|
15
15
|
*/
|
|
16
16
|
import { computed, ref, useId } from "vue"
|
|
17
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
17
18
|
import LpDrawer from "./LpDrawer.vue"
|
|
18
19
|
import LpInput from "./LpInput.vue"
|
|
19
20
|
import LpSidebarNav from "./LpSidebarNav.vue"
|
|
@@ -45,8 +46,10 @@ const props = withDefaults(
|
|
|
45
46
|
* query is active — a hidden match is the same as no match.
|
|
46
47
|
*/
|
|
47
48
|
searchable?: boolean
|
|
48
|
-
/** Placeholder for that field. */
|
|
49
|
+
/** Placeholder for that field (defaults to the `search` label). */
|
|
49
50
|
searchPlaceholder?: string
|
|
51
|
+
/** Text overrides: `nav` names the landmark, `search` the filter field. */
|
|
52
|
+
labels?: Partial<{ nav: string; search: string }>
|
|
50
53
|
/** The query, when the app wants to own it (v-model:search). */
|
|
51
54
|
search?: string
|
|
52
55
|
/** Show a skeleton placeholder instead of the items. */
|
|
@@ -113,7 +116,6 @@ const props = withDefaults(
|
|
|
113
116
|
autoCollapseAfter?: number
|
|
114
117
|
}>(),
|
|
115
118
|
{
|
|
116
|
-
searchPlaceholder: "Search",
|
|
117
119
|
skeletonRows: 6,
|
|
118
120
|
skeletonHeader: true,
|
|
119
121
|
responsive: false,
|
|
@@ -132,6 +134,9 @@ const emit = defineEmits<{
|
|
|
132
134
|
(e: "update:search", value: string): void
|
|
133
135
|
}>()
|
|
134
136
|
|
|
137
|
+
const text = useLabels("sidebar", { nav: "Sidebar", search: "Search" }, () => props.labels)
|
|
138
|
+
const placeholder = computed(() => props.searchPlaceholder ?? text.value.search)
|
|
139
|
+
|
|
135
140
|
// Normalise both inputs to a single section list.
|
|
136
141
|
const allGroups = computed<SidebarSection[]>(() =>
|
|
137
142
|
props.sections ?? (props.items ? [{ items: props.items }] : []),
|
|
@@ -271,7 +276,7 @@ defineOptions({ inheritAttrs: false })
|
|
|
271
276
|
class="h-full flex-col gap-1 p-3"
|
|
272
277
|
:class="[railClass, chromeClass, width ? '' : 'w-60']"
|
|
273
278
|
:style="width ? { width } : undefined"
|
|
274
|
-
aria-label="
|
|
279
|
+
:aria-label="text.nav"
|
|
275
280
|
v-bind="$attrs"
|
|
276
281
|
>
|
|
277
282
|
<!-- Rail: edge-to-edge (negative margins cancel the p-3) and h-16 to match
|
|
@@ -298,8 +303,8 @@ defineOptions({ inheritAttrs: false })
|
|
|
298
303
|
size="sm"
|
|
299
304
|
icon="lucide:search"
|
|
300
305
|
class="mb-2 shrink-0"
|
|
301
|
-
:placeholder="
|
|
302
|
-
:aria-label="
|
|
306
|
+
:placeholder="placeholder"
|
|
307
|
+
:aria-label="placeholder"
|
|
303
308
|
/>
|
|
304
309
|
<LpSidebarNav
|
|
305
310
|
:groups="groups"
|
|
@@ -339,7 +344,7 @@ defineOptions({ inheritAttrs: false })
|
|
|
339
344
|
:edge-breakpoint="mobileBreakpoint"
|
|
340
345
|
@update:open="(v) => emit('update:open', v)"
|
|
341
346
|
>
|
|
342
|
-
<
|
|
347
|
+
<nav class="flex h-full flex-col gap-1" :aria-label="text.nav">
|
|
343
348
|
<div
|
|
344
349
|
v-if="$slots.header"
|
|
345
350
|
class="shrink-0"
|
|
@@ -357,8 +362,8 @@ defineOptions({ inheritAttrs: false })
|
|
|
357
362
|
size="sm"
|
|
358
363
|
icon="lucide:search"
|
|
359
364
|
class="mb-2 shrink-0"
|
|
360
|
-
:placeholder="
|
|
361
|
-
:aria-label="
|
|
365
|
+
:placeholder="placeholder"
|
|
366
|
+
:aria-label="placeholder"
|
|
362
367
|
/>
|
|
363
368
|
<LpSidebarNav
|
|
364
369
|
:groups="groups"
|
|
@@ -383,6 +388,6 @@ defineOptions({ inheritAttrs: false })
|
|
|
383
388
|
<div v-if="$slots.footer" class="mt-2 shrink-0 border-t border-line pt-3">
|
|
384
389
|
<slot name="footer" />
|
|
385
390
|
</div>
|
|
386
|
-
</
|
|
391
|
+
</nav>
|
|
387
392
|
</LpDrawer>
|
|
388
393
|
</template>
|
|
@@ -21,6 +21,8 @@ import LpContextMenu from "./LpContextMenu.vue"
|
|
|
21
21
|
import LpIcon from "./LpIcon.vue"
|
|
22
22
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
23
23
|
import type { SidebarItem, SidebarSection } from "./sidebar"
|
|
24
|
+
import { resolveLink, type ResolvedLink } from "./link"
|
|
25
|
+
import { useLinkComponent } from "../composables/useLpConfig"
|
|
24
26
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
25
27
|
|
|
26
28
|
const props = withDefaults(
|
|
@@ -75,6 +77,18 @@ function isOpen(section: SidebarSection): boolean {
|
|
|
75
77
|
|
|
76
78
|
const pillTransition = usePillTransition()
|
|
77
79
|
|
|
80
|
+
const linkComponent = useLinkComponent()
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* A disabled item is always a disabled button: an anchor cannot be disabled,
|
|
84
|
+
* and the old enabled-button fallback for a disabled link stayed focusable.
|
|
85
|
+
*/
|
|
86
|
+
function rowOf(item: SidebarItem): ResolvedLink {
|
|
87
|
+
const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href })
|
|
88
|
+
if (link) return link
|
|
89
|
+
return { is: "button", props: { type: "button", disabled: item.disabled || undefined } }
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
// The nav fills the remaining height with a fixed gap between groups; a short sidebar needs both under the consumer's control.
|
|
79
93
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
80
94
|
"flex min-h-0 flex-1 flex-col gap-4",
|
|
@@ -118,7 +132,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
118
132
|
because a thing that reacts to a click must look like one. -->
|
|
119
133
|
<CollapsibleTrigger
|
|
120
134
|
v-if="section.title && section.collapsible"
|
|
121
|
-
class="group/section mt-2 flex cursor-pointer items-center gap-2 rounded-control px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:bg-
|
|
135
|
+
class="group/section mt-2 flex cursor-pointer items-center gap-2 rounded-control px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:bg-ink/[0.04] hover:text-ink focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
122
136
|
>
|
|
123
137
|
<LpIcon
|
|
124
138
|
v-if="section.icon"
|
|
@@ -165,10 +179,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
165
179
|
:items="item.menu ?? []"
|
|
166
180
|
>
|
|
167
181
|
<component
|
|
168
|
-
:is="item.
|
|
169
|
-
|
|
170
|
-
:type="item.href ? undefined : 'button'"
|
|
171
|
-
:disabled="item.href ? undefined : item.disabled"
|
|
182
|
+
:is="rowOf(item).is"
|
|
183
|
+
v-bind="rowOf(item).props"
|
|
172
184
|
:aria-current="itemActive(item) ? 'page' : undefined"
|
|
173
185
|
:aria-disabled="item.disabled || undefined"
|
|
174
186
|
class="group/item relative flex items-center gap-3 rounded-control px-3 py-2 text-sm font-medium outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
@@ -88,7 +88,10 @@ const isBlock = computed(() => !slots.default)
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
@media (prefers-reduced-motion: reduce) {
|
|
91
|
-
|
|
91
|
+
/* Repeats the descendant form: it outranks a bare class, and grouped items
|
|
92
|
+
kept pulsing under reduced motion. */
|
|
93
|
+
.lp-skeleton-item,
|
|
94
|
+
.lp-skeleton .lp-skeleton-item {
|
|
92
95
|
animation: none;
|
|
93
96
|
opacity: 0.75;
|
|
94
97
|
}
|
|
@@ -9,6 +9,7 @@ export default { inheritAttrs: false }
|
|
|
9
9
|
// Single-value slider (reka Slider). reka models value as an array; we expose
|
|
10
10
|
// a plain number for the common single-thumb case. Themed via tokens.
|
|
11
11
|
import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from "reka-ui"
|
|
12
|
+
import { useFieldControl } from "../composables/useField"
|
|
12
13
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
13
14
|
|
|
14
15
|
const props = withDefaults(
|
|
@@ -38,6 +39,8 @@ function onCommit(v: number[] | undefined) {
|
|
|
38
39
|
if (v && v.length) emit("change", v[0])
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
const field = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }))
|
|
43
|
+
|
|
41
44
|
// A slider is full-width by default; putting one in a toolbar means constraining it, and a fixed h-5 has to yield to a denser row.
|
|
42
45
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
43
46
|
"relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55",
|
|
@@ -50,7 +53,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
50
53
|
:min="min"
|
|
51
54
|
:max="max"
|
|
52
55
|
:step="step"
|
|
53
|
-
:disabled="disabled"
|
|
56
|
+
:disabled="field.disabled"
|
|
54
57
|
:class="rootClass"
|
|
55
58
|
v-bind="rest"
|
|
56
59
|
@update:model-value="onUpdate"
|
|
@@ -60,7 +63,10 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
60
63
|
<SliderRange class="absolute h-full rounded-pill bg-brand" />
|
|
61
64
|
</SliderTrack>
|
|
62
65
|
<SliderThumb
|
|
66
|
+
:id="field.id"
|
|
63
67
|
:aria-label="ariaLabel"
|
|
68
|
+
:aria-labelledby="ariaLabel ? undefined : field.labelId"
|
|
69
|
+
:aria-describedby="field.describedBy"
|
|
64
70
|
class="block size-4 rounded-pill border-2 border-brand bg-surface shadow outline-none transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:scale-110 active:scale-95 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hover:scale-100 motion-reduce:active:scale-100"
|
|
65
71
|
/>
|
|
66
72
|
</SliderRoot>
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { computed } from "vue"
|
|
18
18
|
import { useReveal } from "../composables/useReveal"
|
|
19
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
19
20
|
import { areaPath, extent, linePath, normalise, type Box } from "./chart"
|
|
20
21
|
|
|
21
22
|
const props = withDefaults(
|
|
@@ -56,6 +57,7 @@ const props = withDefaults(
|
|
|
56
57
|
* watched being drawn — not a figure mostly spent easing out.
|
|
57
58
|
*/
|
|
58
59
|
duration?: number
|
|
60
|
+
labels?: Partial<Record<"empty" | "up" | "down" | "flat" | "latest", string>>
|
|
59
61
|
}>(),
|
|
60
62
|
{ width: 84, height: 24, tone: "brand", smooth: 0.9, animate: true, duration: 900 },
|
|
61
63
|
)
|
|
@@ -131,14 +133,20 @@ const lastPoint = computed(() => {
|
|
|
131
133
|
const hasData = computed(() => series.value[0].values.some((v) => v != null))
|
|
132
134
|
|
|
133
135
|
/* A bare <svg> is invisible to a screen reader; say the shape out loud. */
|
|
136
|
+
const text = useLabels(
|
|
137
|
+
"sparkline",
|
|
138
|
+
{ empty: "No data", up: "trending up", down: "trending down", flat: "flat", latest: "latest" },
|
|
139
|
+
() => props.labels,
|
|
140
|
+
)
|
|
141
|
+
|
|
134
142
|
const label = computed(() => {
|
|
135
143
|
if (props.ariaLabel) return props.ariaLabel
|
|
136
144
|
const values = series.value[0].values.filter((v): v is number => v != null)
|
|
137
|
-
if (!values.length) return
|
|
145
|
+
if (!values.length) return text.value.empty
|
|
138
146
|
const first = values[0]
|
|
139
147
|
const last = values[values.length - 1]
|
|
140
|
-
const dir = last > first ?
|
|
141
|
-
return `${dir}, latest ${last}`
|
|
148
|
+
const dir = last > first ? text.value.up : last < first ? text.value.down : text.value.flat
|
|
149
|
+
return `${dir}, ${text.value.latest} ${last}`
|
|
142
150
|
})
|
|
143
151
|
|
|
144
152
|
/*
|
|
@@ -6,8 +6,9 @@ export default { inheritAttrs: false }
|
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
|
-
import { computed, useSlots } from "vue"
|
|
9
|
+
import { computed, useId, useSlots } from "vue"
|
|
10
10
|
import { SwitchRoot, SwitchThumb } from "reka-ui"
|
|
11
|
+
import { useFieldControl } from "../composables/useField"
|
|
11
12
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
12
13
|
|
|
13
14
|
const props = withDefaults(
|
|
@@ -46,31 +47,53 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() => {
|
|
|
46
47
|
: "inline-flex cursor-pointer items-center gap-2 text-sm text-ink"
|
|
47
48
|
})
|
|
48
49
|
|
|
50
|
+
// Whatever is not class (name, aria-*, @focus…) belongs to the switch itself,
|
|
51
|
+
// labelled or not: on the label row it would land on the <label>.
|
|
52
|
+
const field = useFieldControl(() => ({
|
|
53
|
+
wrapped: false,
|
|
54
|
+
id: typeof rest.value.id === "string" ? rest.value.id : undefined,
|
|
55
|
+
disabled: props.disabled,
|
|
56
|
+
}))
|
|
57
|
+
const descriptionId = `${useId()}-description`
|
|
58
|
+
const describedBy = computed(
|
|
59
|
+
() => [props.description && labelled.value ? descriptionId : "", field.value.describedBy ?? ""].filter(Boolean).join(" ") || undefined,
|
|
60
|
+
)
|
|
61
|
+
|
|
49
62
|
function onUpdate(value: boolean) {
|
|
50
63
|
emit("update:modelValue", value)
|
|
51
64
|
}
|
|
52
65
|
</script>
|
|
53
66
|
|
|
54
67
|
<template>
|
|
55
|
-
<label v-if="labelled" :class="[rootClass, disabled && 'cursor-not-allowed']"
|
|
68
|
+
<label v-if="labelled" :class="[rootClass, field.disabled && 'cursor-not-allowed']">
|
|
56
69
|
<span v-if="labelPosition === 'start'" class="flex min-w-0 flex-col">
|
|
57
70
|
<slot>{{ label }}</slot>
|
|
58
|
-
<span v-if="description" class="text-xs text-muted">{{ description }}</span>
|
|
71
|
+
<span v-if="description" :id="descriptionId" class="text-xs text-muted">{{ description }}</span>
|
|
59
72
|
</span>
|
|
60
|
-
<SwitchRoot
|
|
73
|
+
<SwitchRoot
|
|
74
|
+
v-bind="rest"
|
|
75
|
+
:id="field.id"
|
|
76
|
+
:model-value="modelValue"
|
|
77
|
+
:disabled="field.disabled"
|
|
78
|
+
:aria-describedby="describedBy"
|
|
79
|
+
:class="TRACK"
|
|
80
|
+
@update:model-value="onUpdate"
|
|
81
|
+
>
|
|
61
82
|
<SwitchThumb :class="THUMB" />
|
|
62
83
|
</SwitchRoot>
|
|
63
84
|
<span v-if="labelPosition === 'end'" class="flex min-w-0 flex-col">
|
|
64
85
|
<slot>{{ label }}</slot>
|
|
65
|
-
<span v-if="description" class="text-xs text-muted">{{ description }}</span>
|
|
86
|
+
<span v-if="description" :id="descriptionId" class="text-xs text-muted">{{ description }}</span>
|
|
66
87
|
</span>
|
|
67
88
|
</label>
|
|
68
89
|
<SwitchRoot
|
|
69
90
|
v-else
|
|
91
|
+
v-bind="rest"
|
|
92
|
+
:id="field.id"
|
|
70
93
|
:model-value="modelValue"
|
|
71
|
-
:disabled="disabled"
|
|
94
|
+
:disabled="field.disabled"
|
|
95
|
+
:aria-describedby="describedBy"
|
|
72
96
|
:class="rootClass"
|
|
73
|
-
v-bind="rest"
|
|
74
97
|
@update:model-value="onUpdate"
|
|
75
98
|
>
|
|
76
99
|
<SwitchThumb :class="THUMB" />
|
|
@@ -13,6 +13,8 @@ import LpPagination from "./LpPagination.vue"
|
|
|
13
13
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
14
14
|
import { useAsyncTask } from "../composables/useAsyncTask"
|
|
15
15
|
import { useRovingFocus } from "../composables/useRovingFocus"
|
|
16
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
17
|
+
import { compareValues } from "./tableSort"
|
|
16
18
|
|
|
17
19
|
export interface TableColumn<Row> {
|
|
18
20
|
key: string
|
|
@@ -21,6 +23,10 @@ export interface TableColumn<Row> {
|
|
|
21
23
|
width?: string
|
|
22
24
|
/** Allow clicking the header to sort by this column. */
|
|
23
25
|
sortable?: boolean
|
|
26
|
+
/** The value this column sorts by, when the cell holds something else. */
|
|
27
|
+
sortValue?: (row: Row) => unknown
|
|
28
|
+
/** Full control over the order; wins over `sortValue`. Ascending order. */
|
|
29
|
+
compare?: (a: Row, b: Row) => number
|
|
24
30
|
/**
|
|
25
31
|
* Offer "show only these values" in the header's menu, built from the values
|
|
26
32
|
* actually present in the rows. For a column that holds a small vocabulary —
|
|
@@ -38,6 +44,19 @@ export interface TableColumn<Row> {
|
|
|
38
44
|
alwaysVisible?: boolean
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
export interface TableLabels {
|
|
48
|
+
[key: string]: string
|
|
49
|
+
loading: string
|
|
50
|
+
error: string
|
|
51
|
+
empty: string
|
|
52
|
+
sortAsc: string
|
|
53
|
+
sortDesc: string
|
|
54
|
+
sortClear: string
|
|
55
|
+
filter: string
|
|
56
|
+
showAll: string
|
|
57
|
+
columns: string
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
export interface SortState {
|
|
42
61
|
key: string
|
|
43
62
|
dir: "asc" | "desc"
|
|
@@ -68,6 +87,7 @@ export interface TablePage<Row> {
|
|
|
68
87
|
|
|
69
88
|
type RowKey = string | number
|
|
70
89
|
|
|
90
|
+
|
|
71
91
|
const props = withDefaults(
|
|
72
92
|
defineProps<{
|
|
73
93
|
columns: TableColumn<T>[]
|
|
@@ -86,7 +106,11 @@ const props = withDefaults(
|
|
|
86
106
|
debounce?: number
|
|
87
107
|
loadingLabel?: string
|
|
88
108
|
errorLabel?: string
|
|
89
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Stable id per row. Without it a row is keyed by its position in `rows`,
|
|
111
|
+
* which survives sorting and filtering but not a new array; set it whenever
|
|
112
|
+
* rows are replaced (`load`, polling) and selection must carry over.
|
|
113
|
+
*/
|
|
90
114
|
rowKey?: keyof T | ((row: T) => RowKey)
|
|
91
115
|
emptyLabel?: string
|
|
92
116
|
emptyIcon?: string
|
|
@@ -111,27 +135,50 @@ const props = withDefaults(
|
|
|
111
135
|
filterLabel?: string
|
|
112
136
|
showAllLabel?: string
|
|
113
137
|
columnsLabel?: string
|
|
138
|
+
/** All of the wording above in one object, and the config-level scope "table". */
|
|
139
|
+
labels?: Partial<TableLabels>
|
|
114
140
|
}>(),
|
|
115
141
|
{
|
|
116
142
|
rows: () => [],
|
|
117
143
|
pageSize: 25,
|
|
118
144
|
debounce: 150,
|
|
119
|
-
loadingLabel: "Loading…",
|
|
120
|
-
errorLabel: "Could not load",
|
|
121
|
-
emptyLabel: "Nothing here yet",
|
|
122
145
|
emptyIcon: "lucide:inbox",
|
|
123
146
|
selectable: false,
|
|
124
147
|
selected: () => [],
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
sortClearLabel: "Clear sorting",
|
|
129
|
-
filterLabel: "Show only",
|
|
130
|
-
showAllLabel: "Show all",
|
|
131
|
-
columnsLabel: "Columns",
|
|
148
|
+
// undefined, not null: null is a parent saying "unsorted", undefined is no
|
|
149
|
+
// parent at all, and only the latter lets the table keep its own sort.
|
|
150
|
+
sort: undefined,
|
|
132
151
|
},
|
|
133
152
|
)
|
|
134
153
|
|
|
154
|
+
const labelSet = useLabels<TableLabels>(
|
|
155
|
+
"table",
|
|
156
|
+
{
|
|
157
|
+
loading: "Loading…",
|
|
158
|
+
error: "Could not load",
|
|
159
|
+
empty: "Nothing here yet",
|
|
160
|
+
sortAsc: "Sort ascending",
|
|
161
|
+
sortDesc: "Sort descending",
|
|
162
|
+
sortClear: "Clear sorting",
|
|
163
|
+
filter: "Show only",
|
|
164
|
+
showAll: "Show all",
|
|
165
|
+
columns: "Columns",
|
|
166
|
+
},
|
|
167
|
+
() => props.labels,
|
|
168
|
+
)
|
|
169
|
+
const text = computed<TableLabels>(() => ({
|
|
170
|
+
...labelSet.value,
|
|
171
|
+
loading: props.loadingLabel ?? labelSet.value.loading,
|
|
172
|
+
error: props.errorLabel ?? labelSet.value.error,
|
|
173
|
+
empty: props.emptyLabel ?? labelSet.value.empty,
|
|
174
|
+
sortAsc: props.sortAscLabel ?? labelSet.value.sortAsc,
|
|
175
|
+
sortDesc: props.sortDescLabel ?? labelSet.value.sortDesc,
|
|
176
|
+
sortClear: props.sortClearLabel ?? labelSet.value.sortClear,
|
|
177
|
+
filter: props.filterLabel ?? labelSet.value.filter,
|
|
178
|
+
showAll: props.showAllLabel ?? labelSet.value.showAll,
|
|
179
|
+
columns: props.columnsLabel ?? labelSet.value.columns,
|
|
180
|
+
}))
|
|
181
|
+
|
|
135
182
|
/*
|
|
136
183
|
* Layout classes belong on the root, not on the scroll area. Since the pager
|
|
137
184
|
* wrapper became the root, a caller's `min-h-0 flex-1` landed on the inner
|
|
@@ -156,7 +203,9 @@ const emit = defineEmits<{
|
|
|
156
203
|
function keyFor(row: T, index: number): RowKey {
|
|
157
204
|
if (typeof props.rowKey === "function") return props.rowKey(row)
|
|
158
205
|
if (props.rowKey) return row[props.rowKey] as RowKey
|
|
159
|
-
|
|
206
|
+
// Position in the source, not in the sorted view: a sort must not move a
|
|
207
|
+
// tick from one row to another.
|
|
208
|
+
return sourceIndex.value.get(row) ?? index
|
|
160
209
|
}
|
|
161
210
|
|
|
162
211
|
function alignClass(align?: "left" | "right" | "center"): string {
|
|
@@ -207,7 +256,7 @@ const contextMenu = computed<ContextMenuItemDef[]>(() => {
|
|
|
207
256
|
// emitted an event nobody was listening for, and left the rows exactly as they
|
|
208
257
|
// were: a control that answers a click by doing nothing.
|
|
209
258
|
const ownSort = ref<SortState | null>(null)
|
|
210
|
-
const activeSort = computed(() => props.sort
|
|
259
|
+
const activeSort = computed(() => (props.sort !== undefined ? props.sort : ownSort.value))
|
|
211
260
|
|
|
212
261
|
// ── server-backed rows ───────────────────────────────────────
|
|
213
262
|
const loaded = shallowRef<T[]>([])
|
|
@@ -219,6 +268,7 @@ const task = useAsyncTask<TablePage<T>>()
|
|
|
219
268
|
const loading = task.pending
|
|
220
269
|
|
|
221
270
|
const sourceRows = computed<T[]>(() => (props.load ? loaded.value : props.rows))
|
|
271
|
+
const sourceIndex = computed(() => new Map(sourceRows.value.map((row, i) => [row, i])))
|
|
222
272
|
|
|
223
273
|
// The tallest page shown so far, held as a floor: the last page is usually
|
|
224
274
|
// short, and without it the pager below jumps up the moment you reach it.
|
|
@@ -321,17 +371,13 @@ const displayRows = computed<T[]>(() => {
|
|
|
321
371
|
const col = props.columns.find((c) => c.key === s.key)
|
|
322
372
|
if (!col?.sortable) return filteredRows.value
|
|
323
373
|
const factor = s.dir === "asc" ? 1 : -1
|
|
324
|
-
|
|
374
|
+
const own = col.compare
|
|
375
|
+
const value = col.sortValue ?? ((row: T) => row[s.key])
|
|
376
|
+
return [...filteredRows.value].sort(
|
|
377
|
+
(a, b) => (own ? own(a, b) : compareValues(value(a), value(b))) * factor,
|
|
378
|
+
)
|
|
325
379
|
})
|
|
326
380
|
|
|
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 })
|
|
333
|
-
}
|
|
334
|
-
|
|
335
381
|
function toggleSort(col: TableColumn<T>) {
|
|
336
382
|
if (!col.sortable) return
|
|
337
383
|
const s = activeSort.value
|
|
@@ -377,19 +423,19 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
|
377
423
|
if (col.sortable) {
|
|
378
424
|
items.push(
|
|
379
425
|
{
|
|
380
|
-
label:
|
|
426
|
+
label: text.value.sortAsc,
|
|
381
427
|
icon: "lucide:arrow-up",
|
|
382
428
|
disabled: s?.key === col.key && s.dir === "asc",
|
|
383
429
|
onSelect: set({ key: col.key, dir: "asc" }),
|
|
384
430
|
},
|
|
385
431
|
{
|
|
386
|
-
label:
|
|
432
|
+
label: text.value.sortDesc,
|
|
387
433
|
icon: "lucide:arrow-down",
|
|
388
434
|
disabled: s?.key === col.key && s.dir === "desc",
|
|
389
435
|
onSelect: set({ key: col.key, dir: "desc" }),
|
|
390
436
|
},
|
|
391
437
|
{
|
|
392
|
-
label:
|
|
438
|
+
label: text.value.sortClear,
|
|
393
439
|
icon: "lucide:x",
|
|
394
440
|
disabled: !s,
|
|
395
441
|
onSelect: set(null),
|
|
@@ -402,7 +448,7 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
|
402
448
|
if (col.filterable) {
|
|
403
449
|
const hidden = hiddenValues.value[col.key]
|
|
404
450
|
items.push({
|
|
405
|
-
label:
|
|
451
|
+
label: text.value.filter,
|
|
406
452
|
icon: "lucide:filter",
|
|
407
453
|
separatorBefore: items.length > 0,
|
|
408
454
|
children: [
|
|
@@ -412,7 +458,7 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
|
|
|
412
458
|
onSelect: () => toggleValue(col.key, value),
|
|
413
459
|
})),
|
|
414
460
|
{
|
|
415
|
-
label:
|
|
461
|
+
label: text.value.showAll,
|
|
416
462
|
icon: "lucide:list",
|
|
417
463
|
separatorBefore: true,
|
|
418
464
|
disabled: !hidden?.size,
|
|
@@ -435,7 +481,7 @@ function tableMenu(): ContextMenuItemDef[] {
|
|
|
435
481
|
if (!offered.length) return []
|
|
436
482
|
return [
|
|
437
483
|
{
|
|
438
|
-
label:
|
|
484
|
+
label: text.value.columns,
|
|
439
485
|
icon: "lucide:columns-3",
|
|
440
486
|
children: offered.map((col) => ({
|
|
441
487
|
label: col.label,
|
|
@@ -462,6 +508,13 @@ function fetchPage(delay = 0) {
|
|
|
462
508
|
task.run((signal) => load(query, signal), {
|
|
463
509
|
delay,
|
|
464
510
|
apply: (result) => {
|
|
511
|
+
// Rows deleted from the last page leave it past the end; step back to
|
|
512
|
+
// the page that now exists rather than strand the user on an empty one.
|
|
513
|
+
const last = Math.max(1, Math.ceil(result.total / props.pageSize))
|
|
514
|
+
if (page.value > last) {
|
|
515
|
+
page.value = last
|
|
516
|
+
return
|
|
517
|
+
}
|
|
465
518
|
loaded.value = result.rows
|
|
466
519
|
total.value = result.total
|
|
467
520
|
facets.value = result.facets ?? {}
|
|
@@ -488,7 +541,8 @@ defineExpose({ reload: () => fetchPage() })
|
|
|
488
541
|
|
|
489
542
|
// ── selection ────────────────────────────────────────────────
|
|
490
543
|
const selectedSet = computed(() => new Set(props.selected))
|
|
491
|
-
|
|
544
|
+
// "All" is what the user can see: rows a header filter hides are left alone.
|
|
545
|
+
const allKeys = computed(() => filteredRows.value.map((r, i) => keyFor(r, i)))
|
|
492
546
|
const allChecked = computed(
|
|
493
547
|
() => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k)),
|
|
494
548
|
)
|
|
@@ -497,7 +551,12 @@ const someChecked = computed(
|
|
|
497
551
|
)
|
|
498
552
|
|
|
499
553
|
function toggleAll(checked: boolean) {
|
|
500
|
-
|
|
554
|
+
const next = new Set(selectedSet.value)
|
|
555
|
+
for (const key of allKeys.value) {
|
|
556
|
+
if (checked) next.add(key)
|
|
557
|
+
else next.delete(key)
|
|
558
|
+
}
|
|
559
|
+
emit("update:selected", [...next])
|
|
501
560
|
}
|
|
502
561
|
|
|
503
562
|
function toggleRow(key: RowKey, checked: boolean) {
|
|
@@ -507,7 +566,7 @@ function toggleRow(key: RowKey, checked: boolean) {
|
|
|
507
566
|
emit("update:selected", [...next])
|
|
508
567
|
}
|
|
509
568
|
|
|
510
|
-
const colSpan = computed(() =>
|
|
569
|
+
const colSpan = computed(() => visibleColumns.value.length + (props.selectable ? 1 : 0))
|
|
511
570
|
|
|
512
571
|
/*
|
|
513
572
|
* Keyboard navigation over the rows. A table is one Tab stop, not one per row:
|
|
@@ -703,7 +762,7 @@ const barInsetTop = computed(() =>
|
|
|
703
762
|
class="mx-auto mb-2 opacity-60"
|
|
704
763
|
:class="loading ? 'animate-spin' : ''"
|
|
705
764
|
/>
|
|
706
|
-
<div>{{ loading ?
|
|
765
|
+
<div>{{ loading ? text.loading : failed ? text.error : text.empty }}</div>
|
|
707
766
|
</td>
|
|
708
767
|
</tr>
|
|
709
768
|
<!-- A plain <tr>, because TransitionGroup animates element children and
|