@svgrid/grid 1.2.34 → 2.0.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 +6 -5
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/SvAccordion.svelte +109 -0
- package/dist/SvAccordion.svelte.d.ts +28 -0
- package/dist/SvAlert.svelte +92 -0
- package/dist/SvAlert.svelte.d.ts +27 -0
- package/dist/SvAutoComplete.svelte +47 -18
- package/dist/SvAutoComplete.svelte.d.ts +2 -5
- package/dist/SvAvatar.svelte +75 -0
- package/dist/SvAvatar.svelte.d.ts +14 -0
- package/dist/SvAvatarGroup.svelte +48 -0
- package/dist/SvAvatarGroup.svelte.d.ts +16 -0
- package/dist/SvBadge.svelte +54 -0
- package/dist/SvBadge.svelte.d.ts +23 -0
- package/dist/SvBreadcrumb.svelte +88 -0
- package/dist/SvBreadcrumb.svelte.d.ts +23 -0
- package/dist/SvButtonGroup.svelte +121 -0
- package/dist/SvButtonGroup.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +72 -9
- package/dist/SvCalendar.svelte.d.ts +34 -1
- package/dist/SvCard.svelte +71 -0
- package/dist/SvCard.svelte.d.ts +28 -0
- package/dist/SvCarousel.svelte +134 -0
- package/dist/SvCarousel.svelte.d.ts +28 -0
- package/dist/SvCheckBox.svelte +36 -21
- package/dist/SvCheckBox.svelte.d.ts +2 -5
- package/dist/SvChip.svelte +88 -0
- package/dist/SvChip.svelte.d.ts +31 -0
- package/dist/SvCircularProgress.svelte +109 -0
- package/dist/SvCircularProgress.svelte.d.ts +31 -0
- package/dist/SvColorInput.svelte +54 -27
- package/dist/SvColorInput.svelte.d.ts +10 -6
- package/dist/SvComboBox.svelte +119 -33
- package/dist/SvComboBox.svelte.d.ts +18 -6
- package/dist/SvCommand.svelte +197 -0
- package/dist/SvCommand.svelte.d.ts +26 -0
- package/dist/SvContextMenu.svelte +96 -0
- package/dist/SvContextMenu.svelte.d.ts +25 -0
- package/dist/SvCountryInput.svelte +56 -21
- package/dist/SvCountryInput.svelte.d.ts +9 -5
- package/dist/SvDateRangeInput.svelte +256 -0
- package/dist/SvDateRangeInput.svelte.d.ts +37 -0
- package/dist/SvDateTimePicker.svelte +57 -20
- package/dist/SvDateTimePicker.svelte.d.ts +12 -4
- package/dist/SvDivider.svelte +61 -0
- package/dist/SvDivider.svelte.d.ts +22 -0
- package/dist/SvDrawer.svelte +226 -0
- package/dist/SvDrawer.svelte.d.ts +43 -0
- package/dist/SvDropDownList.svelte +114 -35
- package/dist/SvDropDownList.svelte.d.ts +7 -6
- package/dist/SvDurationInput.svelte +108 -0
- package/dist/SvDurationInput.svelte.d.ts +15 -0
- package/dist/SvEmptyState.svelte +59 -0
- package/dist/SvEmptyState.svelte.d.ts +25 -0
- package/dist/SvField.svelte +85 -0
- package/dist/SvField.svelte.d.ts +30 -0
- package/dist/SvFileUpload.svelte +169 -0
- package/dist/SvFileUpload.svelte.d.ts +24 -0
- package/dist/SvForm.svelte +7 -1
- package/dist/SvForm.svelte.d.ts +3 -0
- package/dist/SvGauge.svelte +7 -1
- package/dist/SvGauge.svelte.d.ts +11 -0
- package/dist/SvGrid.controller.svelte.d.ts +6 -0
- package/dist/SvGrid.controller.svelte.js +64 -9
- package/dist/SvGrid.css +2376 -2343
- package/dist/SvGrid.svelte +2764 -2651
- package/dist/SvGrid.types.d.ts +366 -0
- package/dist/SvGridBoard.svelte +2281 -0
- package/dist/SvGridBoard.svelte.d.ts +31 -0
- package/dist/SvGridDropdown.svelte +666 -665
- package/dist/SvGridSelect.svelte +255 -0
- package/dist/SvGridSelect.svelte.d.ts +32 -0
- package/dist/SvGroupCell.svelte +109 -0
- package/dist/SvGroupCell.svelte.d.ts +41 -0
- package/dist/SvListBox.svelte +123 -36
- package/dist/SvListBox.svelte.d.ts +12 -8
- package/dist/SvMaskedInput.svelte +53 -13
- package/dist/SvMaskedInput.svelte.d.ts +16 -6
- package/dist/SvMenu.svelte +101 -0
- package/dist/SvMenu.svelte.d.ts +26 -0
- package/dist/SvMenuList.svelte +143 -0
- package/dist/SvMenuList.svelte.d.ts +24 -0
- package/dist/SvModal.svelte +220 -0
- package/dist/SvModal.svelte.d.ts +31 -0
- package/dist/SvMultiSelect.svelte +277 -0
- package/dist/SvMultiSelect.svelte.d.ts +31 -0
- package/dist/SvNavPane.svelte +314 -0
- package/dist/SvNavPane.svelte.d.ts +59 -0
- package/dist/SvNumberInput.svelte +23 -9
- package/dist/SvNumberInput.svelte.d.ts +2 -0
- package/dist/SvOtpInput.svelte +157 -0
- package/dist/SvOtpInput.svelte.d.ts +16 -0
- package/dist/SvPagination.svelte +100 -0
- package/dist/SvPagination.svelte.d.ts +16 -0
- package/dist/SvPasswordInput.svelte +42 -12
- package/dist/SvPasswordInput.svelte.d.ts +13 -6
- package/dist/SvPhoneInput.svelte +43 -11
- package/dist/SvPhoneInput.svelte.d.ts +10 -11
- package/dist/SvPopover.svelte +127 -0
- package/dist/SvPopover.svelte.d.ts +35 -0
- package/dist/SvProgress.svelte +116 -0
- package/dist/SvProgress.svelte.d.ts +32 -0
- package/dist/SvRadioGroup.svelte +40 -22
- package/dist/SvRadioGroup.svelte.d.ts +2 -10
- package/dist/SvRating.svelte +51 -30
- package/dist/SvRating.svelte.d.ts +10 -7
- package/dist/SvRichText.svelte +192 -0
- package/dist/SvRichText.svelte.d.ts +16 -0
- package/dist/SvRowGroupPanel.svelte +147 -0
- package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
- package/dist/SvScrollArea.svelte +50 -0
- package/dist/SvScrollArea.svelte.d.ts +22 -0
- package/dist/SvSkeleton.svelte +76 -0
- package/dist/SvSkeleton.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +41 -9
- package/dist/SvSlider.svelte.d.ts +4 -4
- package/dist/SvSparkline.svelte +77 -0
- package/dist/SvSparkline.svelte.d.ts +28 -0
- package/dist/SvSplitter.svelte +138 -0
- package/dist/SvSplitter.svelte.d.ts +32 -0
- package/dist/SvStat.svelte +86 -0
- package/dist/SvStat.svelte.d.ts +30 -0
- package/dist/SvStepper.svelte +91 -0
- package/dist/SvStepper.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +39 -24
- package/dist/SvSwitchButton.svelte.d.ts +2 -5
- package/dist/SvTabs.svelte +147 -28
- package/dist/SvTabs.svelte.d.ts +16 -4
- package/dist/SvTagsInput.svelte +59 -22
- package/dist/SvTagsInput.svelte.d.ts +9 -4
- package/dist/SvTextArea.svelte +113 -0
- package/dist/SvTextArea.svelte.d.ts +18 -0
- package/dist/SvTextInput.svelte +115 -0
- package/dist/SvTextInput.svelte.d.ts +20 -0
- package/dist/SvTimePicker.svelte +19 -5
- package/dist/SvTimePicker.svelte.d.ts +12 -0
- package/dist/SvTimeline.svelte +75 -0
- package/dist/SvTimeline.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +124 -0
- package/dist/SvToaster.svelte.d.ts +10 -0
- package/dist/SvToggleButton.svelte +37 -12
- package/dist/SvToggleButton.svelte.d.ts +2 -4
- package/dist/SvTooltip.svelte +113 -0
- package/dist/SvTooltip.svelte.d.ts +22 -0
- package/dist/SvTour.svelte +151 -0
- package/dist/SvTour.svelte.d.ts +25 -0
- package/dist/SvTree.svelte +286 -44
- package/dist/SvTree.svelte.d.ts +26 -9
- package/dist/SvTreeSelect.svelte +222 -0
- package/dist/SvTreeSelect.svelte.d.ts +24 -0
- package/dist/a11y/dismissable.d.ts +32 -0
- package/dist/a11y/dismissable.js +54 -0
- package/dist/a11y/focus-trap.d.ts +39 -0
- package/dist/a11y/focus-trap.js +142 -0
- package/dist/a11y/live-region.d.ts +8 -0
- package/dist/a11y/live-region.js +60 -0
- package/dist/a11y/scroll-lock.d.ts +4 -0
- package/dist/a11y/scroll-lock.js +41 -0
- package/dist/a11y.d.ts +3 -1
- package/dist/a11y.js +1 -1
- package/dist/avatar.d.ts +10 -0
- package/dist/avatar.js +22 -0
- package/dist/builtin-editors.d.ts +5 -0
- package/dist/builtin-editors.js +55 -0
- package/dist/cdn/svgrid.js +16022 -8235
- package/dist/cdn/svgrid.svelte-external.js +14543 -6765
- package/dist/columns.js +3 -2
- package/dist/core.d.ts +7 -1
- package/dist/countries.d.ts +12 -0
- package/dist/countries.js +24 -0
- package/dist/createAccordion.svelte.d.ts +71 -0
- package/dist/createAccordion.svelte.js +99 -0
- package/dist/createAutocomplete.svelte.d.ts +13 -4
- package/dist/createAutocomplete.svelte.js +11 -1
- package/dist/createButtonGroup.svelte.d.ts +62 -0
- package/dist/createButtonGroup.svelte.js +154 -0
- package/dist/createCalendar.svelte.d.ts +5 -0
- package/dist/createCalendar.svelte.js +2 -0
- package/dist/createCheckbox.svelte.d.ts +9 -17
- package/dist/createCheckbox.svelte.js +28 -1
- package/dist/createColorInput.svelte.d.ts +12 -22
- package/dist/createColorInput.svelte.js +13 -0
- package/dist/createCombobox.svelte.d.ts +16 -5
- package/dist/createCombobox.svelte.js +13 -2
- package/dist/createCountryInput.svelte.d.ts +14 -3
- package/dist/createCountryInput.svelte.js +12 -2
- package/dist/createDateTimePicker.svelte.d.ts +17 -2
- package/dist/createDateTimePicker.svelte.js +12 -2
- package/dist/createDropdownList.svelte.d.ts +14 -5
- package/dist/createDropdownList.svelte.js +50 -1
- package/dist/createFileUpload.svelte.d.ts +68 -0
- package/dist/createFileUpload.svelte.js +122 -0
- package/dist/createListbox.svelte.d.ts +10 -1
- package/dist/createListbox.svelte.js +48 -1
- package/dist/createMaskedInput.svelte.d.ts +11 -2
- package/dist/createMaskedInput.svelte.js +11 -1
- package/dist/createNumberInput.svelte.d.ts +1 -0
- package/dist/createNumberInput.svelte.js +1 -0
- package/dist/createPasswordInput.svelte.d.ts +19 -21
- package/dist/createPasswordInput.svelte.js +16 -4
- package/dist/createPhoneInput.svelte.d.ts +26 -7
- package/dist/createPhoneInput.svelte.js +24 -3
- package/dist/createPopoverSelect.svelte.d.ts +86 -0
- package/dist/createPopoverSelect.svelte.js +196 -0
- package/dist/createRadioGroup.svelte.d.ts +9 -0
- package/dist/createRadioGroup.svelte.js +11 -1
- package/dist/createRating.svelte.d.ts +11 -0
- package/dist/createRating.svelte.js +12 -1
- package/dist/createSlider.svelte.d.ts +2 -0
- package/dist/createSlider.svelte.js +16 -5
- package/dist/createSplitter.svelte.d.ts +67 -0
- package/dist/createSplitter.svelte.js +99 -0
- package/dist/createSwitch.svelte.d.ts +9 -16
- package/dist/createSwitch.svelte.js +28 -1
- package/dist/createTabs.svelte.d.ts +7 -0
- package/dist/createTabs.svelte.js +17 -2
- package/dist/createTagsInput.svelte.d.ts +13 -1
- package/dist/createTagsInput.svelte.js +12 -3
- package/dist/createToggle.svelte.d.ts +9 -0
- package/dist/createToggle.svelte.js +12 -1
- package/dist/createTree.svelte.d.ts +19 -0
- package/dist/createTree.svelte.js +108 -15
- package/dist/debounce.d.ts +11 -0
- package/dist/debounce.js +30 -0
- package/dist/duration.d.ts +15 -0
- package/dist/duration.js +51 -0
- package/dist/editor-contract.d.ts +62 -7
- package/dist/editor-contract.js +45 -5
- package/dist/editor-registry.d.ts +67 -0
- package/dist/editor-registry.js +38 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/editors/cell-editors.js +5 -0
- package/dist/fuzzy.d.ts +3 -0
- package/dist/fuzzy.js +41 -0
- package/dist/grid-select.d.ts +9 -0
- package/dist/grid-select.js +8 -0
- package/dist/index.d.ts +79 -6
- package/dist/index.js +78 -4
- package/dist/keyboard-handlers.js +19 -0
- package/dist/list-option.d.ts +36 -0
- package/dist/list-option.js +68 -0
- package/dist/otp.d.ts +16 -0
- package/dist/otp.js +26 -0
- package/dist/paginate.d.ts +20 -0
- package/dist/paginate.js +40 -0
- package/dist/popover.d.ts +15 -1
- package/dist/popover.js +29 -3
- package/dist/recurrence.d.ts +36 -0
- package/dist/recurrence.js +90 -0
- package/dist/server-data-source.d.ts +89 -0
- package/dist/server-data-source.js +4 -0
- package/dist/server-group-model.d.ts +98 -0
- package/dist/server-group-model.js +263 -0
- package/dist/toast-store.svelte.d.ts +41 -0
- package/dist/toast-store.svelte.js +113 -0
- package/dist/tree-select.d.ts +19 -0
- package/dist/tree-select.js +40 -0
- package/dist/ui-app.types.d.ts +80 -0
- package/dist/ui-app.types.js +7 -0
- package/dist/validators.d.ts +43 -0
- package/dist/validators.js +53 -0
- package/dist/virtual.d.ts +35 -0
- package/dist/virtual.js +34 -0
- package/package.json +85 -85
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/SvAccordion.svelte +109 -0
- package/src/SvAccordion.test.ts +66 -0
- package/src/SvAlert.svelte +92 -0
- package/src/SvAutoComplete.svelte +47 -18
- package/src/SvAvatar.svelte +75 -0
- package/src/SvAvatarGroup.svelte +48 -0
- package/src/SvBadge.svelte +54 -0
- package/src/SvBreadcrumb.svelte +88 -0
- package/src/SvButtonGroup.svelte +121 -0
- package/src/SvButtonGroup.test.ts +72 -0
- package/src/SvCalendar.svelte +72 -9
- package/src/SvCard.svelte +71 -0
- package/src/SvCarousel.svelte +134 -0
- package/src/SvCheckBox.svelte +36 -21
- package/src/SvChip.svelte +88 -0
- package/src/SvCircularProgress.svelte +109 -0
- package/src/SvColorInput.svelte +54 -27
- package/src/SvComboBox.svelte +119 -33
- package/src/SvCommand.svelte +197 -0
- package/src/SvContextMenu.svelte +96 -0
- package/src/SvCountryInput.svelte +56 -21
- package/src/SvDateRangeInput.svelte +256 -0
- package/src/SvDateRangeInput.test.ts +80 -0
- package/src/SvDateTimePicker.svelte +57 -20
- package/src/SvDivider.svelte +61 -0
- package/src/SvDrawer.svelte +226 -0
- package/src/SvDrawer.test.ts +125 -0
- package/src/SvDropDownList.svelte +114 -35
- package/src/SvDurationInput.svelte +108 -0
- package/src/SvEmptyState.svelte +59 -0
- package/src/SvField.svelte +85 -0
- package/src/SvFileUpload.svelte +169 -0
- package/src/SvFileUpload.test.ts +59 -0
- package/src/SvForm.svelte +7 -1
- package/src/SvGauge.svelte +7 -1
- package/src/SvGrid.controller.svelte.ts +2553 -2494
- package/src/SvGrid.css +2376 -2343
- package/src/SvGrid.svelte +2764 -2651
- package/src/SvGrid.types.ts +944 -570
- package/src/SvGridBoard.svelte +2281 -0
- package/src/SvGridDropdown.svelte +666 -665
- package/src/SvGridSelect.svelte +255 -0
- package/src/SvGridSelect.test.ts +100 -0
- package/src/SvGroupCell.svelte +109 -0
- package/src/SvListBox.svelte +123 -36
- package/src/SvMaskedInput.svelte +53 -13
- package/src/SvMenu.svelte +101 -0
- package/src/SvMenu.test.ts +73 -0
- package/src/SvMenuList.svelte +143 -0
- package/src/SvModal.svelte +220 -0
- package/src/SvMultiSelect.svelte +277 -0
- package/src/SvMultiSelect.test.ts +167 -0
- package/src/SvNavPane.svelte +314 -0
- package/src/SvNavPane.test.ts +70 -0
- package/src/SvNumberInput.svelte +23 -9
- package/src/SvOtpInput.svelte +157 -0
- package/src/SvPagination.svelte +100 -0
- package/src/SvPasswordInput.svelte +42 -12
- package/src/SvPhoneInput.svelte +43 -11
- package/src/SvPopover.svelte +127 -0
- package/src/SvProgress.svelte +116 -0
- package/src/SvRadioGroup.svelte +40 -22
- package/src/SvRating.svelte +51 -30
- package/src/SvRichText.svelte +192 -0
- package/src/SvRowGroupPanel.svelte +147 -0
- package/src/SvScrollArea.svelte +50 -0
- package/src/SvSkeleton.svelte +76 -0
- package/src/SvSlider.svelte +41 -9
- package/src/SvSparkline.svelte +77 -0
- package/src/SvSplitter.svelte +138 -0
- package/src/SvSplitter.test.ts +55 -0
- package/src/SvStat.svelte +86 -0
- package/src/SvStepper.svelte +91 -0
- package/src/SvSwitchButton.svelte +39 -24
- package/src/SvTabs.svelte +147 -28
- package/src/SvTagsInput.svelte +59 -22
- package/src/SvTextArea.svelte +113 -0
- package/src/SvTextInput.svelte +115 -0
- package/src/SvTimePicker.svelte +19 -5
- package/src/SvTimeline.svelte +75 -0
- package/src/SvToaster.svelte +124 -0
- package/src/SvToaster.test.ts +63 -0
- package/src/SvToggleButton.svelte +37 -12
- package/src/SvTooltip.svelte +113 -0
- package/src/SvTour.svelte +151 -0
- package/src/SvTree.svelte +286 -44
- package/src/SvTreeSelect.svelte +222 -0
- package/src/SvTreeSelect.test.ts +87 -0
- package/src/a11y/dismissable.test.ts +95 -0
- package/src/a11y/dismissable.ts +94 -0
- package/src/a11y/focus-trap.test.ts +110 -0
- package/src/a11y/focus-trap.ts +134 -0
- package/src/a11y/live-region.test.ts +40 -0
- package/src/a11y/live-region.ts +65 -0
- package/src/a11y/scroll-lock.test.ts +38 -0
- package/src/a11y/scroll-lock.ts +41 -0
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -59
- package/src/avatar.test.ts +32 -0
- package/src/avatar.ts +21 -0
- package/src/board.test.ts +927 -0
- package/src/build-api.ts +798 -798
- package/src/builtin-editors.grid.test.ts +85 -0
- package/src/builtin-editors.test.ts +58 -0
- package/src/builtin-editors.ts +56 -0
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +3 -2
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1101
- package/src/countries.ts +25 -0
- package/src/createAccordion.svelte.ts +126 -0
- package/src/createAutocomplete.svelte.ts +18 -1
- package/src/createButtonGroup.svelte.ts +166 -0
- package/src/createCalendar.svelte.ts +6 -0
- package/src/createCheckbox.svelte.ts +23 -1
- package/src/createColorInput.svelte.ts +20 -0
- package/src/createCombobox.svelte.ts +22 -2
- package/src/createCountryInput.svelte.ts +21 -2
- package/src/createDateTimePicker.svelte.ts +23 -2
- package/src/createDropdownList.svelte.ts +31 -2
- package/src/createFileUpload.svelte.ts +144 -0
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +35 -3
- package/src/createMaskedInput.svelte.ts +18 -1
- package/src/createNumberInput.svelte.ts +2 -0
- package/src/createPasswordInput.svelte.ts +31 -5
- package/src/createPhoneInput.svelte.ts +38 -4
- package/src/createPopoverSelect.svelte.ts +208 -0
- package/src/createRadioGroup.svelte.ts +23 -1
- package/src/createRating.svelte.ts +26 -1
- package/src/createSlider.svelte.ts +14 -3
- package/src/createSplitter.svelte.ts +119 -0
- package/src/createSwitch.svelte.ts +26 -3
- package/src/createTabs.svelte.ts +20 -3
- package/src/createTagsInput.svelte.ts +24 -3
- package/src/createToggle.svelte.ts +25 -2
- package/src/createTree.svelte.ts +104 -14
- package/src/debounce.test.ts +50 -0
- package/src/debounce.ts +41 -0
- package/src/duration.test.ts +41 -0
- package/src/duration.ts +51 -0
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +87 -8
- package/src/editor-registry.grid.test.ts +98 -0
- package/src/editor-registry.test.ts +86 -0
- package/src/editor-registry.ts +106 -0
- package/src/editors/cell-editors.ts +10 -1
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/flex-render.ts +3 -3
- package/src/fuzzy.test.ts +40 -0
- package/src/fuzzy.ts +39 -0
- package/src/grid-select.test.ts +28 -0
- package/src/grid-select.ts +20 -0
- package/src/index.ts +463 -318
- package/src/keyboard-handlers.ts +20 -0
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-option.ts +74 -0
- package/src/list-power.test.ts +71 -0
- package/src/menus.ts +582 -582
- package/src/merge-objects.ts +48 -48
- package/src/otp.test.ts +28 -0
- package/src/otp.ts +28 -0
- package/src/overlays.test.ts +85 -0
- package/src/paginate.test.ts +41 -0
- package/src/paginate.ts +61 -0
- package/src/phone-valid.test.ts +26 -0
- package/src/popover.ts +31 -3
- package/src/progress.test.ts +55 -0
- package/src/recurrence.test.ts +91 -0
- package/src/recurrence.ts +111 -0
- package/src/render-component.ts +28 -28
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -311
- package/src/server-group-model.test.ts +294 -0
- package/src/server-group-model.ts +370 -0
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +304 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +439 -439
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.responsive.test.ts +72 -0
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/toast-store.svelte.ts +148 -0
- package/src/toast-store.test.ts +86 -0
- package/src/tree-edit.test.ts +49 -0
- package/src/tree-filter.test.ts +56 -0
- package/src/tree-select.test.ts +48 -0
- package/src/tree-select.ts +62 -0
- package/src/ui-app.test.ts +130 -0
- package/src/ui-app.types.ts +88 -0
- package/src/ui-display.test.ts +79 -0
- package/src/ui-inputs.test.ts +1 -1
- package/src/ui-status.test.ts +141 -0
- package/src/ui-tier1.test.ts +142 -0
- package/src/ui-tier2.test.ts +124 -0
- package/src/ui-tier3.test.ts +139 -0
- package/src/validators.test.ts +58 -0
- package/src/validators.ts +105 -0
- package/src/virtual.test.ts +72 -0
- package/src/virtual.ts +67 -0
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
package/src/createTabs.svelte.ts
CHANGED
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
* a render concern, so the component watches `focusTick`/`focusId` and calls
|
|
24
24
|
* `.focus()` itself; the core only decides *which* tab should take focus.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
import type { EditorDir } from './editor-contract'
|
|
27
|
+
|
|
28
|
+
export type TabItem = { id: string; label: string; disabled?: boolean; closable?: boolean }
|
|
27
29
|
|
|
28
30
|
export type TabsOrientation = 'horizontal' | 'vertical'
|
|
29
31
|
export type TabsActivation = 'automatic' | 'manual'
|
|
@@ -33,8 +35,12 @@ export type TabsConfig = {
|
|
|
33
35
|
tabs: () => ReadonlyArray<TabItem>
|
|
34
36
|
value: () => string | undefined
|
|
35
37
|
onChange?: (id: string) => void
|
|
38
|
+
/** Fired when a closable tab is closed (Delete key, or the renderer's x). */
|
|
39
|
+
onClose?: (id: string) => void
|
|
36
40
|
orientation?: () => TabsOrientation
|
|
37
41
|
activation?: () => TabsActivation
|
|
42
|
+
/** Text direction; under `'rtl'` horizontal Left/Right arrows flip. */
|
|
43
|
+
dir?: () => EditorDir | undefined
|
|
38
44
|
ariaLabel?: () => string | undefined
|
|
39
45
|
}
|
|
40
46
|
|
|
@@ -64,18 +70,28 @@ export function createTabs(config: TabsConfig) {
|
|
|
64
70
|
config.onChange?.(tid)
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
function close(tid: string) {
|
|
74
|
+
const t = tabs().find((x) => x.id === tid)
|
|
75
|
+
if (t?.closable) config.onClose?.(tid)
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
function onKeydown(e: KeyboardEvent) {
|
|
68
79
|
const list = enabled
|
|
69
80
|
if (!list.length) return
|
|
70
81
|
const idx = list.findIndex((t) => t.id === active)
|
|
71
|
-
const
|
|
72
|
-
|
|
82
|
+
const horiz = orientation() === 'horizontal'
|
|
83
|
+
// Under horizontal RTL the Left/Right arrows swap so focus follows the
|
|
84
|
+
// visual order (Right moves to the previous tab).
|
|
85
|
+
const rtlH = horiz && config.dir?.() === 'rtl'
|
|
86
|
+
const fwd = horiz ? (rtlH ? 'ArrowLeft' : 'ArrowRight') : 'ArrowDown'
|
|
87
|
+
const back = horiz ? (rtlH ? 'ArrowRight' : 'ArrowLeft') : 'ArrowUp'
|
|
73
88
|
let next: string | null = null
|
|
74
89
|
if (e.key === fwd) next = list[(idx + 1) % list.length]?.id ?? null
|
|
75
90
|
else if (e.key === back) next = list[(idx - 1 + list.length) % list.length]?.id ?? null
|
|
76
91
|
else if (e.key === 'Home') next = list[0]?.id ?? null
|
|
77
92
|
else if (e.key === 'End') next = list.at(-1)?.id ?? null
|
|
78
93
|
else if ((e.key === 'Enter' || e.key === ' ') && activation() === 'manual') { e.preventDefault(); select(active); return }
|
|
94
|
+
else if (e.key === 'Delete') { e.preventDefault(); close(active); return }
|
|
79
95
|
else return
|
|
80
96
|
if (next) {
|
|
81
97
|
e.preventDefault()
|
|
@@ -93,6 +109,7 @@ export function createTabs(config: TabsConfig) {
|
|
|
93
109
|
get focusTick() { return focusTick },
|
|
94
110
|
isActive: (tid: string) => tid === active,
|
|
95
111
|
select,
|
|
112
|
+
close,
|
|
96
113
|
onKeydown,
|
|
97
114
|
/** Spread onto the tablist container element. */
|
|
98
115
|
tablistProps: () => ({
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { editorAria, type EditorAriaState } from './editor-contract'
|
|
23
|
+
|
|
22
24
|
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
23
25
|
export type TagsInputConfig = {
|
|
24
26
|
value: () => string[]
|
|
@@ -28,6 +30,16 @@ export type TagsInputConfig = {
|
|
|
28
30
|
unique?: () => boolean
|
|
29
31
|
max?: () => number
|
|
30
32
|
ariaLabel?: () => string | undefined
|
|
33
|
+
/** Accessible label for the draft input (localizable). */
|
|
34
|
+
addLabel?: () => string | undefined
|
|
35
|
+
/** Accessible label prefix for a chip's remove button (localizable). */
|
|
36
|
+
removeLabel?: () => string | undefined
|
|
37
|
+
// Editor contract (ARIA + validation) - folded into rootProps().
|
|
38
|
+
id?: () => string | undefined
|
|
39
|
+
invalid?: () => boolean
|
|
40
|
+
required?: () => boolean
|
|
41
|
+
error?: () => string | undefined
|
|
42
|
+
hint?: () => string | undefined
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
export function createTagsInput(config: TagsInputConfig) {
|
|
@@ -36,6 +48,15 @@ export function createTagsInput(config: TagsInputConfig) {
|
|
|
36
48
|
const max = () => config.max?.() ?? Infinity
|
|
37
49
|
const tags = () => config.value()
|
|
38
50
|
|
|
51
|
+
const ariaState = (): EditorAriaState => ({
|
|
52
|
+
id: config.id?.(),
|
|
53
|
+
invalid: config.invalid?.(),
|
|
54
|
+
required: config.required?.(),
|
|
55
|
+
error: config.error?.(),
|
|
56
|
+
hint: config.hint?.(),
|
|
57
|
+
ariaLabel: config.ariaLabel?.(),
|
|
58
|
+
})
|
|
59
|
+
|
|
39
60
|
let draft = $state('')
|
|
40
61
|
|
|
41
62
|
function add(raw: string) {
|
|
@@ -70,14 +91,14 @@ export function createTagsInput(config: TagsInputConfig) {
|
|
|
70
91
|
/** Spread onto the container element. */
|
|
71
92
|
rootProps: () => ({
|
|
72
93
|
role: 'group' as const,
|
|
73
|
-
|
|
94
|
+
...editorAria(ariaState()),
|
|
74
95
|
}),
|
|
75
96
|
/** Spread onto the text <input>. */
|
|
76
97
|
inputProps: () => ({
|
|
77
98
|
type: 'text' as const,
|
|
78
99
|
value: draft,
|
|
79
100
|
disabled: disabled(),
|
|
80
|
-
'aria-label': config.ariaLabel?.() ?? 'Add tag',
|
|
101
|
+
'aria-label': config.addLabel?.() ?? config.ariaLabel?.() ?? 'Add tag',
|
|
81
102
|
oninput: onInput,
|
|
82
103
|
onkeydown: onKeydown,
|
|
83
104
|
onblur: onBlur,
|
|
@@ -89,7 +110,7 @@ export function createTagsInput(config: TagsInputConfig) {
|
|
|
89
110
|
/** Spread onto a chip's remove <button> at `index`. */
|
|
90
111
|
removeProps: (index: number) => ({
|
|
91
112
|
type: 'button' as const,
|
|
92
|
-
'aria-label':
|
|
113
|
+
'aria-label': `${config.removeLabel?.() ?? 'Remove'} ${tags()[index] ?? ''}`.trim(),
|
|
93
114
|
onclick: () => removeAt(index),
|
|
94
115
|
}),
|
|
95
116
|
}
|
|
@@ -16,18 +16,30 @@
|
|
|
16
16
|
* The styled <SvToggleButton> is just one renderer over this core.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { editorAria, type EditorAriaState } from './editor-contract'
|
|
20
|
+
|
|
19
21
|
/** Reactive inputs are passed as getters so the core tracks live prop changes
|
|
20
22
|
* (the same controlled pattern used across the SvGrid UI kit). */
|
|
21
23
|
export type ToggleConfig = {
|
|
22
24
|
pressed: () => boolean
|
|
23
25
|
onChange?: (pressed: boolean) => void
|
|
24
26
|
disabled?: () => boolean
|
|
27
|
+
// Editor contract (ARIA + validation) - folded into buttonProps().
|
|
28
|
+
id?: () => string | undefined
|
|
29
|
+
invalid?: () => boolean
|
|
30
|
+
required?: () => boolean
|
|
31
|
+
error?: () => string | undefined
|
|
32
|
+
hint?: () => string | undefined
|
|
25
33
|
ariaLabel?: () => string | undefined
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export type ToggleButtonProps = {
|
|
29
37
|
type: 'button'
|
|
38
|
+
id: string | undefined
|
|
30
39
|
'aria-pressed': boolean
|
|
40
|
+
'aria-invalid': 'true' | undefined
|
|
41
|
+
'aria-required': 'true' | undefined
|
|
42
|
+
'aria-describedby': string | undefined
|
|
31
43
|
'aria-label': string | undefined
|
|
32
44
|
disabled: boolean
|
|
33
45
|
'data-pressed': '' | undefined
|
|
@@ -53,16 +65,27 @@ export function createToggle(config: ToggleConfig): Toggle {
|
|
|
53
65
|
config.onChange?.(!pressed())
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
// The validation/label ARIA is derived from the editor contract state.
|
|
69
|
+
const ariaState = (): EditorAriaState => ({
|
|
70
|
+
id: config.id?.(),
|
|
71
|
+
invalid: config.invalid?.(),
|
|
72
|
+
required: config.required?.(),
|
|
73
|
+
error: config.error?.(),
|
|
74
|
+
hint: config.hint?.(),
|
|
75
|
+
ariaLabel: config.ariaLabel?.(),
|
|
76
|
+
})
|
|
77
|
+
|
|
56
78
|
return {
|
|
57
79
|
get pressed() { return pressed() },
|
|
58
80
|
get disabled() { return disabled() },
|
|
59
81
|
toggle,
|
|
60
82
|
buttonProps: () => ({
|
|
61
|
-
type: 'button',
|
|
83
|
+
type: 'button' as const,
|
|
84
|
+
id: config.id?.(),
|
|
62
85
|
'aria-pressed': pressed(),
|
|
63
|
-
'aria-label': config.ariaLabel?.(),
|
|
64
86
|
disabled: disabled(),
|
|
65
87
|
'data-pressed': pressed() ? '' : undefined,
|
|
88
|
+
...editorAria(ariaState()),
|
|
66
89
|
onclick: toggle,
|
|
67
90
|
}),
|
|
68
91
|
}
|
package/src/createTree.svelte.ts
CHANGED
|
@@ -26,6 +26,9 @@ export type TreeNode = {
|
|
|
26
26
|
label: string
|
|
27
27
|
children?: TreeNode[]
|
|
28
28
|
disabled?: boolean
|
|
29
|
+
/** Marks a node whose children are loaded on demand (shows an expand arrow
|
|
30
|
+
* even with no children yet; the renderer loads them on first expand). */
|
|
31
|
+
lazy?: boolean
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export type CheckState = 'checked' | 'indeterminate' | 'unchecked'
|
|
@@ -66,6 +69,64 @@ function findNode(list: ReadonlyArray<TreeNode>, id: string): TreeNode | null {
|
|
|
66
69
|
return null
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
/** Where a dragged node lands relative to the drop target. */
|
|
73
|
+
export type TreeDropPosition = 'before' | 'after' | 'inside'
|
|
74
|
+
|
|
75
|
+
/** True if `id` is inside `node`'s subtree (used to block invalid drops). */
|
|
76
|
+
export function treeContains(node: TreeNode, id: string): boolean {
|
|
77
|
+
return node.children?.some((c) => c.id === id || treeContains(c, id)) ?? false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return a NEW node tree with `dragId` moved before/after/inside `targetId`
|
|
82
|
+
* (pure - the caller sets it back as the controlled `nodes`). No-ops on invalid
|
|
83
|
+
* moves (self, or into own descendant).
|
|
84
|
+
*/
|
|
85
|
+
export function moveTreeNode(
|
|
86
|
+
nodes: ReadonlyArray<TreeNode>,
|
|
87
|
+
dragId: string,
|
|
88
|
+
targetId: string,
|
|
89
|
+
position: TreeDropPosition,
|
|
90
|
+
): TreeNode[] {
|
|
91
|
+
if (dragId === targetId) return [...nodes]
|
|
92
|
+
const dragged = findNode(nodes, dragId)
|
|
93
|
+
if (!dragged) return [...nodes]
|
|
94
|
+
if (dragId === targetId || treeContains(dragged, targetId)) return [...nodes]
|
|
95
|
+
|
|
96
|
+
const without = (list: ReadonlyArray<TreeNode>): TreeNode[] => {
|
|
97
|
+
const out: TreeNode[] = []
|
|
98
|
+
for (const n of list) {
|
|
99
|
+
if (n.id === dragId) continue
|
|
100
|
+
out.push(n.children ? { ...n, children: without(n.children) } : n)
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
const insert = (list: ReadonlyArray<TreeNode>): TreeNode[] => {
|
|
105
|
+
const out: TreeNode[] = []
|
|
106
|
+
for (const n of list) {
|
|
107
|
+
if (n.id === targetId) {
|
|
108
|
+
if (position === 'before') { out.push(dragged, n) }
|
|
109
|
+
else if (position === 'after') { out.push(n, dragged) }
|
|
110
|
+
else out.push({ ...n, children: [...(n.children ?? []), dragged] })
|
|
111
|
+
} else if (n.children) {
|
|
112
|
+
out.push({ ...n, children: insert(n.children) })
|
|
113
|
+
} else out.push(n)
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
return insert(without(nodes))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Sort siblings (recursively) by a comparator (pure). */
|
|
121
|
+
export function sortTreeNodes(
|
|
122
|
+
nodes: ReadonlyArray<TreeNode>,
|
|
123
|
+
compare: (a: TreeNode, b: TreeNode) => number,
|
|
124
|
+
): TreeNode[] {
|
|
125
|
+
return [...nodes]
|
|
126
|
+
.sort(compare)
|
|
127
|
+
.map((n) => (n.children ? { ...n, children: sortTreeNodes(n.children, compare) } : n))
|
|
128
|
+
}
|
|
129
|
+
|
|
69
130
|
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
70
131
|
export type TreeConfig = {
|
|
71
132
|
nodes: () => ReadonlyArray<TreeNode>
|
|
@@ -78,6 +139,10 @@ export type TreeConfig = {
|
|
|
78
139
|
checked?: () => string[]
|
|
79
140
|
onCheck?: (ids: string[]) => void
|
|
80
141
|
ariaLabel?: () => string | undefined
|
|
142
|
+
/** Text direction; under `'rtl'` the Left/Right expand/collapse keys swap. */
|
|
143
|
+
dir?: () => import('./editor-contract').EditorDir | undefined
|
|
144
|
+
/** Filter query: show only matching nodes + their ancestors, auto-expanded. */
|
|
145
|
+
filter?: () => string | undefined
|
|
81
146
|
}
|
|
82
147
|
|
|
83
148
|
export function createTree(config: TreeConfig) {
|
|
@@ -92,14 +157,34 @@ export function createTree(config: TreeConfig) {
|
|
|
92
157
|
if (!seeded) { seeded = true; const ids = config.expandedIds?.(); if (ids) expanded = new Set(ids) }
|
|
93
158
|
})
|
|
94
159
|
|
|
160
|
+
// When filtering, precompute which nodes to show (a match, or an ancestor of a
|
|
161
|
+
// match) and which to force-open so matches are revealed.
|
|
162
|
+
const filterInfo = $derived.by(() => {
|
|
163
|
+
const q = config.filter?.()?.trim().toLowerCase()
|
|
164
|
+
if (!q) return null
|
|
165
|
+
const visible = new Set<string>()
|
|
166
|
+
const forceOpen = new Set<string>()
|
|
167
|
+
const walk = (node: TreeNode): boolean => {
|
|
168
|
+
let childMatch = false
|
|
169
|
+
for (const c of node.children ?? []) childMatch = walk(c) || childMatch
|
|
170
|
+
const selfMatch = node.label.toLowerCase().includes(q)
|
|
171
|
+
if (selfMatch || childMatch) { visible.add(node.id); if (childMatch) forceOpen.add(node.id) }
|
|
172
|
+
return selfMatch || childMatch
|
|
173
|
+
}
|
|
174
|
+
for (const n of nodes()) walk(n)
|
|
175
|
+
return { visible, forceOpen }
|
|
176
|
+
})
|
|
177
|
+
|
|
95
178
|
const rows = $derived.by<TreeRow[]>(() => {
|
|
96
179
|
const out: TreeRow[] = []
|
|
180
|
+
const fi = filterInfo
|
|
97
181
|
const walk = (list: ReadonlyArray<TreeNode>, depth: number, parentId: string | null) => {
|
|
98
182
|
for (const node of list) {
|
|
99
|
-
|
|
100
|
-
const
|
|
183
|
+
if (fi && !fi.visible.has(node.id)) continue
|
|
184
|
+
const hasChildren = !!node.children?.length || !!node.lazy
|
|
185
|
+
const open = fi ? fi.forceOpen.has(node.id) || expanded.has(node.id) : expanded.has(node.id)
|
|
101
186
|
out.push({ node, depth, hasChildren, open, parentId, index: out.length })
|
|
102
|
-
if (
|
|
187
|
+
if (open && node.children?.length) walk(node.children, depth + 1, node.id)
|
|
103
188
|
}
|
|
104
189
|
}
|
|
105
190
|
walk(nodes(), 0, null)
|
|
@@ -120,7 +205,9 @@ export function createTree(config: TreeConfig) {
|
|
|
120
205
|
const isSelected = (id: string) => id === selected()
|
|
121
206
|
|
|
122
207
|
function toggleExpand(node: TreeNode) {
|
|
123
|
-
|
|
208
|
+
// Lazy nodes have no children *yet* - they must still be expandable so the
|
|
209
|
+
// toggle fires onToggle and the host can fetch children on demand.
|
|
210
|
+
if (!node.children?.length && !node.lazy) return
|
|
124
211
|
const next = new Set(expanded)
|
|
125
212
|
const willOpen = !next.has(node.id)
|
|
126
213
|
willOpen ? next.add(node.id) : next.delete(node.id)
|
|
@@ -147,19 +234,22 @@ export function createTree(config: TreeConfig) {
|
|
|
147
234
|
function onKeydown(e: KeyboardEvent) {
|
|
148
235
|
const item = rows[active]
|
|
149
236
|
if (!item) return
|
|
237
|
+
// "expand" opens the node or steps into the first child; "collapse" closes it
|
|
238
|
+
// or jumps to the parent. Under RTL the physical Left/Right keys are swapped.
|
|
239
|
+
const expand = () => {
|
|
240
|
+
if (item.hasChildren && !item.open) toggleExpand(item.node)
|
|
241
|
+
else if (item.hasChildren && item.open) setActive(Math.min(active + 1, rows.length - 1), true)
|
|
242
|
+
}
|
|
243
|
+
const collapse = () => {
|
|
244
|
+
if (item.hasChildren && item.open) toggleExpand(item.node)
|
|
245
|
+
else if (item.parentId) { const pi = rows.findIndex((r) => r.node.id === item.parentId); if (pi >= 0) setActive(pi, true) }
|
|
246
|
+
}
|
|
247
|
+
const rtl = config.dir?.() === 'rtl'
|
|
150
248
|
switch (e.key) {
|
|
151
249
|
case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, rows.length - 1), true); break
|
|
152
250
|
case 'ArrowUp': e.preventDefault(); setActive(Math.max(active - 1, 0), true); break
|
|
153
|
-
case 'ArrowRight':
|
|
154
|
-
|
|
155
|
-
if (item.hasChildren && !item.open) toggleExpand(item.node)
|
|
156
|
-
else if (item.hasChildren && item.open) setActive(Math.min(active + 1, rows.length - 1), true)
|
|
157
|
-
break
|
|
158
|
-
case 'ArrowLeft':
|
|
159
|
-
e.preventDefault()
|
|
160
|
-
if (item.hasChildren && item.open) toggleExpand(item.node)
|
|
161
|
-
else if (item.parentId) { const pi = rows.findIndex((r) => r.node.id === item.parentId); if (pi >= 0) setActive(pi, true) }
|
|
162
|
-
break
|
|
251
|
+
case 'ArrowRight': e.preventDefault(); (rtl ? collapse : expand)(); break
|
|
252
|
+
case 'ArrowLeft': e.preventDefault(); (rtl ? expand : collapse)(); break
|
|
163
253
|
case 'Enter': e.preventDefault(); select(item.node); break
|
|
164
254
|
case ' ': e.preventDefault(); checkable() ? toggleCheck(item.node) : select(item.node); break
|
|
165
255
|
case 'Home': e.preventDefault(); setActive(0, true); break
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { debounce } from './debounce'
|
|
3
|
+
|
|
4
|
+
beforeEach(() => vi.useFakeTimers())
|
|
5
|
+
afterEach(() => vi.useRealTimers())
|
|
6
|
+
|
|
7
|
+
describe('debounce', () => {
|
|
8
|
+
it('runs once after the quiet period with the latest args', () => {
|
|
9
|
+
const fn = vi.fn()
|
|
10
|
+
const d = debounce(fn, 200)
|
|
11
|
+
d('a'); d('b'); d('c')
|
|
12
|
+
expect(fn).not.toHaveBeenCalled()
|
|
13
|
+
vi.advanceTimersByTime(199)
|
|
14
|
+
expect(fn).not.toHaveBeenCalled()
|
|
15
|
+
vi.advanceTimersByTime(1)
|
|
16
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
17
|
+
expect(fn).toHaveBeenCalledWith('c')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('resets the timer on each call', () => {
|
|
21
|
+
const fn = vi.fn()
|
|
22
|
+
const d = debounce(fn, 200)
|
|
23
|
+
d('x')
|
|
24
|
+
vi.advanceTimersByTime(150)
|
|
25
|
+
d('y') // restarts the 200ms window
|
|
26
|
+
vi.advanceTimersByTime(150)
|
|
27
|
+
expect(fn).not.toHaveBeenCalled()
|
|
28
|
+
vi.advanceTimersByTime(50)
|
|
29
|
+
expect(fn).toHaveBeenCalledExactlyOnceWith('y')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('cancel() drops the pending call', () => {
|
|
33
|
+
const fn = vi.fn()
|
|
34
|
+
const d = debounce(fn, 200)
|
|
35
|
+
d('a')
|
|
36
|
+
d.cancel()
|
|
37
|
+
vi.advanceTimersByTime(500)
|
|
38
|
+
expect(fn).not.toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('flush() runs the pending call immediately', () => {
|
|
42
|
+
const fn = vi.fn()
|
|
43
|
+
const d = debounce(fn, 200)
|
|
44
|
+
d('a')
|
|
45
|
+
d.flush()
|
|
46
|
+
expect(fn).toHaveBeenCalledExactlyOnceWith('a')
|
|
47
|
+
vi.advanceTimersByTime(500)
|
|
48
|
+
expect(fn).toHaveBeenCalledTimes(1) // not called again
|
|
49
|
+
})
|
|
50
|
+
})
|
package/src/debounce.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debounce - coalesce rapid calls into one trailing call after `ms` of quiet.
|
|
3
|
+
* Used for remote search in the async dropdowns (SvMultiSelect/GridSelect/...),
|
|
4
|
+
* but general-purpose. Returns the debounced function with `cancel()` (drop the
|
|
5
|
+
* pending call) and `flush()` (run it now). Framework-free + pure.
|
|
6
|
+
*/
|
|
7
|
+
export type Debounced<A extends unknown[]> = ((...args: A) => void) & {
|
|
8
|
+
cancel: () => void
|
|
9
|
+
flush: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {
|
|
13
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
14
|
+
let pending: A | null = null
|
|
15
|
+
|
|
16
|
+
const run = () => {
|
|
17
|
+
timer = null
|
|
18
|
+
const args = pending
|
|
19
|
+
pending = null
|
|
20
|
+
if (args) fn(...args)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const debounced = ((...args: A) => {
|
|
24
|
+
pending = args
|
|
25
|
+
if (timer) clearTimeout(timer)
|
|
26
|
+
timer = setTimeout(run, ms)
|
|
27
|
+
}) as Debounced<A>
|
|
28
|
+
|
|
29
|
+
debounced.cancel = () => {
|
|
30
|
+
if (timer) clearTimeout(timer)
|
|
31
|
+
timer = null
|
|
32
|
+
pending = null
|
|
33
|
+
}
|
|
34
|
+
debounced.flush = () => {
|
|
35
|
+
if (timer) {
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
run()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return debounced
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { parseDuration, formatDuration } from './duration'
|
|
3
|
+
|
|
4
|
+
describe('parseDuration', () => {
|
|
5
|
+
it('parses h:mm', () => {
|
|
6
|
+
expect(parseDuration('1:30')).toBe(90)
|
|
7
|
+
expect(parseDuration('0:05')).toBe(5)
|
|
8
|
+
})
|
|
9
|
+
it('rejects invalid minutes in h:mm', () => {
|
|
10
|
+
expect(parseDuration('1:75')).toBeNull()
|
|
11
|
+
})
|
|
12
|
+
it('parses unit form incl. decimals', () => {
|
|
13
|
+
expect(parseDuration('1h 30m')).toBe(90)
|
|
14
|
+
expect(parseDuration('1.5h')).toBe(90)
|
|
15
|
+
expect(parseDuration('45m')).toBe(45)
|
|
16
|
+
})
|
|
17
|
+
it('treats a bare number as minutes', () => {
|
|
18
|
+
expect(parseDuration('90')).toBe(90)
|
|
19
|
+
})
|
|
20
|
+
it('returns null for empty / garbage', () => {
|
|
21
|
+
expect(parseDuration('')).toBeNull()
|
|
22
|
+
expect(parseDuration('abc')).toBeNull()
|
|
23
|
+
expect(parseDuration('1h banana')).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('formatDuration', () => {
|
|
28
|
+
it('formats colon style', () => {
|
|
29
|
+
expect(formatDuration(90)).toBe('1:30')
|
|
30
|
+
expect(formatDuration(5)).toBe('0:05')
|
|
31
|
+
})
|
|
32
|
+
it('formats unit style', () => {
|
|
33
|
+
expect(formatDuration(90, 'units')).toBe('1h 30m')
|
|
34
|
+
expect(formatDuration(60, 'units')).toBe('1h')
|
|
35
|
+
expect(formatDuration(45, 'units')).toBe('45m')
|
|
36
|
+
})
|
|
37
|
+
it('returns empty for invalid input', () => {
|
|
38
|
+
expect(formatDuration(-1)).toBe('')
|
|
39
|
+
expect(formatDuration(NaN)).toBe('')
|
|
40
|
+
})
|
|
41
|
+
})
|
package/src/duration.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duration - pure parse/format helpers behind <SvDurationInput>. The canonical
|
|
3
|
+
* value is a number of MINUTES; these convert to/from the human forms people
|
|
4
|
+
* type ("1h 30m", "1:30", "90"). Framework-free + pure so they are unit-tested
|
|
5
|
+
* directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a human duration to minutes, or `null` if unparseable. Accepts:
|
|
10
|
+
* - `h:mm` e.g. "1:30" -> 90
|
|
11
|
+
* - unit form e.g. "1h 30m", "1.5h", "45m" -> 90 / 90 / 45
|
|
12
|
+
* - a bare number e.g. "90" -> 90 (treated as minutes)
|
|
13
|
+
*/
|
|
14
|
+
export function parseDuration(input: string): number | null {
|
|
15
|
+
const s = input.trim().toLowerCase()
|
|
16
|
+
if (!s) return null
|
|
17
|
+
|
|
18
|
+
const colon = s.match(/^(\d+):(\d{1,2})$/)
|
|
19
|
+
if (colon) {
|
|
20
|
+
const mm = parseInt(colon[2]!, 10)
|
|
21
|
+
if (mm > 59) return null
|
|
22
|
+
return parseInt(colon[1]!, 10) * 60 + mm
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const units = s.match(/\d+(?:\.\d+)?\s*[hm]/g)
|
|
26
|
+
if (units && units.join('').replace(/\s/g, '').length === s.replace(/\s/g, '').length) {
|
|
27
|
+
let total = 0
|
|
28
|
+
for (const part of units) {
|
|
29
|
+
const m = part.match(/(\d+(?:\.\d+)?)\s*([hm])/)!
|
|
30
|
+
const n = parseFloat(m[1]!)
|
|
31
|
+
total += m[2] === 'h' ? n * 60 : n
|
|
32
|
+
}
|
|
33
|
+
return Math.round(total)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const num = Number(s)
|
|
37
|
+
return Number.isFinite(num) ? Math.round(num) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format minutes as `h:mm` (colon) or `Nh Nm` (units). Empty for invalid input. */
|
|
41
|
+
export function formatDuration(minutes: number, style: 'colon' | 'units' = 'colon'): string {
|
|
42
|
+
if (!Number.isFinite(minutes) || minutes < 0) return ''
|
|
43
|
+
const h = Math.floor(minutes / 60)
|
|
44
|
+
const m = Math.round(minutes % 60)
|
|
45
|
+
if (style === 'units') {
|
|
46
|
+
if (h && m) return `${h}h ${m}m`
|
|
47
|
+
if (h) return `${h}h`
|
|
48
|
+
return `${m}m`
|
|
49
|
+
}
|
|
50
|
+
return `${h}:${String(m).padStart(2, '0')}`
|
|
51
|
+
}
|