@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
|
@@ -25,6 +25,7 @@ import { prefersReducedMotion } from "../composables/easing"
|
|
|
25
25
|
import { type LatLon, OSM_TILES, type TileSource, project, tileUrl, unproject } from "./address"
|
|
26
26
|
import LpIcon from "./LpIcon.vue"
|
|
27
27
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
28
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
28
29
|
|
|
29
30
|
const props = withDefaults(
|
|
30
31
|
defineProps<{
|
|
@@ -45,6 +46,8 @@ const props = withDefaults(
|
|
|
45
46
|
zoomOutLabel?: string
|
|
46
47
|
locateLabel?: string
|
|
47
48
|
recentreLabel?: string
|
|
49
|
+
/** Accessible name of the map itself. */
|
|
50
|
+
mapLabel?: string
|
|
48
51
|
}>(),
|
|
49
52
|
{
|
|
50
53
|
tiles: () => OSM_TILES,
|
|
@@ -52,15 +55,26 @@ const props = withDefaults(
|
|
|
52
55
|
center: () => ({ lat: 50.11, lon: 14.44 }),
|
|
53
56
|
height: "20rem",
|
|
54
57
|
locatable: true,
|
|
55
|
-
zoomInLabel: "Zoom in",
|
|
56
|
-
zoomOutLabel: "Zoom out",
|
|
57
|
-
locateLabel: "Use my location",
|
|
58
|
-
recentreLabel: "Back to the pin",
|
|
59
58
|
},
|
|
60
59
|
)
|
|
61
60
|
|
|
62
61
|
const emit = defineEmits<{ (e: "update:modelValue", value: LatLon): void }>()
|
|
63
62
|
|
|
63
|
+
const labelSet = useLabels("mapPicker", {
|
|
64
|
+
zoomIn: "Zoom in",
|
|
65
|
+
zoomOut: "Zoom out",
|
|
66
|
+
locate: "Use my location",
|
|
67
|
+
recentre: "Back to the pin",
|
|
68
|
+
map: "Map — arrows pan, +/− zoom, Enter drops the pin in the centre",
|
|
69
|
+
})
|
|
70
|
+
const text = computed(() => ({
|
|
71
|
+
zoomIn: props.zoomInLabel ?? labelSet.value.zoomIn,
|
|
72
|
+
zoomOut: props.zoomOutLabel ?? labelSet.value.zoomOut,
|
|
73
|
+
locate: props.locateLabel ?? labelSet.value.locate,
|
|
74
|
+
recentre: props.recentreLabel ?? labelSet.value.recentre,
|
|
75
|
+
map: props.mapLabel ?? labelSet.value.map,
|
|
76
|
+
}))
|
|
77
|
+
|
|
64
78
|
const PAN_STEP = 80
|
|
65
79
|
const ZOOM_MS = 220
|
|
66
80
|
|
|
@@ -452,6 +466,15 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
452
466
|
e.preventDefault()
|
|
453
467
|
recentre()
|
|
454
468
|
break
|
|
469
|
+
// Without this a keyboard could pan and zoom but never actually pick.
|
|
470
|
+
case "Enter":
|
|
471
|
+
case " ": {
|
|
472
|
+
const el = viewport.value
|
|
473
|
+
if (!el) return
|
|
474
|
+
e.preventDefault()
|
|
475
|
+
emit("update:modelValue", toLatLon({ x: el.clientWidth / 2, y: el.clientHeight / 2 }))
|
|
476
|
+
break
|
|
477
|
+
}
|
|
455
478
|
}
|
|
456
479
|
}
|
|
457
480
|
|
|
@@ -563,7 +586,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
563
586
|
:class="disabled ? 'cursor-not-allowed opacity-55' : panning ? 'cursor-grabbing' : 'cursor-grab'"
|
|
564
587
|
:tabindex="disabled ? -1 : 0"
|
|
565
588
|
role="application"
|
|
566
|
-
:aria-label="
|
|
589
|
+
:aria-label="text.map"
|
|
567
590
|
@pointerdown="onPointerDown"
|
|
568
591
|
@pointermove="onPointerMove"
|
|
569
592
|
@pointerup="onPointerUp"
|
|
@@ -616,8 +639,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
616
639
|
:class="CONTROL"
|
|
617
640
|
type="button"
|
|
618
641
|
:disabled="disabled || level >= maxZoom"
|
|
619
|
-
:title="
|
|
620
|
-
:aria-label="
|
|
642
|
+
:title="text.zoomIn"
|
|
643
|
+
:aria-label="text.zoomIn"
|
|
621
644
|
@click="zoomBy(1)"
|
|
622
645
|
>
|
|
623
646
|
<LpIcon name="lucide:plus" :size="16" />
|
|
@@ -626,8 +649,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
626
649
|
:class="CONTROL"
|
|
627
650
|
type="button"
|
|
628
651
|
:disabled="disabled || level <= minZoom"
|
|
629
|
-
:title="
|
|
630
|
-
:aria-label="
|
|
652
|
+
:title="text.zoomOut"
|
|
653
|
+
:aria-label="text.zoomOut"
|
|
631
654
|
@click="zoomBy(-1)"
|
|
632
655
|
>
|
|
633
656
|
<LpIcon name="lucide:minus" :size="16" />
|
|
@@ -636,8 +659,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
636
659
|
v-if="canRecentre"
|
|
637
660
|
:class="CONTROL"
|
|
638
661
|
type="button"
|
|
639
|
-
:title="
|
|
640
|
-
:aria-label="
|
|
662
|
+
:title="text.recentre"
|
|
663
|
+
:aria-label="text.recentre"
|
|
641
664
|
@click="recentre"
|
|
642
665
|
>
|
|
643
666
|
<LpIcon name="lucide:locate-fixed" :size="16" />
|
|
@@ -647,8 +670,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
647
670
|
:class="CONTROL"
|
|
648
671
|
type="button"
|
|
649
672
|
:disabled="disabled || locating"
|
|
650
|
-
:title="
|
|
651
|
-
:aria-label="
|
|
673
|
+
:title="text.locate"
|
|
674
|
+
:aria-label="text.locate"
|
|
652
675
|
@click="locate"
|
|
653
676
|
>
|
|
654
677
|
<LpIcon
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* cover is per image and the reveal is remembered per image, because a set is
|
|
25
25
|
* usually mixed — one marked screenshot among plain ones.
|
|
26
26
|
*/
|
|
27
|
-
import { computed, ref } from "vue"
|
|
27
|
+
import { computed, ref, watch } from "vue"
|
|
28
28
|
import type { LightboxItem } from "./lightbox"
|
|
29
29
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
30
30
|
import LpIcon from "./LpIcon.vue"
|
|
@@ -88,6 +88,12 @@ defineSlots<{
|
|
|
88
88
|
const open = ref(false)
|
|
89
89
|
const index = ref(0)
|
|
90
90
|
|
|
91
|
+
// Closing happens inside the lightbox (Escape, the scrim, the X), so the gallery
|
|
92
|
+
// reports both edges of v-model:open, not only the one it starts.
|
|
93
|
+
watch(open, (value, was) => {
|
|
94
|
+
if (!value && was) emit("update:open", false)
|
|
95
|
+
})
|
|
96
|
+
|
|
91
97
|
// `max` counts what is SHOWN, so the badge reads "+N more" rather than "N
|
|
92
98
|
// total". A set of 7 capped at 4 shows 4 cells, the last one badged +3.
|
|
93
99
|
const visible = computed(() =>
|
|
@@ -188,8 +194,8 @@ function activate(i: number) {
|
|
|
188
194
|
const at = openIndexOf(i)
|
|
189
195
|
if (at === -1) return
|
|
190
196
|
index.value = at
|
|
197
|
+
if (!open.value) emit("update:open", true)
|
|
191
198
|
open.value = true
|
|
192
|
-
emit("update:open", true)
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
/** The badge is about the FIRST hidden image, so that is where it opens. */
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "reka-ui"
|
|
11
11
|
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
|
|
12
12
|
import { useGlass } from "../composables/useGlass"
|
|
13
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
13
14
|
import { useShift } from "../composables/useShift"
|
|
14
15
|
import { useModalLayer } from "./modalDepth"
|
|
15
16
|
import { CLOSE_ICON } from "./dropdown"
|
|
@@ -33,7 +34,8 @@ import LpScrollArea from "./LpScrollArea.vue"
|
|
|
33
34
|
* same fix, as the lightbox's own rungs in tokens.css — only the depth here is
|
|
34
35
|
* not known until a dialog finds itself inside another.
|
|
35
36
|
*/
|
|
36
|
-
const
|
|
37
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
38
|
+
const { claim, release, layer } = useModalLayer({ element: () => panelRef.value?.parentElement })
|
|
37
39
|
|
|
38
40
|
const props = withDefaults(
|
|
39
41
|
defineProps<{
|
|
@@ -70,10 +72,20 @@ const props = withDefaults(
|
|
|
70
72
|
* `provideGlass`, and without one it is off.
|
|
71
73
|
*/
|
|
72
74
|
glass?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Accessible name for a dialog without a visible `title`. Rendered as a
|
|
77
|
+
* visually hidden title, so assistive tech still announces what opened.
|
|
78
|
+
*/
|
|
79
|
+
ariaLabel?: string
|
|
80
|
+
/** `alertdialog` for a prompt that interrupts the user (a confirmation). */
|
|
81
|
+
role?: "dialog" | "alertdialog"
|
|
82
|
+
labels?: Partial<{ close: string }>
|
|
73
83
|
}>(),
|
|
74
84
|
{ size: "md", autoFocus: "auto", glass: undefined },
|
|
75
85
|
)
|
|
76
86
|
|
|
87
|
+
const text = useLabels("modal", { close: "Close" }, () => props.labels)
|
|
88
|
+
|
|
77
89
|
const frosted = useGlass(() => props.glass)
|
|
78
90
|
|
|
79
91
|
defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
@@ -95,9 +107,10 @@ const onInteractOutside = (event: Event) => {
|
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
const describedByAttrs = computed(() =>
|
|
99
|
-
props.description ? {} : { "aria-describedby": undefined },
|
|
100
|
-
)
|
|
110
|
+
const describedByAttrs = computed(() => ({
|
|
111
|
+
...(props.description ? {} : { "aria-describedby": undefined }),
|
|
112
|
+
...(props.role ? { role: props.role } : {}),
|
|
113
|
+
}))
|
|
101
114
|
|
|
102
115
|
const widthClass = computed(() => {
|
|
103
116
|
if (props.width) return ""
|
|
@@ -152,15 +165,13 @@ const bodyPad = computed(() =>
|
|
|
152
165
|
* scroll area exactly as before. A panel whose size never moves is pinned once
|
|
153
166
|
* and never transitions.
|
|
154
167
|
*/
|
|
155
|
-
const panelRef = ref<HTMLElement | null>(null)
|
|
156
|
-
|
|
157
168
|
/**
|
|
158
169
|
* Controls that are worth landing on when the dialog opens, in order of
|
|
159
170
|
* preference. A field the user is going to type in wins over a button, since
|
|
160
171
|
* typing is the reason most dialogs exist.
|
|
161
172
|
*/
|
|
162
173
|
const AUTO_FOCUS_ORDER = [
|
|
163
|
-
"input:not([type=hidden]):not([disabled]):not([readonly])",
|
|
174
|
+
"input:not([type=hidden]):not([disabled]):not([readonly]):not([tabindex='-1'])",
|
|
164
175
|
"textarea:not([disabled]):not([readonly])",
|
|
165
176
|
"select:not([disabled])",
|
|
166
177
|
"[data-lp-autofocus]",
|
|
@@ -178,6 +189,18 @@ const AUTO_FOCUS_ORDER = [
|
|
|
178
189
|
*
|
|
179
190
|
* Both are fixed here: pick a meaningful target, and mark it so the ring shows.
|
|
180
191
|
*/
|
|
192
|
+
/**
|
|
193
|
+
* Whether a candidate can actually take focus: reka renders hidden form inputs
|
|
194
|
+
* for its switches and checkboxes, and a control hidden at this breakpoint
|
|
195
|
+
* (`lg:hidden`) is in the DOM but not on screen.
|
|
196
|
+
*/
|
|
197
|
+
function focusable(el: HTMLElement): boolean {
|
|
198
|
+
if (el.hasAttribute("disabled") || el.getAttribute("tabindex") === "-1") return false
|
|
199
|
+
if (el.closest("[aria-hidden='true'], [data-hidden], [inert]")) return false
|
|
200
|
+
if (typeof el.checkVisibility === "function") return el.checkVisibility({ visibilityProperty: true })
|
|
201
|
+
return el.getClientRects().length > 0
|
|
202
|
+
}
|
|
203
|
+
|
|
181
204
|
function onOpenAutoFocus(event: Event) {
|
|
182
205
|
if (props.autoFocus === "none") return
|
|
183
206
|
const panel = panelRef.value
|
|
@@ -186,26 +209,26 @@ function onOpenAutoFocus(event: Event) {
|
|
|
186
209
|
const selectors =
|
|
187
210
|
props.autoFocus === "auto" ? AUTO_FOCUS_ORDER : [props.autoFocus]
|
|
188
211
|
for (const selector of selectors) {
|
|
189
|
-
const target
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
for (const target of panel.querySelectorAll<HTMLElement>(selector)) {
|
|
213
|
+
if (!focusable(target)) continue
|
|
214
|
+
target.focus()
|
|
215
|
+
// Only once focus really landed: a prevented auto-focus that failed would
|
|
216
|
+
// leave the keyboard on the page behind the dialog.
|
|
217
|
+
if (document.activeElement !== target) continue
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
// `focus-visible` is the browser's own heuristic and cannot be forced, so
|
|
220
|
+
// the ring is driven off an attribute the kit owns. Skipped for a control
|
|
221
|
+
// whose own shell already draws a ring on focus-within.
|
|
222
|
+
if (!target.closest("[data-lp-ring-owner]")) {
|
|
223
|
+
target.setAttribute("data-lp-focus-ring", "")
|
|
224
|
+
target.addEventListener(
|
|
225
|
+
"blur",
|
|
226
|
+
() => target.removeAttribute("data-lp-focus-ring"),
|
|
227
|
+
{ once: true },
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
return
|
|
206
231
|
}
|
|
207
|
-
target.focus()
|
|
208
|
-
return
|
|
209
232
|
}
|
|
210
233
|
}
|
|
211
234
|
// The panel's height eases between content-derived sizes — see
|
|
@@ -305,6 +328,9 @@ watch(
|
|
|
305
328
|
:class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
|
|
306
329
|
:style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
|
|
307
330
|
>
|
|
331
|
+
<DialogTitle v-if="!(title || $slots.title) && ariaLabel" class="sr-only">
|
|
332
|
+
{{ ariaLabel }}
|
|
333
|
+
</DialogTitle>
|
|
308
334
|
<header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
|
|
309
335
|
<div class="flex flex-col gap-1">
|
|
310
336
|
<DialogTitle class="text-base font-semibold text-ink">
|
|
@@ -319,7 +345,7 @@ watch(
|
|
|
319
345
|
<DialogClose
|
|
320
346
|
data-lp-dialog-close
|
|
321
347
|
class="group flex shrink-0 items-center rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
322
|
-
aria-label="
|
|
348
|
+
:aria-label="text.close"
|
|
323
349
|
>
|
|
324
350
|
<LpIcon
|
|
325
351
|
name="lucide:x"
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* one the sections are simply unreachable on a phone.
|
|
15
15
|
*/
|
|
16
16
|
import { computed, ref } from "vue"
|
|
17
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
17
18
|
import LpButton from "./LpButton.vue"
|
|
18
19
|
import LpIcon from "./LpIcon.vue"
|
|
19
20
|
import LpModal from "./LpModal.vue"
|
|
@@ -62,7 +63,6 @@ const props = withDefaults(
|
|
|
62
63
|
size: "2xl",
|
|
63
64
|
navWidth: "13rem",
|
|
64
65
|
mobileBreakpoint: "lg",
|
|
65
|
-
navLabel: "Sections",
|
|
66
66
|
navSide: "start",
|
|
67
67
|
glass: undefined,
|
|
68
68
|
},
|
|
@@ -73,6 +73,9 @@ const emit = defineEmits<{
|
|
|
73
73
|
(e: "update:modelValue", value: string): void
|
|
74
74
|
}>()
|
|
75
75
|
|
|
76
|
+
const text = useLabels("modalNav", { nav: "Sections" })
|
|
77
|
+
const navName = computed(() => props.navLabel ?? text.value.nav)
|
|
78
|
+
|
|
76
79
|
const navOpen = ref(false)
|
|
77
80
|
|
|
78
81
|
const active = computed({
|
|
@@ -187,7 +190,7 @@ const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
|
|
|
187
190
|
variant="ghost"
|
|
188
191
|
size="sm"
|
|
189
192
|
square
|
|
190
|
-
:aria-label="
|
|
193
|
+
:aria-label="navName"
|
|
191
194
|
:class="triggerClass"
|
|
192
195
|
@click="navOpen = true"
|
|
193
196
|
>
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* `select`. Mirrors the kit convention (cf. LpSidebar): data in, events out,
|
|
9
9
|
* no data fetching inside.
|
|
10
10
|
*/
|
|
11
|
-
import { computed, getCurrentInstance } from "vue"
|
|
11
|
+
import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref } from "vue"
|
|
12
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
12
13
|
import LpBadge from "./LpBadge.vue"
|
|
13
14
|
import LpButton from "./LpButton.vue"
|
|
14
15
|
import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
|
|
@@ -56,6 +57,15 @@ export interface NotificationLabels {
|
|
|
56
57
|
dismiss?: string
|
|
57
58
|
empty?: string
|
|
58
59
|
ariaLabel?: string
|
|
60
|
+
/** Appended to the button's name while there is unread news; `{count}` is filled in. */
|
|
61
|
+
unread?: string
|
|
62
|
+
/** Screen-reader marker on an unread row. */
|
|
63
|
+
unreadItem?: string
|
|
64
|
+
/** Relative times; `{n}` is filled in. */
|
|
65
|
+
justNow?: string
|
|
66
|
+
minutes?: string
|
|
67
|
+
hours?: string
|
|
68
|
+
days?: string
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
const props = withDefaults(
|
|
@@ -83,16 +93,32 @@ const props = withDefaults(
|
|
|
83
93
|
|
|
84
94
|
// Single lookup for every label, so the template never repeats the fallbacks.
|
|
85
95
|
// `title` stays a top-level prop for compatibility and wins over labels.title.
|
|
96
|
+
const text = useLabels(
|
|
97
|
+
"notifications",
|
|
98
|
+
{
|
|
99
|
+
title: "Notifications",
|
|
100
|
+
markAllRead: "Mark all read",
|
|
101
|
+
clear: "Clear",
|
|
102
|
+
markAsRead: "Mark as read",
|
|
103
|
+
markAsUnread: "Mark as unread",
|
|
104
|
+
open: "Open",
|
|
105
|
+
dismiss: "Dismiss",
|
|
106
|
+
empty: "No notifications",
|
|
107
|
+
ariaLabel: "",
|
|
108
|
+
unread: "{count} unread",
|
|
109
|
+
unreadItem: "Unread",
|
|
110
|
+
justNow: "just now",
|
|
111
|
+
minutes: "{n}m",
|
|
112
|
+
hours: "{n}h",
|
|
113
|
+
days: "{n}d",
|
|
114
|
+
},
|
|
115
|
+
() => props.labels,
|
|
116
|
+
)
|
|
86
117
|
const l = computed(() => ({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
markAsUnread: props.labels?.markAsUnread ?? "Mark as unread",
|
|
92
|
-
open: props.labels?.open ?? "Open",
|
|
93
|
-
dismiss: props.labels?.dismiss ?? "Dismiss",
|
|
94
|
-
empty: props.emptyLabel ?? props.labels?.empty ?? "No notifications",
|
|
95
|
-
ariaLabel: props.labels?.ariaLabel ?? props.labels?.title ?? "Notifications",
|
|
118
|
+
...text.value,
|
|
119
|
+
title: props.title ?? text.value.title,
|
|
120
|
+
empty: props.emptyLabel ?? text.value.empty,
|
|
121
|
+
ariaLabel: text.value.ariaLabel || text.value.title,
|
|
96
122
|
}))
|
|
97
123
|
|
|
98
124
|
// Popover open state. A defineModel (not a plain `open` prop) gives a local
|
|
@@ -119,6 +145,11 @@ const badgeLabel = computed(() =>
|
|
|
119
145
|
)
|
|
120
146
|
const hasUnread = computed(() => unread.value > 0)
|
|
121
147
|
|
|
148
|
+
// The badge is drawn, not read: its count goes into the button's own name.
|
|
149
|
+
const buttonLabel = computed(() =>
|
|
150
|
+
hasUnread.value ? `${l.value.ariaLabel}, ${l.value.unread.replace("{count}", String(unread.value))}` : l.value.ariaLabel,
|
|
151
|
+
)
|
|
152
|
+
|
|
122
153
|
/** Icon tints for an unread row, keyed by the item's category tone. */
|
|
123
154
|
const TONE_CLASS: Record<string, string> = {
|
|
124
155
|
brand: "bg-brand-soft text-brand",
|
|
@@ -208,19 +239,32 @@ function rowMenu(item: NotificationItem): ContextMenuItemDef[] {
|
|
|
208
239
|
|
|
209
240
|
// Compact relative time ("just now", "5m", "3h", "2d"); falls back to a date.
|
|
210
241
|
// A `formatTime` prop takes over entirely — that's the localisation seam.
|
|
242
|
+
//
|
|
243
|
+
// The clock is only read after mount and then ticks, so a server render and the
|
|
244
|
+
// hydrating client agree (both print nothing) and "just now" does not stay
|
|
245
|
+
// "just now" for the life of the page.
|
|
246
|
+
const now = ref<number | null>(null)
|
|
247
|
+
let clock: ReturnType<typeof setInterval> | undefined
|
|
248
|
+
onMounted(() => {
|
|
249
|
+
now.value = Date.now()
|
|
250
|
+
clock = setInterval(() => (now.value = Date.now()), 30_000)
|
|
251
|
+
})
|
|
252
|
+
onBeforeUnmount(() => clearInterval(clock))
|
|
253
|
+
|
|
211
254
|
function timeAgo(iso?: string): string {
|
|
212
|
-
if (!iso) return ""
|
|
255
|
+
if (!iso || now.value === null) return ""
|
|
213
256
|
if (props.formatTime) return props.formatTime(iso)
|
|
214
257
|
const then = new Date(iso).getTime()
|
|
215
258
|
if (Number.isNaN(then)) return ""
|
|
216
|
-
const secs = Math.max(0, Math.round((
|
|
217
|
-
|
|
259
|
+
const secs = Math.max(0, Math.round((now.value - then) / 1000))
|
|
260
|
+
const n = (template: string, value: number) => template.replace("{n}", String(value))
|
|
261
|
+
if (secs < 45) return l.value.justNow
|
|
218
262
|
const mins = Math.round(secs / 60)
|
|
219
|
-
if (mins < 60) return
|
|
263
|
+
if (mins < 60) return n(l.value.minutes, mins)
|
|
220
264
|
const hrs = Math.round(mins / 60)
|
|
221
|
-
if (hrs < 24) return
|
|
265
|
+
if (hrs < 24) return n(l.value.hours, hrs)
|
|
222
266
|
const days = Math.round(hrs / 24)
|
|
223
|
-
if (days < 7) return
|
|
267
|
+
if (days < 7) return n(l.value.days, days)
|
|
224
268
|
return new Date(then).toLocaleDateString()
|
|
225
269
|
}
|
|
226
270
|
</script>
|
|
@@ -243,12 +287,13 @@ function timeAgo(iso?: string): string {
|
|
|
243
287
|
variant="ghost"
|
|
244
288
|
size="sm"
|
|
245
289
|
class="relative"
|
|
246
|
-
:aria-label="
|
|
290
|
+
:aria-label="buttonLabel"
|
|
247
291
|
@click="open = !open"
|
|
248
292
|
>
|
|
249
293
|
<LpIcon name="lucide:bell" :size="18" />
|
|
250
294
|
<LpBadge
|
|
251
295
|
v-if="hasUnread"
|
|
296
|
+
aria-hidden="true"
|
|
252
297
|
tone="danger"
|
|
253
298
|
class="absolute -right-1 -top-1 min-w-4 justify-center px-1 py-0 text-[10px] leading-4"
|
|
254
299
|
>
|
|
@@ -327,6 +372,7 @@ function timeAgo(iso?: string): string {
|
|
|
327
372
|
{{ item.title }}
|
|
328
373
|
</span>
|
|
329
374
|
<span v-if="!item.read" class="size-1.5 shrink-0 rounded-full bg-brand" />
|
|
375
|
+
<span v-if="!item.read" class="sr-only">{{ l.unreadItem }}</span>
|
|
330
376
|
</span>
|
|
331
377
|
<span v-if="item.body" class="mt-0.5 line-clamp-2 block text-xs text-muted">
|
|
332
378
|
{{ item.body }}
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
NumberFieldInput,
|
|
14
14
|
NumberFieldRoot,
|
|
15
15
|
} from "reka-ui"
|
|
16
|
-
import { useId } from "vue"
|
|
16
|
+
import { computed, useId } from "vue"
|
|
17
17
|
import LpIcon from "./LpIcon.vue"
|
|
18
|
+
import { useFieldControl } from "../composables/useField"
|
|
18
19
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
19
20
|
|
|
20
|
-
defineProps<{
|
|
21
|
+
const props = defineProps<{
|
|
21
22
|
// null = empty field (reka's native state); consumers that need a number
|
|
22
23
|
// should default it themselves, the field doesn't invent a value.
|
|
23
24
|
modelValue?: number | null
|
|
@@ -30,6 +31,7 @@ defineProps<{
|
|
|
30
31
|
// "Depth", "Weight" tells the reader nothing about which one wants grams.
|
|
31
32
|
// Not part of the value: it is never typed, selected, or submitted.
|
|
32
33
|
unit?: string
|
|
34
|
+
invalid?: boolean
|
|
33
35
|
}>()
|
|
34
36
|
|
|
35
37
|
defineEmits<{ (e: "update:modelValue", value: number | null): void }>()
|
|
@@ -39,9 +41,19 @@ defineEmits<{ (e: "update:modelValue", value: number | null): void }>()
|
|
|
39
41
|
// and leaves out the milliamps that make it mean anything.
|
|
40
42
|
const unitId = useId()
|
|
41
43
|
|
|
44
|
+
const field = useFieldControl(() => ({ wrapped: false, invalid: props.invalid, disabled: props.disabled }))
|
|
45
|
+
const describedBy = computed(
|
|
46
|
+
() => [field.value.describedBy, props.unit ? unitId : undefined].filter(Boolean).join(" ") || undefined,
|
|
47
|
+
)
|
|
48
|
+
|
|
42
49
|
// The field is full-width with its own control frame — the two things a consumer laying out a compact numeric column needs to change.
|
|
43
|
-
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
44
|
-
|
|
50
|
+
const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
51
|
+
[
|
|
52
|
+
"flex h-(--size-control-md) w-full items-center rounded-control border bg-surface-soft transition-colors duration-[var(--duration-fast)] focus-within:ring-2 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
|
|
53
|
+
field.value.invalid
|
|
54
|
+
? "border-danger focus-within:border-danger focus-within:ring-danger-soft"
|
|
55
|
+
: "border-line focus-within:border-brand focus-within:ring-ring",
|
|
56
|
+
].join(" "),
|
|
45
57
|
)
|
|
46
58
|
</script>
|
|
47
59
|
|
|
@@ -51,7 +63,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
51
63
|
:min="min"
|
|
52
64
|
:max="max"
|
|
53
65
|
:step="step"
|
|
54
|
-
:disabled="disabled"
|
|
66
|
+
:disabled="field.disabled"
|
|
67
|
+
:required="field.required"
|
|
55
68
|
data-lp-ring-owner
|
|
56
69
|
:class="rootClass"
|
|
57
70
|
v-bind="rest"
|
|
@@ -67,7 +80,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
67
80
|
/>
|
|
68
81
|
</NumberFieldDecrement>
|
|
69
82
|
<NumberFieldInput
|
|
70
|
-
:
|
|
83
|
+
:id="field.id"
|
|
84
|
+
:aria-describedby="describedBy"
|
|
85
|
+
:aria-invalid="field.invalid || undefined"
|
|
71
86
|
class="min-w-0 flex-1 bg-transparent text-center text-sm text-ink outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
|
|
72
87
|
/>
|
|
73
88
|
<!-- The unit sits between the value and the increment button, so a long one
|
|
@@ -126,6 +126,15 @@ const travel = (to: number) => {
|
|
|
126
126
|
const from = displayed.value
|
|
127
127
|
if (from === to) return
|
|
128
128
|
|
|
129
|
+
// Infinity and NaN (a rate divided by zero) have no digits to roll through.
|
|
130
|
+
if (!Number.isFinite(to) || !Number.isFinite(from)) {
|
|
131
|
+
displayed.value = to
|
|
132
|
+
lastTarget = to
|
|
133
|
+
settled.value = true
|
|
134
|
+
padTo.value = 0
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
129
138
|
if (prefersReducedMotion() || props.duration <= 0) {
|
|
130
139
|
displayed.value = to
|
|
131
140
|
return
|
|
@@ -221,7 +230,11 @@ const UNITS = [
|
|
|
221
230
|
|
|
222
231
|
const scaleFor = (v: number) => {
|
|
223
232
|
const abs = Math.abs(v)
|
|
224
|
-
|
|
233
|
+
let at = UNITS.findIndex((u) => abs >= u.limit)
|
|
234
|
+
// 999 950 at one place reads "1000.0k"; that is the next unit up.
|
|
235
|
+
if (at > 0 && Math.abs(Number((v / UNITS[at].limit).toFixed(1))) >= 1000) at--
|
|
236
|
+
if (at === -1 && abs >= 999.5) at = UNITS.length - 1
|
|
237
|
+
const unit = UNITS[at]
|
|
225
238
|
if (!unit) return { divisor: 1, suffix: "", decimals: 0 }
|
|
226
239
|
// One decimal is what makes 1.2k readable; a value that lands exactly on the
|
|
227
240
|
// unit (5M) keeps none, matching how these are written by hand.
|
|
@@ -234,11 +247,13 @@ const scaleFor = (v: number) => {
|
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
|
|
237
|
-
const formatted = computed(() =>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
const formatted = computed(() => formatAt(round(displayed.value), !settled.value))
|
|
251
|
+
|
|
252
|
+
// Rounded first by the caller: every branch should render the shape the
|
|
253
|
+
// endpoints imply, not whatever precision an intermediate value carries.
|
|
254
|
+
function formatAt(v: number, padded: boolean): string {
|
|
241
255
|
if (props.format) return props.format(v)
|
|
256
|
+
if (!Number.isFinite(v)) return String(v)
|
|
242
257
|
if (props.compact) {
|
|
243
258
|
/*
|
|
244
259
|
* The unit tracks the CURRENT value, not the destination — the way a real
|
|
@@ -254,7 +269,7 @@ const formatted = computed(() => {
|
|
|
254
269
|
const places = divisor === 1 ? 0 : 1
|
|
255
270
|
return `${(v / divisor).toFixed(places)}${suffix}`
|
|
256
271
|
}
|
|
257
|
-
const pad =
|
|
272
|
+
const pad = padded ? Math.max(1, padTo.value) : 1
|
|
258
273
|
if (props.grouped)
|
|
259
274
|
return v.toLocaleString(props.locale, {
|
|
260
275
|
minimumFractionDigits: props.decimals,
|
|
@@ -271,7 +286,7 @@ const formatted = computed(() => {
|
|
|
271
286
|
const int = dot === -1 ? body : body.slice(0, dot)
|
|
272
287
|
const rest = dot === -1 ? "" : body.slice(dot)
|
|
273
288
|
return `${neg ? "-" : ""}${int.padStart(pad, "0")}${rest}`
|
|
274
|
-
}
|
|
289
|
+
}
|
|
275
290
|
|
|
276
291
|
interface Cell {
|
|
277
292
|
/** Stable across renders so a column keeps its identity as digits shift. */
|
|
@@ -357,8 +372,10 @@ const offsetFor = (digit: number) => {
|
|
|
357
372
|
return digit
|
|
358
373
|
}
|
|
359
374
|
|
|
375
|
+
/* What a screen reader hears: the settled value, formatted the way it will
|
|
376
|
+
land, never a frame of the roll. */
|
|
360
377
|
const label = computed(
|
|
361
|
-
() => `${props.prefix ?? ""}${props.value}${props.suffix ?? ""}`,
|
|
378
|
+
() => `${props.prefix ?? ""}${formatAt(props.value, false)}${props.suffix ?? ""}`,
|
|
362
379
|
)
|
|
363
380
|
</script>
|
|
364
381
|
|
|
@@ -368,9 +385,8 @@ const label = computed(
|
|
|
368
385
|
<span
|
|
369
386
|
ref="revealAnchor"
|
|
370
387
|
class="lp-nf inline-flex items-baseline tabular-nums"
|
|
371
|
-
:aria-label="label"
|
|
372
|
-
role="text"
|
|
373
388
|
>
|
|
389
|
+
<span class="sr-only">{{ label }}</span>
|
|
374
390
|
<span v-if="prefix" aria-hidden="true">{{ prefix }}</span>
|
|
375
391
|
|
|
376
392
|
<span class="lp-nf__track inline-flex items-baseline" aria-hidden="true">
|