@leavepulse/ui 0.44.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -3
- package/dist/assets/{LpCommandPalette-DCP5g2ik.css → LpCommandPalette-HzAA4gri.css} +21 -21
- package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
- package/dist/assets/{LpShortcutsDialog-ChUyU7oV.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
- package/dist/assets/{LpTable-DDQNdf86.css → LpTable-CPwWxTJS.css} +49 -9
- package/dist/chunks/{ContextMenuItems-CrfeNPWT.js → ContextMenuItems-BtKU7074.js} +2 -1
- package/dist/chunks/{LayoutCanvas-D-jPOAvg.js → LayoutCanvas-BkaGXTjx.js} +1 -1
- package/dist/chunks/{LayoutNode-D8Xy9N-o.js → LayoutNode-B3_9E1Xa.js} +9 -5
- package/dist/chunks/{LpActionBar-BbwCF6l5.js → LpActionBar-CnfFVIwC.js} +3 -3
- package/dist/chunks/{LpAddressInput-BrOqkZqg.js → LpAddressInput-CJOYGZlI.js} +7 -7
- package/dist/chunks/{LpAlert-DkOIjsbq.js → LpAlert-B6ilrM7T.js} +1 -1
- package/dist/chunks/{LpAppShell-BUKPcHco.js → LpAppShell-B07Lf48e.js} +4 -4
- package/dist/chunks/{LpAutocomplete-DxV3Y9h4.js → LpAutocomplete-C8IgSMsi.js} +2 -1
- package/dist/chunks/{LpAvatar-ClBbEYZN.js → LpAvatar-C1XStdOo.js} +3 -3
- package/dist/chunks/{LpBreadcrumbs-B8IJgFIc.js → LpBreadcrumbs-CQJd73Gm.js} +1 -1
- package/dist/chunks/{LpButton-kqJBHIIb.js → LpButton-CKSwC6Mu.js} +1 -1
- package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
- package/dist/chunks/{LpCard-CpRByR0q.js → LpCard-C2IvucmX.js} +1 -1
- package/dist/chunks/{LpCheckbox-DpqchzH4.js → LpCheckbox-Bkj13CbW.js} +1 -1
- package/dist/chunks/{LpCodeBlock-DeHJPq7p.js → LpCodeBlock-ca7ANFyW.js} +1 -1
- package/dist/chunks/{LpColorPicker-Bhf1qxhB.js → LpColorPicker-zz4VGkG4.js} +46 -20
- package/dist/chunks/{LpConfirmDialog-CIUOSnWo.js → LpConfirmDialog-BTVAR4tF.js} +3 -3
- package/dist/chunks/{LpConfirmHost-CDm0Wtpv.js → LpConfirmHost-Ci_uCHgQ.js} +1 -1
- package/dist/chunks/{LpContextMenu-DQw5dGfG.js → LpContextMenu-BqkAYa9X.js} +2 -1
- package/dist/chunks/{LpDatePicker-Drirx1AK.js → LpDatePicker-BrdFtSTy.js} +16 -10
- package/dist/chunks/LpDateRangePicker-Drd7EbIz.js +210 -0
- package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
- package/dist/chunks/{LpDrawer-bScx-KMO.js → LpDrawer-CfRhHwmE.js} +3 -3
- package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
- package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
- package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
- package/dist/chunks/{LpFileTree-CSI_5SK2.js → LpFileTree-CNurOvkV.js} +3 -3
- package/dist/chunks/{LpFileTreeNode-B4MiSQX2.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
- package/dist/chunks/{LpFormField-CIk5qosh.js → LpFormField-CtVYj6tO.js} +29 -15
- package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
- package/dist/chunks/{LpInput-C0e8y8_d.js → LpInput-D-YGQbEv.js} +39 -16
- package/dist/chunks/{LpLightbox-Mr0W_6HK.js → LpLightbox-DSDahF23.js} +1 -1
- package/dist/chunks/{LpLogViewer-YBOaFnc_.js → LpLogViewer-KypbgOyo.js} +252 -58
- package/dist/chunks/{LpMapPicker-NUzS9e37.js → LpMapPicker-Cc84LaM3.js} +1 -1
- package/dist/chunks/{LpMediaGallery-DfJmMW0I.js → LpMediaGallery-YKr-cQPN.js} +2 -2
- package/dist/chunks/{LpModal-DPwGKBPv.js → LpModal-BzTVeNqe.js} +3 -3
- package/dist/chunks/{LpModalNav-DPg5H9f3.js → LpModalNav-CjABSGCu.js} +14 -8
- package/dist/chunks/{LpNotificationBell-t5yn-siX.js → LpNotificationBell-CCwyJP5C.js} +26 -14
- package/dist/chunks/{LpNumberField-DWnQKktS.js → LpNumberField-B77O-z28.js} +1 -1
- package/dist/chunks/{LpPagination-DFngX7Xo.js → LpPagination-BcDlo3r_.js} +1 -1
- package/dist/chunks/{LpPasswordInput-CWVDuBJ5.js → LpPasswordInput-eKzQhbQP.js} +2 -2
- package/dist/chunks/{LpPhoneInput-CD-ZTeSd.js → LpPhoneInput-BCbrrhgE.js} +126 -23
- package/dist/chunks/{LpPie-BLco31Nx.js → LpPie-BEJ2SfI6.js} +9 -10
- package/dist/chunks/{LpPopover-p_e_shN3.js → LpPopover-DCKeqiVn.js} +1 -0
- package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
- package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
- package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
- package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
- package/dist/chunks/{LpSegmented-Dejd7cIx.js → LpSegmented-C09EEam2.js} +1 -1
- package/dist/chunks/{LpSelect-DrRD9I7L.js → LpSelect-DNIL6MB9.js} +20 -23
- package/dist/chunks/{LpSidebar-BgwUNWpJ.js → LpSidebar-Bwws_X29.js} +3 -3
- package/dist/chunks/{LpSidebarNav-Bqxz8aWT.js → LpSidebarNav-kx-LdqNU.js} +3 -3
- package/dist/chunks/{LpStat-dXtyrdX9.js → LpStat-B-7udBRP.js} +1 -1
- package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
- package/dist/chunks/{LpSwitch-B1aLKulj.js → LpSwitch-OrcOJX9m.js} +1 -1
- package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
- package/dist/chunks/{LpTabs-_qMVaZJ6.js → LpTabs-29iW1XOx.js} +1 -1
- package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
- package/dist/chunks/{LpTextarea-yP8hGedR.js → LpTextarea-uuScWo6-.js} +1 -1
- package/dist/chunks/{LpThemeProvider-Wjm7Tuit.js → LpThemeProvider-Be4r2TQX.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-CKLc10KZ.js → LpThemeSwitcher-B4aVPbNC.js} +19 -22
- package/dist/chunks/{LpTimeField-C5HRgS5H.js → LpTimeField-Cqe7fXnh.js} +1 -1
- package/dist/chunks/{LpToaster--oT6lfhL.js → LpToaster-DTtWuKvq.js} +76 -9
- package/dist/chunks/{LpTooltip-CBdzeKNf.js → LpTooltip-CXVKcOXT.js} +3 -2
- package/dist/chunks/{LpTopologyCanvas-DocsLal3.js → LpTopologyCanvas-6ozNrQKh.js} +2 -1
- package/dist/chunks/{LpUptimeBar-DLtxOMRy.js → LpUptimeBar-ugcv25xj.js} +2 -2
- package/dist/chunks/icons-CkA0njzm.js +43 -0
- package/dist/chunks/{useTheme-C8JHUlPM.js → useTheme-DJm1JSsY.js} +47 -50
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.d.ts +2 -2
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.d.ts +16 -1
- package/dist/components/LpChart.vue.js +63 -5
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +2 -2
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +6 -0
- package/dist/components/LpCommandPalette.vue.js +62 -11
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
- package/dist/components/LpDateRangePicker.vue.js +4 -0
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.d.ts +8 -0
- package/dist/components/LpFieldNotice.vue.js +4 -0
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.d.ts +4 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpIcon.vue.d.ts +0 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +2 -2
- package/dist/components/LpInput.vue.d.ts +19 -0
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +52 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.d.ts +13 -0
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPopover.vue.js +1 -1
- package/dist/components/LpProgress.vue.d.ts +1 -1
- package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
- package/dist/components/LpRangeCalendar.vue.js +4 -0
- package/dist/components/LpRollingText.vue.d.ts +1 -1
- package/dist/components/LpRollingText.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +6 -0
- package/dist/components/LpShortcutsDialog.vue.js +56 -14
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +84 -2
- package/dist/components/LpTable.vue.js +481 -107
- package/dist/components/LpTableFilter.vue.d.ts +31 -0
- package/dist/components/LpTableFilter.vue.js +4 -0
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.d.ts +77 -0
- package/dist/components/LpTagsInput.vue.js +4 -0
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeProvider.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.d.ts +2 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.d.ts +2 -0
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/chart.js +3 -2
- package/dist/components/commandSearch.js +3 -0
- package/dist/components/dateRange.d.ts +43 -0
- package/dist/components/dateRange.js +76 -0
- package/dist/components/exportData.d.ts +58 -0
- package/dist/components/exportData.js +165 -0
- package/dist/components/logSearch.d.ts +18 -0
- package/dist/components/logSearch.js +36 -0
- package/dist/components/phone.d.ts +41 -0
- package/dist/components/phone.js +80 -0
- package/dist/components/tableFilter.d.ts +36 -0
- package/dist/components/tableFilter.js +84 -0
- package/dist/components/tags.d.ts +20 -0
- package/dist/components/tags.js +37 -0
- package/dist/composables/useField.d.ts +4 -0
- package/dist/composables/useFieldNotice.d.ts +17 -0
- package/dist/composables/useFieldNotice.js +28 -0
- package/dist/composables/useForm.d.ts +53 -0
- package/dist/composables/useForm.js +207 -0
- package/dist/composables/useHotkeys.d.ts +65 -5
- package/dist/composables/useHotkeys.js +170 -9
- package/dist/composables/useInputMask.d.ts +59 -0
- package/dist/composables/useInputMask.js +170 -0
- package/dist/composables/useLocale.d.ts +2 -0
- package/dist/composables/useLocale.js +9 -0
- package/dist/composables/usePaintSelect.d.ts +22 -0
- package/dist/composables/usePaintSelect.js +169 -0
- package/dist/composables/useShift.js +7 -2
- package/dist/icons-all.d.ts +1 -0
- package/dist/icons-all.js +3 -0
- package/dist/icons.d.ts +14 -1
- package/dist/index.d.ts +29 -3
- package/dist/index.js +195 -138
- package/dist/kit-icons.d.ts +2 -0
- package/dist/theme/backdrop.d.ts +12 -0
- package/dist/theme/presets.d.ts +0 -2
- package/package.json +13 -4
- package/src/component-names.ts +4 -0
- package/src/components/ContextMenuItems.vue +1 -1
- package/src/components/LayoutNode.vue +11 -2
- package/src/components/LpActionBar.vue +1 -1
- package/src/components/LpAutocomplete.vue +1 -0
- package/src/components/LpAvatar.vue +2 -2
- package/src/components/LpButton.vue +1 -1
- package/src/components/LpChart.vue +80 -1
- package/src/components/LpColorPicker.vue +34 -1
- package/src/components/LpCommandPalette.vue +82 -8
- package/src/components/LpContextMenu.vue +1 -1
- package/src/components/LpDatePicker.vue +11 -4
- package/src/components/LpDateRangePicker.vue +226 -0
- package/src/components/LpDrawer.vue +1 -1
- package/src/components/LpDropdownMenu.vue +1 -0
- package/src/components/LpFieldNotice.vue +65 -0
- package/src/components/LpFormField.vue +33 -4
- package/src/components/LpIcon.vue +22 -4
- package/src/components/LpInput.vue +46 -6
- package/src/components/LpLogViewer.vue +225 -22
- package/src/components/LpModal.vue +2 -2
- package/src/components/LpModalNav.vue +13 -2
- package/src/components/LpNotificationBell.vue +15 -7
- package/src/components/LpPhoneInput.vue +179 -20
- package/src/components/LpPie.vue +8 -5
- package/src/components/LpPopover.vue +1 -0
- package/src/components/LpRangeCalendar.vue +365 -0
- package/src/components/LpRollingText.vue +3 -0
- package/src/components/LpScrollArea.vue +22 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +77 -8
- package/src/components/LpStepper.vue +23 -10
- package/src/components/LpSwitch.vue +1 -1
- package/src/components/LpTable.vue +584 -65
- package/src/components/LpTableFilter.vue +175 -0
- package/src/components/LpTagsInput.vue +364 -0
- package/src/components/LpThemeSwitcher.vue +19 -23
- package/src/components/LpToaster.vue +91 -5
- package/src/components/LpTooltip.vue +9 -2
- package/src/components/LpTopologyCanvas.vue +7 -1
- package/src/components/chart.ts +5 -3
- package/src/components/commandSearch.ts +6 -0
- package/src/components/dateRange.ts +134 -0
- package/src/components/exportData.ts +228 -0
- package/src/components/logSearch.ts +46 -0
- package/src/components/phone.ts +140 -0
- package/src/components/tableFilter.ts +122 -0
- package/src/components/tags.ts +62 -0
- package/src/composables/useField.ts +4 -0
- package/src/composables/useFieldNotice.ts +59 -0
- package/src/composables/useForm.ts +314 -0
- package/src/composables/useHotkeys.ts +269 -13
- package/src/composables/useInputMask.ts +254 -0
- package/src/composables/useLocale.ts +14 -0
- package/src/composables/usePaintSelect.ts +243 -0
- package/src/composables/useShift.ts +13 -2
- package/src/iconSubset.d.mts +10 -0
- package/src/iconSubset.mjs +51 -0
- package/src/icons-all.ts +10 -0
- package/src/icons.ts +59 -8
- package/src/index.ts +37 -3
- package/src/kit-icons.ts +5 -0
- package/src/nuxt.ts +30 -2
- package/src/runtime/theme.plugin.ts +2 -2
- package/src/theme/backdrop.ts +69 -0
- package/src/theme/presets.ts +6 -64
- package/src/tokens/tokens.css +53 -4
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The phone-number engine behind LpPhoneInput, split from the component so its
|
|
3
|
+
* rules are testable. libphonenumber-js (the `min` metadata build) is loaded
|
|
4
|
+
* on first use, the way the country table is: an app that never renders a
|
|
5
|
+
* phone field never downloads it, and until it arrives the field still takes
|
|
6
|
+
* digits, just without formatting or checks.
|
|
7
|
+
*/
|
|
8
|
+
import type { CountryCode } from "libphonenumber-js/min"
|
|
9
|
+
|
|
10
|
+
export type PhoneLib = typeof import("libphonenumber-js/min")
|
|
11
|
+
type Examples = typeof import("libphonenumber-js/examples.mobile.json")["default"]
|
|
12
|
+
|
|
13
|
+
let lib: Promise<PhoneLib> | undefined
|
|
14
|
+
let examples: Promise<Examples> | undefined
|
|
15
|
+
|
|
16
|
+
export function loadPhoneLib(): Promise<PhoneLib> {
|
|
17
|
+
lib ??= import("libphonenumber-js/min")
|
|
18
|
+
return lib
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadPhoneExamples(): Promise<Examples> {
|
|
22
|
+
examples ??= import("libphonenumber-js/examples.mobile.json").then((m) => m.default)
|
|
23
|
+
return examples
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** E.164 allows at most 15 digits, country code included. */
|
|
27
|
+
export const E164_MAX_DIGITS = 15
|
|
28
|
+
|
|
29
|
+
export function digitsOf(text: string): string {
|
|
30
|
+
return text.replace(/\D/g, "")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** How many digits sit before `caret` in `text`. */
|
|
34
|
+
export function digitsBefore(text: string, caret: number): number {
|
|
35
|
+
return digitsOf(text.slice(0, caret)).length
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The caret position in `formatted` that has `count` digits before it, placed
|
|
40
|
+
* right after the last of them so typing continues from there. Zero digits
|
|
41
|
+
* puts it after a leading "+".
|
|
42
|
+
*/
|
|
43
|
+
export function caretAfterDigits(formatted: string, count: number): number {
|
|
44
|
+
if (count <= 0) return formatted.startsWith("+") ? 1 : 0
|
|
45
|
+
let seen = 0
|
|
46
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
47
|
+
if (/\d/.test(formatted[i]!)) {
|
|
48
|
+
seen++
|
|
49
|
+
if (seen === count) return i + 1
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return formatted.length
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type PhoneProblem = "too-short" | "too-long" | "invalid" | "invalid-country"
|
|
56
|
+
|
|
57
|
+
export interface FormattedPhone {
|
|
58
|
+
/** What the field shows: "+380 50 123 4567". */
|
|
59
|
+
display: string
|
|
60
|
+
/** Normalised "+380501234567", or "" when there are no digits. */
|
|
61
|
+
e164: string
|
|
62
|
+
/** Digits that were typed past the longest number the country allows. */
|
|
63
|
+
dropped: number
|
|
64
|
+
/** The calling code the number starts with ("380"), once recognised. */
|
|
65
|
+
callingCode?: string
|
|
66
|
+
/** Country libphonenumber recognised from the number, when it did. */
|
|
67
|
+
country?: CountryCode
|
|
68
|
+
possible: boolean
|
|
69
|
+
valid: boolean
|
|
70
|
+
type?: string
|
|
71
|
+
problem?: PhoneProblem
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format what was typed, dropping digits beyond what the number may hold:
|
|
76
|
+
* the country's own maximum once libphonenumber knows the country, the E.164
|
|
77
|
+
* cap otherwise. The value always starts with "+", since the field holds a
|
|
78
|
+
* full international number.
|
|
79
|
+
*/
|
|
80
|
+
export function formatPhone(input: string, phone: PhoneLib | undefined): FormattedPhone {
|
|
81
|
+
let digits = digitsOf(input)
|
|
82
|
+
let dropped = 0
|
|
83
|
+
if (digits.length > E164_MAX_DIGITS) {
|
|
84
|
+
dropped += digits.length - E164_MAX_DIGITS
|
|
85
|
+
digits = digits.slice(0, E164_MAX_DIGITS)
|
|
86
|
+
}
|
|
87
|
+
if (!digits) return { display: input.trim().startsWith("+") ? "+" : "", e164: "", dropped, possible: false, valid: false }
|
|
88
|
+
if (!phone) return { display: `+${digits}`, e164: `+${digits}`, dropped, possible: false, valid: false }
|
|
89
|
+
|
|
90
|
+
while (digits.length > 1 && phone.validatePhoneNumberLength(`+${digits}`) === "TOO_LONG") {
|
|
91
|
+
digits = digits.slice(0, -1)
|
|
92
|
+
dropped++
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A number that was already complete and valid does not take one more
|
|
96
|
+
// digit that would make it neither: a Ukrainian mobile ends at nine digits
|
|
97
|
+
// after +380 although the country's longest number type allows ten, and
|
|
98
|
+
// that tenth digit is almost always a slip rather than another kind of line.
|
|
99
|
+
const isValid = (d: string) => phone.parsePhoneNumberFromString(`+${d}`)?.isValid() ?? false
|
|
100
|
+
while (digits.length > 1 && !isValid(digits) && isValid(digits.slice(0, -1))) {
|
|
101
|
+
digits = digits.slice(0, -1)
|
|
102
|
+
dropped++
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const typer = new phone.AsYouType()
|
|
106
|
+
let display = typer.input(`+${digits}`)
|
|
107
|
+
// A length the country allows but no format covers (a 10-digit Ukrainian
|
|
108
|
+
// national number) comes back as one run of digits; group it by threes after
|
|
109
|
+
// the calling code so it still reads as a phone number.
|
|
110
|
+
const code = typer.getCallingCode()
|
|
111
|
+
if (code && !/\s/.test(display.slice(code.length + 2))) {
|
|
112
|
+
const national = digits.slice(code.length)
|
|
113
|
+
display = `+${code}${national ? " " + (national.match(/.{1,3}/g) ?? []).join(" ") : ""}`
|
|
114
|
+
}
|
|
115
|
+
const parsed = phone.parsePhoneNumberFromString(`+${digits}`)
|
|
116
|
+
const country = typer.getCountry() ?? parsed?.country
|
|
117
|
+
const possible = parsed?.isPossible() ?? false
|
|
118
|
+
const valid = parsed?.isValid() ?? false
|
|
119
|
+
const length = phone.validatePhoneNumberLength(`+${digits}`)
|
|
120
|
+
const problem: PhoneProblem | undefined = valid
|
|
121
|
+
? undefined
|
|
122
|
+
: length === "TOO_SHORT" || length === "NOT_A_NUMBER"
|
|
123
|
+
? "too-short"
|
|
124
|
+
: length === "INVALID_COUNTRY"
|
|
125
|
+
? "invalid-country"
|
|
126
|
+
: length === "TOO_LONG"
|
|
127
|
+
? "too-long"
|
|
128
|
+
: "invalid"
|
|
129
|
+
return {
|
|
130
|
+
display,
|
|
131
|
+
e164: `+${digits}`,
|
|
132
|
+
dropped,
|
|
133
|
+
callingCode: code,
|
|
134
|
+
country,
|
|
135
|
+
possible,
|
|
136
|
+
valid,
|
|
137
|
+
type: valid ? parsed?.getType() : undefined,
|
|
138
|
+
problem,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The rules behind LpTable's value filters and remembered state, kept pure so
|
|
3
|
+
* they can be tested. A column's filter is the set of HIDDEN values (see
|
|
4
|
+
* LpTable): a value nobody has seen before shows up rather than staying out.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Hide everything except `keep`: "show only this" in one step. */
|
|
8
|
+
export function onlyValues(values: readonly unknown[], keep: readonly unknown[]): Set<unknown> {
|
|
9
|
+
const kept = new Set(keep)
|
|
10
|
+
return new Set(values.filter((v) => !kept.has(v)))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Hide every value: the "None" step before ticking the few that matter. */
|
|
14
|
+
export function hideAllValues(values: readonly unknown[]): Set<unknown> {
|
|
15
|
+
return new Set(values)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Swap shown and hidden among the values the column holds. */
|
|
19
|
+
export function invertHidden(values: readonly unknown[], hidden: ReadonlySet<unknown>): Set<unknown> {
|
|
20
|
+
return new Set(values.filter((v) => !hidden.has(v)))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Show these values again, leaving the rest of the filter as it was. */
|
|
24
|
+
export function unhideValues(hidden: ReadonlySet<unknown>, values: readonly unknown[]): Set<unknown> {
|
|
25
|
+
const next = new Set(hidden)
|
|
26
|
+
for (const v of values) next.delete(v)
|
|
27
|
+
return next
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Lower-cased and stripped of accents, so "cafe" finds "Café". */
|
|
31
|
+
export function foldText(text: string): string {
|
|
32
|
+
return text.normalize("NFKD").replace(/\p{M}/gu, "").toLocaleLowerCase()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Every word of the query appears somewhere in the label, in any order. */
|
|
36
|
+
export function matchesQuery(label: string, query: string): boolean {
|
|
37
|
+
const words = foldText(query).split(/\s+/).filter(Boolean)
|
|
38
|
+
if (!words.length) return true
|
|
39
|
+
const hay = foldText(label)
|
|
40
|
+
return words.every((w) => hay.includes(w))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** How many rows hold each value of a column. */
|
|
44
|
+
export function countValues<Row extends Record<string, unknown>>(
|
|
45
|
+
rows: readonly Row[],
|
|
46
|
+
key: string,
|
|
47
|
+
): Map<unknown, number> {
|
|
48
|
+
const counts = new Map<unknown, number>()
|
|
49
|
+
for (const row of rows) counts.set(row[key], (counts.get(row[key]) ?? 0) + 1)
|
|
50
|
+
return counts
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A chip's text for one column's filter: the values still shown, the first
|
|
55
|
+
* `listed` by name and the rest as "+N", which says more than "3 of 12".
|
|
56
|
+
*/
|
|
57
|
+
export function summarizeShown(shownLabels: readonly string[], listed = 2): string {
|
|
58
|
+
if (shownLabels.length <= listed) return shownLabels.join(", ")
|
|
59
|
+
return `${shownLabels.slice(0, listed).join(", ")} +${shownLabels.length - listed}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── remembered state ───────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface PersistedTableState {
|
|
65
|
+
sort?: { key: string; dir: "asc" | "desc" } | null
|
|
66
|
+
hidden?: Record<string, unknown[]>
|
|
67
|
+
columns?: string[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type Plain = string | number | boolean | null
|
|
71
|
+
|
|
72
|
+
function isPlain(value: unknown): value is Plain {
|
|
73
|
+
return value === null || ["string", "number", "boolean"].includes(typeof value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The state worth keeping across visits, as JSON. Only plain values survive:
|
|
78
|
+
* a Date or an object hidden in a filter cannot come back as the same value
|
|
79
|
+
* (the filter compares by identity), so it is left out rather than restored
|
|
80
|
+
* as something that matches nothing.
|
|
81
|
+
*/
|
|
82
|
+
export function serializeTableState(state: PersistedTableState): string {
|
|
83
|
+
const hidden: Record<string, Plain[]> = {}
|
|
84
|
+
for (const [key, values] of Object.entries(state.hidden ?? {})) {
|
|
85
|
+
const plain = values.filter(isPlain)
|
|
86
|
+
if (plain.length) hidden[key] = plain
|
|
87
|
+
}
|
|
88
|
+
return JSON.stringify({ sort: state.sort ?? null, hidden, columns: state.columns ?? [] })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read it back, dropping anything about columns the table no longer has. */
|
|
92
|
+
export function parseTableState(raw: string | null, columnKeys: readonly string[]): PersistedTableState | null {
|
|
93
|
+
if (!raw) return null
|
|
94
|
+
let data: unknown
|
|
95
|
+
try {
|
|
96
|
+
data = JSON.parse(raw)
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
if (!data || typeof data !== "object") return null
|
|
101
|
+
const known = new Set(columnKeys)
|
|
102
|
+
const record = data as Record<string, unknown>
|
|
103
|
+
const out: PersistedTableState = {}
|
|
104
|
+
|
|
105
|
+
const sort = record.sort as { key?: unknown; dir?: unknown } | null | undefined
|
|
106
|
+
if (sort === null) out.sort = null
|
|
107
|
+
else if (sort && typeof sort.key === "string" && known.has(sort.key) && (sort.dir === "asc" || sort.dir === "desc")) {
|
|
108
|
+
out.sort = { key: sort.key, dir: sort.dir }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (record.hidden && typeof record.hidden === "object") {
|
|
112
|
+
out.hidden = {}
|
|
113
|
+
for (const [key, values] of Object.entries(record.hidden as Record<string, unknown>)) {
|
|
114
|
+
if (known.has(key) && Array.isArray(values)) out.hidden[key] = values.filter(isPlain)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(record.columns)) {
|
|
119
|
+
out.columns = record.columns.filter((c): c is string => typeof c === "string" && known.has(c))
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The add pipeline behind LpTagsInput, kept pure so its rules are testable:
|
|
3
|
+
* split what was typed or pasted, normalise each part, then accept or reject
|
|
4
|
+
* it against the limit, the duplicate policy and the consumer's validator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type TagDuplicates = "reject" | "ignore" | "allow"
|
|
8
|
+
|
|
9
|
+
/** An error message for an invalid tag; anything else accepts it. */
|
|
10
|
+
export type TagValidator = (tag: string, tags: readonly string[]) => string | null | undefined | void
|
|
11
|
+
|
|
12
|
+
export type TagRejection = { value: string; reason: "max" | "duplicate" | "invalid"; message?: string }
|
|
13
|
+
|
|
14
|
+
export interface AddTagsOptions {
|
|
15
|
+
max?: number
|
|
16
|
+
duplicates?: TagDuplicates
|
|
17
|
+
validate?: TagValidator
|
|
18
|
+
normalize?: (tag: string) => string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function splitTags(text: string, delimiter: string | RegExp | readonly string[]): string[] {
|
|
22
|
+
const parts =
|
|
23
|
+
delimiter instanceof RegExp
|
|
24
|
+
? text.split(new RegExp(delimiter.source, delimiter.flags.includes("g") ? delimiter.flags : `${delimiter.flags}g`))
|
|
25
|
+
: (typeof delimiter === "string" ? [delimiter] : delimiter).reduce<string[]>(
|
|
26
|
+
(acc, d) => (d ? acc.flatMap((p) => p.split(d)) : acc),
|
|
27
|
+
[text],
|
|
28
|
+
)
|
|
29
|
+
return parts.map((p) => p.trim()).filter(Boolean)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function addTags(
|
|
33
|
+
current: readonly string[],
|
|
34
|
+
candidates: readonly string[],
|
|
35
|
+
options: AddTagsOptions = {},
|
|
36
|
+
): { tags: string[]; rejected: TagRejection[]; ignored: string[] } {
|
|
37
|
+
const tags = [...current]
|
|
38
|
+
const rejected: TagRejection[] = []
|
|
39
|
+
/** Repeats dropped under the "ignore" policy, as stored, so the UI can point at the original. */
|
|
40
|
+
const ignored: string[] = []
|
|
41
|
+
const duplicates = options.duplicates ?? "reject"
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
const value = options.normalize ? options.normalize(candidate) : candidate
|
|
44
|
+
if (!value) continue
|
|
45
|
+
if (tags.includes(value) && duplicates !== "allow") {
|
|
46
|
+
if (duplicates === "reject") rejected.push({ value: candidate, reason: "duplicate" })
|
|
47
|
+
else ignored.push(value)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (options.max && tags.length >= options.max) {
|
|
51
|
+
rejected.push({ value: candidate, reason: "max" })
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
const message = options.validate?.(value, tags)
|
|
55
|
+
if (typeof message === "string" && message) {
|
|
56
|
+
rejected.push({ value: candidate, reason: "invalid", message })
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
tags.push(value)
|
|
60
|
+
}
|
|
61
|
+
return { tags, rejected, ignored }
|
|
62
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, inject, provide, useId, type ComputedRef, type InjectionKey } from "vue"
|
|
2
|
+
import type { FieldNoticeOptions } from "./useFieldNotice"
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* What a form field tells the control inside it: the id its label points at,
|
|
@@ -14,6 +15,9 @@ export interface FieldContext {
|
|
|
14
15
|
invalid: boolean
|
|
15
16
|
required: boolean
|
|
16
17
|
disabled: boolean
|
|
18
|
+
/** Float a short note under the control without moving the layout (see useFieldNotice). */
|
|
19
|
+
notify?: (text: string, options?: FieldNoticeOptions) => void
|
|
20
|
+
clearNotice?: () => void
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export const LP_FIELD: InjectionKey<ComputedRef<FieldContext>> = Symbol.for("lp-field")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getCurrentScope, onScopeDispose, ref, type Ref } from "vue"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A short note about what just happened to a field ("already added", "up to 5
|
|
5
|
+
* items") that floats under it instead of taking a line in the layout. A
|
|
6
|
+
* validation message belongs in the field's message line and may push the
|
|
7
|
+
* form down; a passing remark about one keystroke should not, or every refused
|
|
8
|
+
* paste would jolt the fields below.
|
|
9
|
+
*
|
|
10
|
+
* Render it with LpFieldNotice inside a `relative` box around the control.
|
|
11
|
+
* LpFormField does that for its control and hands `notify` down the field
|
|
12
|
+
* context, so a control inside a field (or the app) can raise one without
|
|
13
|
+
* owning any markup.
|
|
14
|
+
*/
|
|
15
|
+
export type FieldNoticeTone = "info" | "danger"
|
|
16
|
+
|
|
17
|
+
export interface FieldNoticeOptions {
|
|
18
|
+
tone?: FieldNoticeTone
|
|
19
|
+
/** How long it stays, in ms. 0 keeps it until `clear()`. */
|
|
20
|
+
duration?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FieldNotice {
|
|
24
|
+
message: Ref<string>
|
|
25
|
+
tone: Ref<FieldNoticeTone>
|
|
26
|
+
/** Bumped on every `show`, so a repeat of the same note can nudge rather than sit still. */
|
|
27
|
+
seq: Ref<number>
|
|
28
|
+
show: (text: string, options?: FieldNoticeOptions) => void
|
|
29
|
+
clear: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const FIELD_NOTICE_MS = 2600
|
|
33
|
+
|
|
34
|
+
export function useFieldNotice(defaultDuration = FIELD_NOTICE_MS): FieldNotice {
|
|
35
|
+
const message = ref("")
|
|
36
|
+
const tone = ref<FieldNoticeTone>("info")
|
|
37
|
+
const seq = ref(0)
|
|
38
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
39
|
+
|
|
40
|
+
function clear() {
|
|
41
|
+
if (timer) clearTimeout(timer)
|
|
42
|
+
timer = undefined
|
|
43
|
+
message.value = ""
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function show(text: string, options: FieldNoticeOptions = {}) {
|
|
47
|
+
if (timer) clearTimeout(timer)
|
|
48
|
+
timer = undefined
|
|
49
|
+
message.value = text
|
|
50
|
+
tone.value = options.tone ?? "info"
|
|
51
|
+
seq.value++
|
|
52
|
+
const duration = options.duration ?? defaultDuration
|
|
53
|
+
if (duration > 0 && text) timer = setTimeout(clear, duration)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (getCurrentScope()) onScopeDispose(clear)
|
|
57
|
+
|
|
58
|
+
return { message, tone, seq, show, clear }
|
|
59
|
+
}
|