@svgrid/grid 2.2.33 → 2.4.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 +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -139
- package/dist/GridMenus.svelte +705 -705
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2814 -2804
- package/dist/SvGrid.svelte +3490 -3490
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +266 -266
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +326 -326
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +289 -289
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +235 -235
- package/dist/cdn/{GridMenus-B7XhhqdN.js → GridMenus-BpVcKyUr.js} +4 -4
- package/dist/cdn/{GridMenus-BxmIvCJh.js → GridMenus-CeXL5IW4.js} +153 -150
- package/dist/cdn/{SvDateTimePicker-ClqtVIj3.js → SvDateTimePicker-DSUhIta8.js} +165 -165
- package/dist/cdn/{SvDateTimePicker-o94wfSUa.js → SvDateTimePicker-h9kPyszT.js} +572 -555
- package/dist/cdn/{SvGridChart-Cr_90pPD.js → SvGridChart-Bs2GIR2Q.js} +28 -21
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-CwhFz7GV.js} +2 -2
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-1WXuStzM.js} +2 -2
- package/dist/cdn/{SvGridChartPanel-ICbxiUvK.js → SvGridChartPanel-e5_nqNP9.js} +20 -12
- package/dist/cdn/{SvGridChartView-Bx42PMvf.js → SvGridChartView-SmPW10dI.js} +3 -3
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-eSPuJE6g.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CUOdm7YA.js → SvGridDropdown-CtdbHcIS.js} +6 -5
- package/dist/cdn/{chart-T1usDN0o.js → chart-i4XcXyHJ.js} +12 -10
- package/dist/cdn/{disclose-version-DljhZohK.js → disclose-version-Dr7rEVv-.js} +989 -1081
- package/dist/cdn/{export-format-JT5pWVT8.js → export-format-Cv1Dll6k.js} +66 -245
- package/dist/cdn/{src-erjBRYfX.js → src-DbeN1ReZ.js} +9221 -8925
- package/dist/cdn/{src-ktbQVOk-.js → src-DmEQFW5_.js} +9021 -8809
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/columns.js +9 -2
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -139
- package/src/GridMenus.svelte +705 -705
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3680 -3680
- package/src/SvGrid.css +2814 -2804
- package/src/SvGrid.svelte +3490 -3490
- package/src/SvGrid.types.ts +1708 -1708
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +266 -266
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +326 -326
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +289 -289
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +235 -235
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +805 -805
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +118 -118
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2295 -2295
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +7 -2
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1261 -1261
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- 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 +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -949
- package/src/editing.ts +606 -606
- package/src/editor-contract.ts +164 -164
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/index.ts +724 -724
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.ts +567 -567
- package/src/merge-objects.ts +48 -48
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.ts +210 -210
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +491 -491
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +526 -526
- package/src/svgrid.async-editor-options.test.ts +271 -271
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +705 -705
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +123 -123
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +322 -322
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.footer-pagesize.svelte.test.ts +86 -0
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +257 -257
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- 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/dist/SvForm.svelte
CHANGED
|
@@ -1,437 +1,437 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
// FormField/FormFieldType live in a plain `.ts` (breaks a Vite dev import
|
|
3
|
-
// cycle); re-exported so `import { type FormField } from './SvForm.svelte'`
|
|
4
|
-
// sites keep working.
|
|
5
|
-
export type { FormField, FormFieldType, FormSection, FormEntry } from './form-field'
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
/**
|
|
10
|
-
* SvForm - a schema-driven form that renders the SvGrid UI-kit controls with
|
|
11
|
-
* labels, required + custom validation, and a submit handler. The schema may be
|
|
12
|
-
* a flat `FormField[]` or mix in titled `FormSection`s; with `stepper` the
|
|
13
|
-
* sections become a validated wizard. Emits `onSubmit(values)` only when valid.
|
|
14
|
-
*/
|
|
15
|
-
import { untrack } from 'svelte'
|
|
16
|
-
import SvNumberInput from './SvNumberInput.svelte'
|
|
17
|
-
import SvPasswordInput from './SvPasswordInput.svelte'
|
|
18
|
-
import SvDropDownList from './SvDropDownList.svelte'
|
|
19
|
-
import SvMultiSelect from './SvMultiSelect.svelte'
|
|
20
|
-
import SvCheckBox from './SvCheckBox.svelte'
|
|
21
|
-
import SvSwitchButton from './SvSwitchButton.svelte'
|
|
22
|
-
import SvDateTimePicker from './SvDateTimePicker.svelte'
|
|
23
|
-
import SvColorInput from './SvColorInput.svelte'
|
|
24
|
-
import SvRating from './SvRating.svelte'
|
|
25
|
-
import SvRadioGroup from './SvRadioGroup.svelte'
|
|
26
|
-
import SvSlider from './SvSlider.svelte'
|
|
27
|
-
import SvTagsInput from './SvTagsInput.svelte'
|
|
28
|
-
import SvPhoneInput from './SvPhoneInput.svelte'
|
|
29
|
-
import SvCountryInput from './SvCountryInput.svelte'
|
|
30
|
-
import SvMaskedInput from './SvMaskedInput.svelte'
|
|
31
|
-
import SvComboBox from './SvComboBox.svelte'
|
|
32
|
-
import SvFileUpload from './SvFileUpload.svelte'
|
|
33
|
-
import SvButton from './SvButton.svelte'
|
|
34
|
-
import SvStepper from './SvStepper.svelte'
|
|
35
|
-
import SvTabs from './SvTabs.svelte'
|
|
36
|
-
import { isFormSection, type FormField, type FormEntry, type FormSection } from './form-field'
|
|
37
|
-
import { createForm, type FormMessages } from './createForm.svelte'
|
|
38
|
-
import type { EditorDir } from './editor-contract'
|
|
39
|
-
|
|
40
|
-
type Props = {
|
|
41
|
-
fields: ReadonlyArray<FormEntry>
|
|
42
|
-
initial?: Record<string, any>
|
|
43
|
-
/** Called with the visible-field values on a valid submit. May be async -
|
|
44
|
-
* the Submit button shows a loading state until it settles. */
|
|
45
|
-
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
|
46
|
-
onChange?: (values: Record<string, any>) => void
|
|
47
|
-
/** When set, a secondary Cancel button renders next to Submit. */
|
|
48
|
-
onCancel?: () => void
|
|
49
|
-
submitLabel?: string
|
|
50
|
-
cancelLabel?: string
|
|
51
|
-
/** Show a Reset button that restores the initial values. */
|
|
52
|
-
showReset?: boolean
|
|
53
|
-
resetLabel?: string
|
|
54
|
-
/** Render titled sections as a validated multi-step wizard (requires sections). */
|
|
55
|
-
stepper?: boolean
|
|
56
|
-
/** Render titled sections as tabs (requires sections; ignored with `stepper`). */
|
|
57
|
-
tabs?: boolean
|
|
58
|
-
/** Columns in the responsive field grid (a section can override its own). */
|
|
59
|
-
columns?: number
|
|
60
|
-
disabled?: boolean
|
|
61
|
-
/** Inject server-side errors (`{ field: message }`); applied reactively. */
|
|
62
|
-
serverErrors?: Record<string, string>
|
|
63
|
-
/** A form-level error message shown in a banner above the fields. */
|
|
64
|
-
formError?: string
|
|
65
|
-
/** Show a summary of field errors (with focus links) above the form after a
|
|
66
|
-
* failed submit. */
|
|
67
|
-
errorSummary?: boolean
|
|
68
|
-
/** Heading for the error summary. Default "Please fix the following:". */
|
|
69
|
-
errorSummaryTitle?: string
|
|
70
|
-
/**
|
|
71
|
-
* Re-seed the form when the `initial` object identity changes (e.g. editing a
|
|
72
|
-
* different record). `off` (default) seeds once; `always` re-seeds on every
|
|
73
|
-
* change; `ifPristine` re-seeds only when the user has no unsaved edits.
|
|
74
|
-
*/
|
|
75
|
-
reinitialize?: 'off' | 'always' | 'ifPristine'
|
|
76
|
-
/** Text direction; `rtl` mirrors the form layout. */
|
|
77
|
-
dir?: EditorDir
|
|
78
|
-
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
79
|
-
messages?: Partial<FormMessages>
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let {
|
|
83
|
-
fields, initial = {}, onSubmit, onChange, onCancel,
|
|
84
|
-
submitLabel = 'Submit', cancelLabel = 'Cancel', showReset = false, resetLabel = 'Reset',
|
|
85
|
-
stepper = false, tabs = false, columns = 1, disabled = false,
|
|
86
|
-
serverErrors, formError, errorSummary = false, errorSummaryTitle = 'Please fix the following:',
|
|
87
|
-
reinitialize = 'off', dir, messages,
|
|
88
|
-
}: Props = $props()
|
|
89
|
-
|
|
90
|
-
const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
|
|
91
|
-
const form = createForm({ fields: () => fields, initial, onSubmit, onChange, messages })
|
|
92
|
-
async function submit(e: Event) {
|
|
93
|
-
e.preventDefault()
|
|
94
|
-
const ok = await form.submit()
|
|
95
|
-
// In tabs mode, surface the first tab that has an error so it is not hidden.
|
|
96
|
-
if (!ok && tabbed) {
|
|
97
|
-
const first = form.firstErrorField()
|
|
98
|
-
const s = first ? sections.find((sec) => sec.fields.some((f) => f.name === first)) : undefined
|
|
99
|
-
if (s) activeTab = s.section
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let formEl = $state<HTMLFormElement | null>(null)
|
|
104
|
-
// Apply server-side errors reactively (e.g. after a failed API submit). The
|
|
105
|
-
// write is untracked so reading `errors` inside setErrors does not make this
|
|
106
|
-
// effect depend on (and re-run from) its own write.
|
|
107
|
-
$effect(() => {
|
|
108
|
-
const se = serverErrors
|
|
109
|
-
if (se) untrack(() => form.setErrors(se))
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
// Opt-in re-seed when the `initial` identity changes. Only `initial` is tracked
|
|
113
|
-
// (read outside `untrack`); the isDirty check + reset run untracked so this
|
|
114
|
-
// never re-fires from its own writes.
|
|
115
|
-
let seeded = false
|
|
116
|
-
$effect(() => {
|
|
117
|
-
const init = initial
|
|
118
|
-
untrack(() => {
|
|
119
|
-
if (!seeded) { seeded = true; return } // the seed-once mount pass
|
|
120
|
-
if (reinitialize === 'off') return
|
|
121
|
-
if (reinitialize === 'ifPristine' && form.isDirty) return
|
|
122
|
-
form.reset(init)
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
const summaryItems = $derived(errorSummary ? form.errorList() : [])
|
|
127
|
-
function focusField(name: string) {
|
|
128
|
-
const wrap = formEl?.querySelector<HTMLElement>(`[data-field="${name}"]`)
|
|
129
|
-
wrap?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
130
|
-
wrap?.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]')?.focus()
|
|
131
|
-
}
|
|
132
|
-
const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
|
|
133
|
-
// Resolve a field's options, running the function form (cascading) against the
|
|
134
|
-
// live values so a child list updates when its parent changes.
|
|
135
|
-
const resolveOptions = (field: FormField) =>
|
|
136
|
-
typeof field.options === 'function' ? field.options(form.values) : (field.options ?? [])
|
|
137
|
-
|
|
138
|
-
const sections = $derived(fields.filter(isFormSection) as FormSection[])
|
|
139
|
-
const wizard = $derived(stepper && sections.length > 0)
|
|
140
|
-
const tabbed = $derived(tabs && !stepper && sections.length > 0)
|
|
141
|
-
const stepItems = $derived(sections.map((s) => ({ label: s.section, description: s.description })))
|
|
142
|
-
|
|
143
|
-
let activeTab = $state<string | undefined>(undefined)
|
|
144
|
-
const sectionHasError = (s: FormSection) => s.fields.some((f) => form.error(f.name))
|
|
145
|
-
const tabItems = $derived(sections.map((s) => ({ id: s.section, label: sectionHasError(s) ? `${s.section} ⚠` : s.section })))
|
|
146
|
-
|
|
147
|
-
let step = $state(0)
|
|
148
|
-
async function next() {
|
|
149
|
-
const s = sections[step]
|
|
150
|
-
if (!s) return
|
|
151
|
-
const ok = await form.validateFields(s.fields.map((f) => f.name))
|
|
152
|
-
if (ok && step < sections.length - 1) step += 1
|
|
153
|
-
}
|
|
154
|
-
const back = () => { if (step > 0) step -= 1 }
|
|
155
|
-
|
|
156
|
-
// Shared shape passed to the `control` snippet so top-level fields and array
|
|
157
|
-
// item fields render the same set of controls without duplication.
|
|
158
|
-
type ControlOpts = {
|
|
159
|
-
id: string
|
|
160
|
-
type: string
|
|
161
|
-
label: string
|
|
162
|
-
value: any
|
|
163
|
-
onChange: (v: any) => void
|
|
164
|
-
onBlur?: () => void
|
|
165
|
-
disabled: boolean
|
|
166
|
-
readonly?: boolean
|
|
167
|
-
invalid?: boolean
|
|
168
|
-
describedby?: string
|
|
169
|
-
placeholder?: string
|
|
170
|
-
options?: ReadonlyArray<{ value: string | number; label: string; color?: string }>
|
|
171
|
-
/** The source field, for type-specific extras (mask, min/max, loadOptions...). */
|
|
172
|
-
field?: FormField
|
|
173
|
-
}
|
|
174
|
-
</script>
|
|
175
|
-
|
|
176
|
-
{#snippet control(o: ControlOpts)}
|
|
177
|
-
{#if o.type === 'textarea'}
|
|
178
|
-
<textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
|
|
179
|
-
{:else if o.type === 'number'}
|
|
180
|
-
<SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
|
|
181
|
-
{:else if o.type === 'password'}
|
|
182
|
-
<SvPasswordInput id={o.id} value={o.value ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block onChange={o.onChange} />
|
|
183
|
-
{:else if o.type === 'mask'}
|
|
184
|
-
<SvMaskedInput id={o.id} value={o.value ?? ''} mask={o.field?.mask ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={(m) => o.onChange(m)} />
|
|
185
|
-
{:else if o.type === 'phone'}
|
|
186
|
-
<SvPhoneInput id={o.id} value={o.value ?? ''} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={(v) => o.onChange(v)} />
|
|
187
|
-
{:else if o.type === 'country'}
|
|
188
|
-
<SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
189
|
-
{:else if o.type === 'select'}
|
|
190
|
-
<SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
191
|
-
{:else if o.type === 'combobox'}
|
|
192
|
-
<SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
|
|
193
|
-
{:else if o.type === 'multiselect'}
|
|
194
|
-
<SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
195
|
-
{:else if o.type === 'radio'}
|
|
196
|
-
<SvRadioGroup id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} orientation="horizontal" onChange={o.onChange} />
|
|
197
|
-
{:else if o.type === 'tags'}
|
|
198
|
-
<SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
199
|
-
{:else if o.type === 'slider'}
|
|
200
|
-
<SvSlider id={o.id} value={o.value ?? o.field?.min ?? 0} min={o.field?.min ?? 0} max={o.field?.max ?? 100} step={o.field?.step ?? 1} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} showValue onChange={o.onChange} />
|
|
201
|
-
{:else if o.type === 'checkbox'}
|
|
202
|
-
<SvCheckBox checked={!!o.value} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange}>{o.label}</SvCheckBox>
|
|
203
|
-
{:else if o.type === 'switch'}
|
|
204
|
-
<div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} readonly={o.readonly} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
|
|
205
|
-
{:else if o.type === 'date'}
|
|
206
|
-
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
|
|
207
|
-
{:else if o.type === 'datetime'}
|
|
208
|
-
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} onChange={o.onChange} />
|
|
209
|
-
{:else if o.type === 'color'}
|
|
210
|
-
<SvColorInput id={o.id} value={o.value ?? '#3b82f6'} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
211
|
-
{:else if o.type === 'file'}
|
|
212
|
-
<SvFileUpload accept={o.field?.accept} multiple={o.field?.multiple ?? false} onChange={o.onChange} />
|
|
213
|
-
{:else if o.type === 'rating'}
|
|
214
|
-
<SvRating id={o.id} value={o.value ?? 0} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange} />
|
|
215
|
-
{:else}
|
|
216
|
-
<input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
|
|
217
|
-
{/if}
|
|
218
|
-
{/snippet}
|
|
219
|
-
|
|
220
|
-
{#snippet renderItemField(arrayName: string, i: number, itf: FormField)}
|
|
221
|
-
{@const type = itf.type ?? 'text'}
|
|
222
|
-
{@const id = `f-${arrayName}-${i}-${itf.name}`}
|
|
223
|
-
{@const err = form.itemError(arrayName, i, itf.name)}
|
|
224
|
-
<div class="sv-form__field" class:has-error={err}>
|
|
225
|
-
{#if type !== 'checkbox' && type !== 'switch'}
|
|
226
|
-
<label class="sv-form__label" for={id}>{itf.label}{#if itf.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
|
|
227
|
-
{/if}
|
|
228
|
-
{@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, readonly: itf.readonly, invalid: !!err, describedby: err ? `${id}__error` : undefined, placeholder: itf.placeholder, options: resolveOptions(itf), field: itf })}
|
|
229
|
-
{#if err}<span class="sv-form__error" id={`${id}__error`} role="alert">{err}</span>{/if}
|
|
230
|
-
</div>
|
|
231
|
-
{/snippet}
|
|
232
|
-
|
|
233
|
-
{#snippet renderArray(field: FormField)}
|
|
234
|
-
{@const count = form.arrayItems(field.name).length}
|
|
235
|
-
<div class="sv-form__field is-full sv-form__array" class:has-error={form.error(field.name)}>
|
|
236
|
-
<label class="sv-form__label">{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
|
|
237
|
-
{#each form.arrayItems(field.name) as _item, i (i)}
|
|
238
|
-
<div class="sv-form__arow">
|
|
239
|
-
<div class="sv-form__grid" style:--cols={field.itemFields?.length ?? 1}>
|
|
240
|
-
{#each field.itemFields ?? [] as itf (itf.name)}{@render renderItemField(field.name, i, itf)}{/each}
|
|
241
|
-
</div>
|
|
242
|
-
<div class="sv-form__arow-tools">
|
|
243
|
-
{#if count > 1}
|
|
244
|
-
<div class="sv-form__amove">
|
|
245
|
-
<button type="button" class="sv-form__amove-btn" disabled={disabled || i === 0} onclick={() => form.moveItem(field.name, i, i - 1)} aria-label="Move up" title="Move up">↑</button>
|
|
246
|
-
<button type="button" class="sv-form__amove-btn" disabled={disabled || i === count - 1} onclick={() => form.moveItem(field.name, i, i + 1)} aria-label="Move down" title="Move down">↓</button>
|
|
247
|
-
</div>
|
|
248
|
-
{/if}
|
|
249
|
-
<button type="button" class="sv-form__aremove" disabled={disabled} onclick={() => form.removeItem(field.name, i)} aria-label="Remove item">×</button>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
{/each}
|
|
253
|
-
<div><SvButton type="button" variant="outline" size="sm" disabled={disabled} onclick={() => form.addItem(field.name)}>{field.addLabel ?? '+ Add'}</SvButton></div>
|
|
254
|
-
{#if form.error(field.name)}<span class="sv-form__error" role="alert">{form.error(field.name)}</span>{/if}
|
|
255
|
-
</div>
|
|
256
|
-
{/snippet}
|
|
257
|
-
|
|
258
|
-
{#snippet renderField(field: FormField, cols: number)}
|
|
259
|
-
{#if form.isVisible(field.name)}
|
|
260
|
-
{@const type = field.type ?? 'text'}
|
|
261
|
-
{#if type === 'array'}
|
|
262
|
-
{@render renderArray(field)}
|
|
263
|
-
{:else}
|
|
264
|
-
{@const fieldDisabled = disabled || form.isDisabled(field.name)}
|
|
265
|
-
<div class="sv-form__field" data-field={field.name} class:is-full={(field.full || (field.span ?? 1) >= cols) || cols === 1} class:has-error={form.error(field.name)} style:grid-column={field.span && field.span > 1 && field.span < cols ? `span ${field.span}` : undefined}>
|
|
266
|
-
{#if type !== 'checkbox' && type !== 'switch'}
|
|
267
|
-
<label class="sv-form__label" for={`f-${field.name}`}>
|
|
268
|
-
{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}
|
|
269
|
-
</label>
|
|
270
|
-
{/if}
|
|
271
|
-
{@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, readonly: field.readonly || !!field.computed, invalid: !!form.error(field.name), describedby: form.error(field.name) ? `f-${field.name}__error` : undefined, placeholder: field.placeholder, options: resolveOptions(field), field })}
|
|
272
|
-
{#if form.error(field.name)}
|
|
273
|
-
<span class="sv-form__error" id={`f-${field.name}__error`} role="alert">{form.error(field.name)}</span>
|
|
274
|
-
{:else if form.isValidating(field.name)}
|
|
275
|
-
<span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>{form.messages.checking}</span>
|
|
276
|
-
{:else if field.help}
|
|
277
|
-
<span class="sv-form__help">{field.help}</span>
|
|
278
|
-
{/if}
|
|
279
|
-
</div>
|
|
280
|
-
{/if}
|
|
281
|
-
{/if}
|
|
282
|
-
{/snippet}
|
|
283
|
-
|
|
284
|
-
{#snippet grid(flds: ReadonlyArray<FormField>, cols: number)}
|
|
285
|
-
<div class="sv-form__grid" style:--cols={cols}>
|
|
286
|
-
{#each flds as field (field.name)}{@render renderField(field, cols)}{/each}
|
|
287
|
-
</div>
|
|
288
|
-
{/snippet}
|
|
289
|
-
|
|
290
|
-
{#snippet actions()}
|
|
291
|
-
<div class="sv-form__actions">
|
|
292
|
-
{#if showReset}
|
|
293
|
-
<SvButton type="button" variant="ghost" disabled={disabled || form.submitting} onclick={() => form.reset()}>{resetLabel}</SvButton>
|
|
294
|
-
{/if}
|
|
295
|
-
{#if onCancel}
|
|
296
|
-
<SvButton type="button" variant="secondary" disabled={disabled || form.submitting} onclick={onCancel}>{cancelLabel}</SvButton>
|
|
297
|
-
{/if}
|
|
298
|
-
<SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
|
|
299
|
-
</div>
|
|
300
|
-
{/snippet}
|
|
301
|
-
|
|
302
|
-
<form class="sv-form" bind:this={formEl} dir={resolvedDir} onsubmit={submit} novalidate>
|
|
303
|
-
{#if formError}
|
|
304
|
-
<div class="sv-form__banner" role="alert">{formError}</div>
|
|
305
|
-
{/if}
|
|
306
|
-
{#if errorSummary && summaryItems.length}
|
|
307
|
-
<div class="sv-form__summary" role="alert">
|
|
308
|
-
<p class="sv-form__summary-title">{errorSummaryTitle}</p>
|
|
309
|
-
<ul>
|
|
310
|
-
{#each summaryItems as it (it.name)}
|
|
311
|
-
<li><button type="button" class="sv-form__summary-link" onclick={() => focusField(it.name)}>{it.label}: {it.message}</button></li>
|
|
312
|
-
{/each}
|
|
313
|
-
</ul>
|
|
314
|
-
</div>
|
|
315
|
-
{/if}
|
|
316
|
-
{#if wizard}
|
|
317
|
-
{@const s = sections[Math.min(step, sections.length - 1)]!}
|
|
318
|
-
<SvStepper steps={stepItems} current={step} onChange={(n) => (step = n)} linear />
|
|
319
|
-
<fieldset class="sv-form__section">
|
|
320
|
-
{#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
|
|
321
|
-
{@render grid(s.fields, s.columns ?? columns)}
|
|
322
|
-
</fieldset>
|
|
323
|
-
<div class="sv-form__actions">
|
|
324
|
-
{#if step > 0}
|
|
325
|
-
<SvButton type="button" variant="secondary" disabled={form.submitting} onclick={back}>Back</SvButton>
|
|
326
|
-
{/if}
|
|
327
|
-
<span class="sv-form__spacer"></span>
|
|
328
|
-
{#if step < sections.length - 1}
|
|
329
|
-
<SvButton type="button" disabled={form.submitting} onclick={next}>Next</SvButton>
|
|
330
|
-
{:else}
|
|
331
|
-
{#if showReset}
|
|
332
|
-
<SvButton type="button" variant="ghost" disabled={disabled || form.submitting} onclick={() => { form.reset(); step = 0 }}>{resetLabel}</SvButton>
|
|
333
|
-
{/if}
|
|
334
|
-
<SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
|
|
335
|
-
{/if}
|
|
336
|
-
</div>
|
|
337
|
-
{:else if tabbed}
|
|
338
|
-
<SvTabs tabs={tabItems} bind:value={activeTab} ariaLabel="Form sections">
|
|
339
|
-
{#snippet panel(id)}
|
|
340
|
-
{@const s = sections.find((sec) => sec.section === id)}
|
|
341
|
-
{#if s}
|
|
342
|
-
{#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
|
|
343
|
-
{@render grid(s.fields, s.columns ?? columns)}
|
|
344
|
-
{/if}
|
|
345
|
-
{/snippet}
|
|
346
|
-
</SvTabs>
|
|
347
|
-
{@render actions()}
|
|
348
|
-
{:else if sections.length > 0}
|
|
349
|
-
{#each fields as entry (isFormSection(entry) ? entry.section : entry.name)}
|
|
350
|
-
{#if isFormSection(entry)}
|
|
351
|
-
<fieldset class="sv-form__section">
|
|
352
|
-
<legend class="sv-form__section-title">{entry.section}</legend>
|
|
353
|
-
{#if entry.description}<p class="sv-form__section-desc">{entry.description}</p>{/if}
|
|
354
|
-
{@render grid(entry.fields, entry.columns ?? columns)}
|
|
355
|
-
</fieldset>
|
|
356
|
-
{:else}
|
|
357
|
-
{@render grid([entry], columns)}
|
|
358
|
-
{/if}
|
|
359
|
-
{/each}
|
|
360
|
-
{@render actions()}
|
|
361
|
-
{:else}
|
|
362
|
-
{@render grid(fields as ReadonlyArray<FormField>, columns)}
|
|
363
|
-
{@render actions()}
|
|
364
|
-
{/if}
|
|
365
|
-
</form>
|
|
366
|
-
|
|
367
|
-
<style>
|
|
368
|
-
.sv-form { --_accent: var(--sg-accent, #2563eb); width: 100%; display: flex; flex-direction: column; gap: 16px; }
|
|
369
|
-
.sv-form__grid { display: grid; grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr)); gap: 14px 18px; }
|
|
370
|
-
.sv-form__section { border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; padding: 16px; margin: 0; min-inline-size: 0; }
|
|
371
|
-
.sv-form__section-title { font-size: 13px; font-weight: 700; color: var(--sg-fg, #0f172a); padding: 0 4px; }
|
|
372
|
-
.sv-form__section-desc { margin: 0 0 12px; font-size: 12px; color: var(--sg-muted, #64748b); }
|
|
373
|
-
.sv-form__field { display: flex; flex-direction: column; gap: 5px; }
|
|
374
|
-
.sv-form__field.is-full { grid-column: 1 / -1; }
|
|
375
|
-
.sv-form__label { font-size: 12.5px; font-weight: 600; color: var(--sg-fg, #0f172a); }
|
|
376
|
-
.sv-form__req { color: var(--sg-danger, #dc2626); }
|
|
377
|
-
.sv-form__control {
|
|
378
|
-
width: 100%; box-sizing: border-box; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
379
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
380
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); outline: none;
|
|
381
|
-
}
|
|
382
|
-
.sv-form__textarea { height: auto; padding: 8px 10px; resize: vertical; }
|
|
383
|
-
.sv-form__control:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
384
|
-
.sv-form__field.has-error .sv-form__control { border-color: var(--sg-danger, #dc2626); }
|
|
385
|
-
.sv-form__inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
386
|
-
.sv-form__error { font-size: 11.5px; color: var(--sg-danger, #dc2626); }
|
|
387
|
-
.sv-form__help { font-size: 11.5px; color: var(--sg-muted, #64748b); }
|
|
388
|
-
.sv-form__banner {
|
|
389
|
-
padding: 10px 12px; border-radius: 8px; font-size: 13px;
|
|
390
|
-
background: color-mix(in srgb, var(--sg-danger, #dc2626) 10%, transparent);
|
|
391
|
-
color: var(--sg-danger, #dc2626); border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 35%, transparent);
|
|
392
|
-
}
|
|
393
|
-
.sv-form__summary {
|
|
394
|
-
padding: 12px 14px; border-radius: 8px;
|
|
395
|
-
background: color-mix(in srgb, var(--sg-danger, #dc2626) 7%, transparent);
|
|
396
|
-
border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 30%, transparent);
|
|
397
|
-
}
|
|
398
|
-
.sv-form__summary-title { margin: 0 0 6px; font-size: 13px; font-weight: 700; color: var(--sg-danger, #dc2626); }
|
|
399
|
-
.sv-form__summary ul { margin: 0; padding-inline-start: 18px; display: flex; flex-direction: column; gap: 3px; }
|
|
400
|
-
.sv-form__summary-link {
|
|
401
|
-
background: none; border: 0; padding: 0; font: inherit; font-size: 12.5px; text-align: start;
|
|
402
|
-
color: var(--sg-danger, #dc2626); cursor: pointer; text-decoration: underline;
|
|
403
|
-
}
|
|
404
|
-
.sv-form__summary-link:hover { color: var(--sg-fg, #0f172a); }
|
|
405
|
-
.sv-form__checking { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--sg-muted, #64748b); }
|
|
406
|
-
.sv-form__spinner {
|
|
407
|
-
width: 11px; height: 11px; border-radius: 50%;
|
|
408
|
-
border: 2px solid color-mix(in srgb, var(--_accent) 30%, transparent);
|
|
409
|
-
border-top-color: var(--_accent); animation: sv-form-spin 0.6s linear infinite;
|
|
410
|
-
}
|
|
411
|
-
@keyframes sv-form-spin { to { transform: rotate(360deg); } }
|
|
412
|
-
@media (prefers-reduced-motion: reduce) { .sv-form__spinner { animation: none; } }
|
|
413
|
-
.sv-form__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
414
|
-
.sv-form__spacer { flex: 1; }
|
|
415
|
-
.sv-form__array { gap: 8px; }
|
|
416
|
-
.sv-form__arow { display: flex; align-items: flex-start; gap: 8px; }
|
|
417
|
-
.sv-form__arow > .sv-form__grid { flex: 1; padding: 10px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; }
|
|
418
|
-
.sv-form__aremove {
|
|
419
|
-
flex: none; width: 28px; height: 28px; margin-top: 10px; border-radius: 6px; cursor: pointer;
|
|
420
|
-
border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
|
|
421
|
-
color: var(--sg-muted, #64748b); font-size: 16px; line-height: 1;
|
|
422
|
-
}
|
|
423
|
-
.sv-form__aremove:hover:not(:disabled) { color: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626); }
|
|
424
|
-
.sv-form__aremove:disabled { opacity: 0.5; cursor: default; }
|
|
425
|
-
.sv-form__arow-tools { display: flex; align-items: center; gap: 4px; margin-top: 10px; }
|
|
426
|
-
.sv-form__amove { display: inline-flex; flex-direction: column; }
|
|
427
|
-
.sv-form__amove-btn {
|
|
428
|
-
width: 22px; height: 14px; display: grid; place-items: center; padding: 0; cursor: pointer;
|
|
429
|
-
border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-muted, #64748b);
|
|
430
|
-
font-size: 10px; line-height: 1;
|
|
431
|
-
}
|
|
432
|
-
.sv-form__amove-btn:first-child { border-radius: 6px 6px 0 0; border-bottom: 0; }
|
|
433
|
-
.sv-form__amove-btn:last-child { border-radius: 0 0 6px 6px; }
|
|
434
|
-
.sv-form__amove-btn:hover:not(:disabled) { color: var(--sg-accent, #2563eb); }
|
|
435
|
-
.sv-form__amove-btn:disabled { opacity: 0.4; cursor: default; }
|
|
436
|
-
.sv-form__arow-tools .sv-form__aremove { margin-top: 0; }
|
|
437
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// FormField/FormFieldType live in a plain `.ts` (breaks a Vite dev import
|
|
3
|
+
// cycle); re-exported so `import { type FormField } from './SvForm.svelte'`
|
|
4
|
+
// sites keep working.
|
|
5
|
+
export type { FormField, FormFieldType, FormSection, FormEntry } from './form-field'
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
/**
|
|
10
|
+
* SvForm - a schema-driven form that renders the SvGrid UI-kit controls with
|
|
11
|
+
* labels, required + custom validation, and a submit handler. The schema may be
|
|
12
|
+
* a flat `FormField[]` or mix in titled `FormSection`s; with `stepper` the
|
|
13
|
+
* sections become a validated wizard. Emits `onSubmit(values)` only when valid.
|
|
14
|
+
*/
|
|
15
|
+
import { untrack } from 'svelte'
|
|
16
|
+
import SvNumberInput from './SvNumberInput.svelte'
|
|
17
|
+
import SvPasswordInput from './SvPasswordInput.svelte'
|
|
18
|
+
import SvDropDownList from './SvDropDownList.svelte'
|
|
19
|
+
import SvMultiSelect from './SvMultiSelect.svelte'
|
|
20
|
+
import SvCheckBox from './SvCheckBox.svelte'
|
|
21
|
+
import SvSwitchButton from './SvSwitchButton.svelte'
|
|
22
|
+
import SvDateTimePicker from './SvDateTimePicker.svelte'
|
|
23
|
+
import SvColorInput from './SvColorInput.svelte'
|
|
24
|
+
import SvRating from './SvRating.svelte'
|
|
25
|
+
import SvRadioGroup from './SvRadioGroup.svelte'
|
|
26
|
+
import SvSlider from './SvSlider.svelte'
|
|
27
|
+
import SvTagsInput from './SvTagsInput.svelte'
|
|
28
|
+
import SvPhoneInput from './SvPhoneInput.svelte'
|
|
29
|
+
import SvCountryInput from './SvCountryInput.svelte'
|
|
30
|
+
import SvMaskedInput from './SvMaskedInput.svelte'
|
|
31
|
+
import SvComboBox from './SvComboBox.svelte'
|
|
32
|
+
import SvFileUpload from './SvFileUpload.svelte'
|
|
33
|
+
import SvButton from './SvButton.svelte'
|
|
34
|
+
import SvStepper from './SvStepper.svelte'
|
|
35
|
+
import SvTabs from './SvTabs.svelte'
|
|
36
|
+
import { isFormSection, type FormField, type FormEntry, type FormSection } from './form-field'
|
|
37
|
+
import { createForm, type FormMessages } from './createForm.svelte'
|
|
38
|
+
import type { EditorDir } from './editor-contract'
|
|
39
|
+
|
|
40
|
+
type Props = {
|
|
41
|
+
fields: ReadonlyArray<FormEntry>
|
|
42
|
+
initial?: Record<string, any>
|
|
43
|
+
/** Called with the visible-field values on a valid submit. May be async -
|
|
44
|
+
* the Submit button shows a loading state until it settles. */
|
|
45
|
+
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
|
46
|
+
onChange?: (values: Record<string, any>) => void
|
|
47
|
+
/** When set, a secondary Cancel button renders next to Submit. */
|
|
48
|
+
onCancel?: () => void
|
|
49
|
+
submitLabel?: string
|
|
50
|
+
cancelLabel?: string
|
|
51
|
+
/** Show a Reset button that restores the initial values. */
|
|
52
|
+
showReset?: boolean
|
|
53
|
+
resetLabel?: string
|
|
54
|
+
/** Render titled sections as a validated multi-step wizard (requires sections). */
|
|
55
|
+
stepper?: boolean
|
|
56
|
+
/** Render titled sections as tabs (requires sections; ignored with `stepper`). */
|
|
57
|
+
tabs?: boolean
|
|
58
|
+
/** Columns in the responsive field grid (a section can override its own). */
|
|
59
|
+
columns?: number
|
|
60
|
+
disabled?: boolean
|
|
61
|
+
/** Inject server-side errors (`{ field: message }`); applied reactively. */
|
|
62
|
+
serverErrors?: Record<string, string>
|
|
63
|
+
/** A form-level error message shown in a banner above the fields. */
|
|
64
|
+
formError?: string
|
|
65
|
+
/** Show a summary of field errors (with focus links) above the form after a
|
|
66
|
+
* failed submit. */
|
|
67
|
+
errorSummary?: boolean
|
|
68
|
+
/** Heading for the error summary. Default "Please fix the following:". */
|
|
69
|
+
errorSummaryTitle?: string
|
|
70
|
+
/**
|
|
71
|
+
* Re-seed the form when the `initial` object identity changes (e.g. editing a
|
|
72
|
+
* different record). `off` (default) seeds once; `always` re-seeds on every
|
|
73
|
+
* change; `ifPristine` re-seeds only when the user has no unsaved edits.
|
|
74
|
+
*/
|
|
75
|
+
reinitialize?: 'off' | 'always' | 'ifPristine'
|
|
76
|
+
/** Text direction; `rtl` mirrors the form layout. */
|
|
77
|
+
dir?: EditorDir
|
|
78
|
+
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
79
|
+
messages?: Partial<FormMessages>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let {
|
|
83
|
+
fields, initial = {}, onSubmit, onChange, onCancel,
|
|
84
|
+
submitLabel = 'Submit', cancelLabel = 'Cancel', showReset = false, resetLabel = 'Reset',
|
|
85
|
+
stepper = false, tabs = false, columns = 1, disabled = false,
|
|
86
|
+
serverErrors, formError, errorSummary = false, errorSummaryTitle = 'Please fix the following:',
|
|
87
|
+
reinitialize = 'off', dir, messages,
|
|
88
|
+
}: Props = $props()
|
|
89
|
+
|
|
90
|
+
const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
|
|
91
|
+
const form = createForm({ fields: () => fields, initial, onSubmit, onChange, messages })
|
|
92
|
+
async function submit(e: Event) {
|
|
93
|
+
e.preventDefault()
|
|
94
|
+
const ok = await form.submit()
|
|
95
|
+
// In tabs mode, surface the first tab that has an error so it is not hidden.
|
|
96
|
+
if (!ok && tabbed) {
|
|
97
|
+
const first = form.firstErrorField()
|
|
98
|
+
const s = first ? sections.find((sec) => sec.fields.some((f) => f.name === first)) : undefined
|
|
99
|
+
if (s) activeTab = s.section
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let formEl = $state<HTMLFormElement | null>(null)
|
|
104
|
+
// Apply server-side errors reactively (e.g. after a failed API submit). The
|
|
105
|
+
// write is untracked so reading `errors` inside setErrors does not make this
|
|
106
|
+
// effect depend on (and re-run from) its own write.
|
|
107
|
+
$effect(() => {
|
|
108
|
+
const se = serverErrors
|
|
109
|
+
if (se) untrack(() => form.setErrors(se))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Opt-in re-seed when the `initial` identity changes. Only `initial` is tracked
|
|
113
|
+
// (read outside `untrack`); the isDirty check + reset run untracked so this
|
|
114
|
+
// never re-fires from its own writes.
|
|
115
|
+
let seeded = false
|
|
116
|
+
$effect(() => {
|
|
117
|
+
const init = initial
|
|
118
|
+
untrack(() => {
|
|
119
|
+
if (!seeded) { seeded = true; return } // the seed-once mount pass
|
|
120
|
+
if (reinitialize === 'off') return
|
|
121
|
+
if (reinitialize === 'ifPristine' && form.isDirty) return
|
|
122
|
+
form.reset(init)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const summaryItems = $derived(errorSummary ? form.errorList() : [])
|
|
127
|
+
function focusField(name: string) {
|
|
128
|
+
const wrap = formEl?.querySelector<HTMLElement>(`[data-field="${name}"]`)
|
|
129
|
+
wrap?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
130
|
+
wrap?.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]')?.focus()
|
|
131
|
+
}
|
|
132
|
+
const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
|
|
133
|
+
// Resolve a field's options, running the function form (cascading) against the
|
|
134
|
+
// live values so a child list updates when its parent changes.
|
|
135
|
+
const resolveOptions = (field: FormField) =>
|
|
136
|
+
typeof field.options === 'function' ? field.options(form.values) : (field.options ?? [])
|
|
137
|
+
|
|
138
|
+
const sections = $derived(fields.filter(isFormSection) as FormSection[])
|
|
139
|
+
const wizard = $derived(stepper && sections.length > 0)
|
|
140
|
+
const tabbed = $derived(tabs && !stepper && sections.length > 0)
|
|
141
|
+
const stepItems = $derived(sections.map((s) => ({ label: s.section, description: s.description })))
|
|
142
|
+
|
|
143
|
+
let activeTab = $state<string | undefined>(undefined)
|
|
144
|
+
const sectionHasError = (s: FormSection) => s.fields.some((f) => form.error(f.name))
|
|
145
|
+
const tabItems = $derived(sections.map((s) => ({ id: s.section, label: sectionHasError(s) ? `${s.section} ⚠` : s.section })))
|
|
146
|
+
|
|
147
|
+
let step = $state(0)
|
|
148
|
+
async function next() {
|
|
149
|
+
const s = sections[step]
|
|
150
|
+
if (!s) return
|
|
151
|
+
const ok = await form.validateFields(s.fields.map((f) => f.name))
|
|
152
|
+
if (ok && step < sections.length - 1) step += 1
|
|
153
|
+
}
|
|
154
|
+
const back = () => { if (step > 0) step -= 1 }
|
|
155
|
+
|
|
156
|
+
// Shared shape passed to the `control` snippet so top-level fields and array
|
|
157
|
+
// item fields render the same set of controls without duplication.
|
|
158
|
+
type ControlOpts = {
|
|
159
|
+
id: string
|
|
160
|
+
type: string
|
|
161
|
+
label: string
|
|
162
|
+
value: any
|
|
163
|
+
onChange: (v: any) => void
|
|
164
|
+
onBlur?: () => void
|
|
165
|
+
disabled: boolean
|
|
166
|
+
readonly?: boolean
|
|
167
|
+
invalid?: boolean
|
|
168
|
+
describedby?: string
|
|
169
|
+
placeholder?: string
|
|
170
|
+
options?: ReadonlyArray<{ value: string | number; label: string; color?: string }>
|
|
171
|
+
/** The source field, for type-specific extras (mask, min/max, loadOptions...). */
|
|
172
|
+
field?: FormField
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
{#snippet control(o: ControlOpts)}
|
|
177
|
+
{#if o.type === 'textarea'}
|
|
178
|
+
<textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
|
|
179
|
+
{:else if o.type === 'number'}
|
|
180
|
+
<SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
|
|
181
|
+
{:else if o.type === 'password'}
|
|
182
|
+
<SvPasswordInput id={o.id} value={o.value ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block onChange={o.onChange} />
|
|
183
|
+
{:else if o.type === 'mask'}
|
|
184
|
+
<SvMaskedInput id={o.id} value={o.value ?? ''} mask={o.field?.mask ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={(m) => o.onChange(m)} />
|
|
185
|
+
{:else if o.type === 'phone'}
|
|
186
|
+
<SvPhoneInput id={o.id} value={o.value ?? ''} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={(v) => o.onChange(v)} />
|
|
187
|
+
{:else if o.type === 'country'}
|
|
188
|
+
<SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
189
|
+
{:else if o.type === 'select'}
|
|
190
|
+
<SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
191
|
+
{:else if o.type === 'combobox'}
|
|
192
|
+
<SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
|
|
193
|
+
{:else if o.type === 'multiselect'}
|
|
194
|
+
<SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
195
|
+
{:else if o.type === 'radio'}
|
|
196
|
+
<SvRadioGroup id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} orientation="horizontal" onChange={o.onChange} />
|
|
197
|
+
{:else if o.type === 'tags'}
|
|
198
|
+
<SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
199
|
+
{:else if o.type === 'slider'}
|
|
200
|
+
<SvSlider id={o.id} value={o.value ?? o.field?.min ?? 0} min={o.field?.min ?? 0} max={o.field?.max ?? 100} step={o.field?.step ?? 1} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} showValue onChange={o.onChange} />
|
|
201
|
+
{:else if o.type === 'checkbox'}
|
|
202
|
+
<SvCheckBox checked={!!o.value} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange}>{o.label}</SvCheckBox>
|
|
203
|
+
{:else if o.type === 'switch'}
|
|
204
|
+
<div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} readonly={o.readonly} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
|
|
205
|
+
{:else if o.type === 'date'}
|
|
206
|
+
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
|
|
207
|
+
{:else if o.type === 'datetime'}
|
|
208
|
+
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} onChange={o.onChange} />
|
|
209
|
+
{:else if o.type === 'color'}
|
|
210
|
+
<SvColorInput id={o.id} value={o.value ?? '#3b82f6'} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
211
|
+
{:else if o.type === 'file'}
|
|
212
|
+
<SvFileUpload accept={o.field?.accept} multiple={o.field?.multiple ?? false} onChange={o.onChange} />
|
|
213
|
+
{:else if o.type === 'rating'}
|
|
214
|
+
<SvRating id={o.id} value={o.value ?? 0} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange} />
|
|
215
|
+
{:else}
|
|
216
|
+
<input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
|
|
217
|
+
{/if}
|
|
218
|
+
{/snippet}
|
|
219
|
+
|
|
220
|
+
{#snippet renderItemField(arrayName: string, i: number, itf: FormField)}
|
|
221
|
+
{@const type = itf.type ?? 'text'}
|
|
222
|
+
{@const id = `f-${arrayName}-${i}-${itf.name}`}
|
|
223
|
+
{@const err = form.itemError(arrayName, i, itf.name)}
|
|
224
|
+
<div class="sv-form__field" class:has-error={err}>
|
|
225
|
+
{#if type !== 'checkbox' && type !== 'switch'}
|
|
226
|
+
<label class="sv-form__label" for={id}>{itf.label}{#if itf.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
|
|
227
|
+
{/if}
|
|
228
|
+
{@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, readonly: itf.readonly, invalid: !!err, describedby: err ? `${id}__error` : undefined, placeholder: itf.placeholder, options: resolveOptions(itf), field: itf })}
|
|
229
|
+
{#if err}<span class="sv-form__error" id={`${id}__error`} role="alert">{err}</span>{/if}
|
|
230
|
+
</div>
|
|
231
|
+
{/snippet}
|
|
232
|
+
|
|
233
|
+
{#snippet renderArray(field: FormField)}
|
|
234
|
+
{@const count = form.arrayItems(field.name).length}
|
|
235
|
+
<div class="sv-form__field is-full sv-form__array" class:has-error={form.error(field.name)}>
|
|
236
|
+
<label class="sv-form__label">{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
|
|
237
|
+
{#each form.arrayItems(field.name) as _item, i (i)}
|
|
238
|
+
<div class="sv-form__arow">
|
|
239
|
+
<div class="sv-form__grid" style:--cols={field.itemFields?.length ?? 1}>
|
|
240
|
+
{#each field.itemFields ?? [] as itf (itf.name)}{@render renderItemField(field.name, i, itf)}{/each}
|
|
241
|
+
</div>
|
|
242
|
+
<div class="sv-form__arow-tools">
|
|
243
|
+
{#if count > 1}
|
|
244
|
+
<div class="sv-form__amove">
|
|
245
|
+
<button type="button" class="sv-form__amove-btn" disabled={disabled || i === 0} onclick={() => form.moveItem(field.name, i, i - 1)} aria-label="Move up" title="Move up">↑</button>
|
|
246
|
+
<button type="button" class="sv-form__amove-btn" disabled={disabled || i === count - 1} onclick={() => form.moveItem(field.name, i, i + 1)} aria-label="Move down" title="Move down">↓</button>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
249
|
+
<button type="button" class="sv-form__aremove" disabled={disabled} onclick={() => form.removeItem(field.name, i)} aria-label="Remove item">×</button>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
{/each}
|
|
253
|
+
<div><SvButton type="button" variant="outline" size="sm" disabled={disabled} onclick={() => form.addItem(field.name)}>{field.addLabel ?? '+ Add'}</SvButton></div>
|
|
254
|
+
{#if form.error(field.name)}<span class="sv-form__error" role="alert">{form.error(field.name)}</span>{/if}
|
|
255
|
+
</div>
|
|
256
|
+
{/snippet}
|
|
257
|
+
|
|
258
|
+
{#snippet renderField(field: FormField, cols: number)}
|
|
259
|
+
{#if form.isVisible(field.name)}
|
|
260
|
+
{@const type = field.type ?? 'text'}
|
|
261
|
+
{#if type === 'array'}
|
|
262
|
+
{@render renderArray(field)}
|
|
263
|
+
{:else}
|
|
264
|
+
{@const fieldDisabled = disabled || form.isDisabled(field.name)}
|
|
265
|
+
<div class="sv-form__field" data-field={field.name} class:is-full={(field.full || (field.span ?? 1) >= cols) || cols === 1} class:has-error={form.error(field.name)} style:grid-column={field.span && field.span > 1 && field.span < cols ? `span ${field.span}` : undefined}>
|
|
266
|
+
{#if type !== 'checkbox' && type !== 'switch'}
|
|
267
|
+
<label class="sv-form__label" for={`f-${field.name}`}>
|
|
268
|
+
{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}
|
|
269
|
+
</label>
|
|
270
|
+
{/if}
|
|
271
|
+
{@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, readonly: field.readonly || !!field.computed, invalid: !!form.error(field.name), describedby: form.error(field.name) ? `f-${field.name}__error` : undefined, placeholder: field.placeholder, options: resolveOptions(field), field })}
|
|
272
|
+
{#if form.error(field.name)}
|
|
273
|
+
<span class="sv-form__error" id={`f-${field.name}__error`} role="alert">{form.error(field.name)}</span>
|
|
274
|
+
{:else if form.isValidating(field.name)}
|
|
275
|
+
<span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>{form.messages.checking}</span>
|
|
276
|
+
{:else if field.help}
|
|
277
|
+
<span class="sv-form__help">{field.help}</span>
|
|
278
|
+
{/if}
|
|
279
|
+
</div>
|
|
280
|
+
{/if}
|
|
281
|
+
{/if}
|
|
282
|
+
{/snippet}
|
|
283
|
+
|
|
284
|
+
{#snippet grid(flds: ReadonlyArray<FormField>, cols: number)}
|
|
285
|
+
<div class="sv-form__grid" style:--cols={cols}>
|
|
286
|
+
{#each flds as field (field.name)}{@render renderField(field, cols)}{/each}
|
|
287
|
+
</div>
|
|
288
|
+
{/snippet}
|
|
289
|
+
|
|
290
|
+
{#snippet actions()}
|
|
291
|
+
<div class="sv-form__actions">
|
|
292
|
+
{#if showReset}
|
|
293
|
+
<SvButton type="button" variant="ghost" disabled={disabled || form.submitting} onclick={() => form.reset()}>{resetLabel}</SvButton>
|
|
294
|
+
{/if}
|
|
295
|
+
{#if onCancel}
|
|
296
|
+
<SvButton type="button" variant="secondary" disabled={disabled || form.submitting} onclick={onCancel}>{cancelLabel}</SvButton>
|
|
297
|
+
{/if}
|
|
298
|
+
<SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
|
|
299
|
+
</div>
|
|
300
|
+
{/snippet}
|
|
301
|
+
|
|
302
|
+
<form class="sv-form" bind:this={formEl} dir={resolvedDir} onsubmit={submit} novalidate>
|
|
303
|
+
{#if formError}
|
|
304
|
+
<div class="sv-form__banner" role="alert">{formError}</div>
|
|
305
|
+
{/if}
|
|
306
|
+
{#if errorSummary && summaryItems.length}
|
|
307
|
+
<div class="sv-form__summary" role="alert">
|
|
308
|
+
<p class="sv-form__summary-title">{errorSummaryTitle}</p>
|
|
309
|
+
<ul>
|
|
310
|
+
{#each summaryItems as it (it.name)}
|
|
311
|
+
<li><button type="button" class="sv-form__summary-link" onclick={() => focusField(it.name)}>{it.label}: {it.message}</button></li>
|
|
312
|
+
{/each}
|
|
313
|
+
</ul>
|
|
314
|
+
</div>
|
|
315
|
+
{/if}
|
|
316
|
+
{#if wizard}
|
|
317
|
+
{@const s = sections[Math.min(step, sections.length - 1)]!}
|
|
318
|
+
<SvStepper steps={stepItems} current={step} onChange={(n) => (step = n)} linear />
|
|
319
|
+
<fieldset class="sv-form__section">
|
|
320
|
+
{#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
|
|
321
|
+
{@render grid(s.fields, s.columns ?? columns)}
|
|
322
|
+
</fieldset>
|
|
323
|
+
<div class="sv-form__actions">
|
|
324
|
+
{#if step > 0}
|
|
325
|
+
<SvButton type="button" variant="secondary" disabled={form.submitting} onclick={back}>Back</SvButton>
|
|
326
|
+
{/if}
|
|
327
|
+
<span class="sv-form__spacer"></span>
|
|
328
|
+
{#if step < sections.length - 1}
|
|
329
|
+
<SvButton type="button" disabled={form.submitting} onclick={next}>Next</SvButton>
|
|
330
|
+
{:else}
|
|
331
|
+
{#if showReset}
|
|
332
|
+
<SvButton type="button" variant="ghost" disabled={disabled || form.submitting} onclick={() => { form.reset(); step = 0 }}>{resetLabel}</SvButton>
|
|
333
|
+
{/if}
|
|
334
|
+
<SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
|
|
335
|
+
{/if}
|
|
336
|
+
</div>
|
|
337
|
+
{:else if tabbed}
|
|
338
|
+
<SvTabs tabs={tabItems} bind:value={activeTab} ariaLabel="Form sections">
|
|
339
|
+
{#snippet panel(id)}
|
|
340
|
+
{@const s = sections.find((sec) => sec.section === id)}
|
|
341
|
+
{#if s}
|
|
342
|
+
{#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
|
|
343
|
+
{@render grid(s.fields, s.columns ?? columns)}
|
|
344
|
+
{/if}
|
|
345
|
+
{/snippet}
|
|
346
|
+
</SvTabs>
|
|
347
|
+
{@render actions()}
|
|
348
|
+
{:else if sections.length > 0}
|
|
349
|
+
{#each fields as entry (isFormSection(entry) ? entry.section : entry.name)}
|
|
350
|
+
{#if isFormSection(entry)}
|
|
351
|
+
<fieldset class="sv-form__section">
|
|
352
|
+
<legend class="sv-form__section-title">{entry.section}</legend>
|
|
353
|
+
{#if entry.description}<p class="sv-form__section-desc">{entry.description}</p>{/if}
|
|
354
|
+
{@render grid(entry.fields, entry.columns ?? columns)}
|
|
355
|
+
</fieldset>
|
|
356
|
+
{:else}
|
|
357
|
+
{@render grid([entry], columns)}
|
|
358
|
+
{/if}
|
|
359
|
+
{/each}
|
|
360
|
+
{@render actions()}
|
|
361
|
+
{:else}
|
|
362
|
+
{@render grid(fields as ReadonlyArray<FormField>, columns)}
|
|
363
|
+
{@render actions()}
|
|
364
|
+
{/if}
|
|
365
|
+
</form>
|
|
366
|
+
|
|
367
|
+
<style>
|
|
368
|
+
.sv-form { --_accent: var(--sg-accent, #2563eb); width: 100%; display: flex; flex-direction: column; gap: 16px; }
|
|
369
|
+
.sv-form__grid { display: grid; grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr)); gap: 14px 18px; }
|
|
370
|
+
.sv-form__section { border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; padding: 16px; margin: 0; min-inline-size: 0; }
|
|
371
|
+
.sv-form__section-title { font-size: 13px; font-weight: 700; color: var(--sg-fg, #0f172a); padding: 0 4px; }
|
|
372
|
+
.sv-form__section-desc { margin: 0 0 12px; font-size: 12px; color: var(--sg-muted, #64748b); }
|
|
373
|
+
.sv-form__field { display: flex; flex-direction: column; gap: 5px; }
|
|
374
|
+
.sv-form__field.is-full { grid-column: 1 / -1; }
|
|
375
|
+
.sv-form__label { font-size: 12.5px; font-weight: 600; color: var(--sg-fg, #0f172a); }
|
|
376
|
+
.sv-form__req { color: var(--sg-danger, #dc2626); }
|
|
377
|
+
.sv-form__control {
|
|
378
|
+
width: 100%; box-sizing: border-box; height: 34px; padding: 0 10px; font: inherit; font-size: 13px;
|
|
379
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
380
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); outline: none;
|
|
381
|
+
}
|
|
382
|
+
.sv-form__textarea { height: auto; padding: 8px 10px; resize: vertical; }
|
|
383
|
+
.sv-form__control:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
384
|
+
.sv-form__field.has-error .sv-form__control { border-color: var(--sg-danger, #dc2626); }
|
|
385
|
+
.sv-form__inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
386
|
+
.sv-form__error { font-size: 11.5px; color: var(--sg-danger, #dc2626); }
|
|
387
|
+
.sv-form__help { font-size: 11.5px; color: var(--sg-muted, #64748b); }
|
|
388
|
+
.sv-form__banner {
|
|
389
|
+
padding: 10px 12px; border-radius: 8px; font-size: 13px;
|
|
390
|
+
background: color-mix(in srgb, var(--sg-danger, #dc2626) 10%, transparent);
|
|
391
|
+
color: var(--sg-danger, #dc2626); border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 35%, transparent);
|
|
392
|
+
}
|
|
393
|
+
.sv-form__summary {
|
|
394
|
+
padding: 12px 14px; border-radius: 8px;
|
|
395
|
+
background: color-mix(in srgb, var(--sg-danger, #dc2626) 7%, transparent);
|
|
396
|
+
border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 30%, transparent);
|
|
397
|
+
}
|
|
398
|
+
.sv-form__summary-title { margin: 0 0 6px; font-size: 13px; font-weight: 700; color: var(--sg-danger, #dc2626); }
|
|
399
|
+
.sv-form__summary ul { margin: 0; padding-inline-start: 18px; display: flex; flex-direction: column; gap: 3px; }
|
|
400
|
+
.sv-form__summary-link {
|
|
401
|
+
background: none; border: 0; padding: 0; font: inherit; font-size: 12.5px; text-align: start;
|
|
402
|
+
color: var(--sg-danger, #dc2626); cursor: pointer; text-decoration: underline;
|
|
403
|
+
}
|
|
404
|
+
.sv-form__summary-link:hover { color: var(--sg-fg, #0f172a); }
|
|
405
|
+
.sv-form__checking { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--sg-muted, #64748b); }
|
|
406
|
+
.sv-form__spinner {
|
|
407
|
+
width: 11px; height: 11px; border-radius: 50%;
|
|
408
|
+
border: 2px solid color-mix(in srgb, var(--_accent) 30%, transparent);
|
|
409
|
+
border-top-color: var(--_accent); animation: sv-form-spin 0.6s linear infinite;
|
|
410
|
+
}
|
|
411
|
+
@keyframes sv-form-spin { to { transform: rotate(360deg); } }
|
|
412
|
+
@media (prefers-reduced-motion: reduce) { .sv-form__spinner { animation: none; } }
|
|
413
|
+
.sv-form__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
414
|
+
.sv-form__spacer { flex: 1; }
|
|
415
|
+
.sv-form__array { gap: 8px; }
|
|
416
|
+
.sv-form__arow { display: flex; align-items: flex-start; gap: 8px; }
|
|
417
|
+
.sv-form__arow > .sv-form__grid { flex: 1; padding: 10px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; }
|
|
418
|
+
.sv-form__aremove {
|
|
419
|
+
flex: none; width: 28px; height: 28px; margin-top: 10px; border-radius: 6px; cursor: pointer;
|
|
420
|
+
border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff);
|
|
421
|
+
color: var(--sg-muted, #64748b); font-size: 16px; line-height: 1;
|
|
422
|
+
}
|
|
423
|
+
.sv-form__aremove:hover:not(:disabled) { color: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626); }
|
|
424
|
+
.sv-form__aremove:disabled { opacity: 0.5; cursor: default; }
|
|
425
|
+
.sv-form__arow-tools { display: flex; align-items: center; gap: 4px; margin-top: 10px; }
|
|
426
|
+
.sv-form__amove { display: inline-flex; flex-direction: column; }
|
|
427
|
+
.sv-form__amove-btn {
|
|
428
|
+
width: 22px; height: 14px; display: grid; place-items: center; padding: 0; cursor: pointer;
|
|
429
|
+
border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-muted, #64748b);
|
|
430
|
+
font-size: 10px; line-height: 1;
|
|
431
|
+
}
|
|
432
|
+
.sv-form__amove-btn:first-child { border-radius: 6px 6px 0 0; border-bottom: 0; }
|
|
433
|
+
.sv-form__amove-btn:last-child { border-radius: 0 0 6px 6px; }
|
|
434
|
+
.sv-form__amove-btn:hover:not(:disabled) { color: var(--sg-accent, #2563eb); }
|
|
435
|
+
.sv-form__amove-btn:disabled { opacity: 0.4; cursor: default; }
|
|
436
|
+
.sv-form__arow-tools .sv-form__aremove { margin-top: 0; }
|
|
437
|
+
</style>
|