@happyvertical/smrt-ui 0.51.39 → 0.53.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/AGENTS.md +25 -3
- package/README.md +558 -8
- package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
- package/dist/actions/swipe-dismiss.d.ts +42 -0
- package/dist/actions/swipe-dismiss.d.ts.map +1 -0
- package/dist/actions/swipe-dismiss.js +69 -0
- package/dist/components/calendar/Calendar.svelte +5 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
- package/dist/components/calendar/CalendarView.svelte +1250 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +5 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
- package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
- package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
- package/dist/components/calendar/calendar-model.d.ts +178 -0
- package/dist/components/calendar/calendar-model.d.ts.map +1 -0
- package/dist/components/calendar/calendar-model.js +362 -0
- package/dist/components/calendar/index.d.ts +12 -1
- package/dist/components/calendar/index.d.ts.map +1 -1
- package/dist/components/calendar/index.js +11 -1
- package/dist/components/data/CollectionList.svelte +23 -5
- package/dist/components/data/CollectionList.svelte.d.ts +8 -2
- package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTable.svelte +178 -55
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +4 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +11 -4
- package/dist/components/data/DataTableResponsive.d.ts +4 -0
- package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
- package/dist/components/data/DataTableResponsive.js +22 -0
- package/dist/components/data/ListSortSelect.svelte +121 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
- package/dist/components/data/SortableHeader.svelte +169 -0
- package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
- package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/CollectionList.test.js +37 -0
- package/dist/components/data/__tests__/DataTable.test.js +76 -0
- package/dist/components/data/__tests__/DataTableController.test.js +35 -0
- package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
- package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
- package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
- package/dist/components/data/__tests__/list-sort.test.js +91 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
- package/dist/components/data/data-surface.d.ts +1 -0
- package/dist/components/data/data-surface.d.ts.map +1 -1
- package/dist/components/data/data-surface.js +1 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +6 -0
- package/dist/components/data/link-surface.d.ts +79 -0
- package/dist/components/data/link-surface.d.ts.map +1 -0
- package/dist/components/data/link-surface.js +155 -0
- package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
- package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
- package/dist/components/data/list-sort-state.svelte.js +47 -0
- package/dist/components/data/list-sort.d.ts +94 -0
- package/dist/components/data/list-sort.d.ts.map +1 -0
- package/dist/components/data/list-sort.js +182 -0
- package/dist/components/data/step-surface.d.ts +73 -0
- package/dist/components/data/step-surface.d.ts.map +1 -0
- package/dist/components/data/step-surface.js +177 -0
- package/dist/components/data/surface-navigation.d.ts +49 -0
- package/dist/components/data/surface-navigation.d.ts.map +1 -0
- package/dist/components/data/surface-navigation.js +100 -0
- package/dist/components/data/types.d.ts +27 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/display/DateDisplay.svelte +76 -14
- package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
- package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/Icon.svelte +3 -16
- package/dist/components/display/Icon.svelte.d.ts +0 -9
- package/dist/components/display/Icon.svelte.d.ts.map +1 -1
- package/dist/components/display/StatusBadge.svelte +29 -2
- package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
- package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
- package/dist/components/display/__tests__/Icon.test.js +132 -2
- package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
- package/dist/components/display/icons.svelte.d.ts +11 -0
- package/dist/components/display/icons.svelte.d.ts.map +1 -0
- package/dist/components/display/icons.svelte.js +60 -0
- package/dist/components/display/index.d.ts +3 -1
- package/dist/components/display/index.d.ts.map +1 -1
- package/dist/components/display/index.js +1 -0
- package/dist/components/display/types.d.ts +2 -0
- package/dist/components/display/types.d.ts.map +1 -1
- package/dist/components/feedback/ConfirmDialog.svelte +59 -48
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
- package/dist/components/feedback/PhoneSheet.svelte +224 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
- package/dist/components/feedback/ToastViewport.svelte +156 -8
- package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
- package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
- package/dist/components/feedback/WorkingStrip.svelte +426 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/toast.test.js +142 -1
- package/dist/components/feedback/index.d.ts +5 -1
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/index.js +5 -0
- package/dist/components/feedback/toast.d.ts +13 -1
- package/dist/components/feedback/toast.d.ts.map +1 -1
- package/dist/components/feedback/toast.js +51 -10
- package/dist/components/feedback/working-status.d.ts +34 -0
- package/dist/components/feedback/working-status.d.ts.map +1 -0
- package/dist/components/feedback/working-status.js +5 -0
- package/dist/components/forms/CameraCapture.svelte +619 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
- package/dist/components/forms/Checkbox.svelte +34 -3
- package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +108 -12
- package/dist/components/forms/Combobox.svelte.d.ts +6 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +128 -0
- package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/DictationStatus.svelte +164 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
- package/dist/components/forms/Fieldset.svelte +5 -1
- package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
- package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +5 -0
- package/dist/components/forms/Form.svelte.d.ts +1 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormActionBar.svelte +84 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +17 -1
- package/dist/components/forms/Input.svelte.d.ts +2 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +39 -1
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -1
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/Radio.svelte +16 -4
- package/dist/components/forms/RadioGroup.svelte +7 -1
- package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/SearchInput.svelte +207 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/SegmentedControl.svelte +152 -11
- package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +17 -1
- package/dist/components/forms/Select.svelte.d.ts +2 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/SignaturePad.svelte +436 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
- package/dist/components/forms/StagedControlReview.svelte +21 -12
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
- package/dist/components/forms/Switch.svelte +18 -5
- package/dist/components/forms/Switch.svelte.d.ts +2 -0
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/TagsInput.svelte +8 -1
- package/dist/components/forms/Textarea.svelte +16 -1
- package/dist/components/forms/Textarea.svelte.d.ts +2 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
- package/dist/components/forms/__tests__/Combobox.test.js +354 -0
- package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
- package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
- package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
- package/dist/components/forms/__tests__/Form.test.js +46 -0
- package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
- package/dist/components/forms/__tests__/Listbox.test.js +303 -0
- package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
- package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
- package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
- package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
- package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.js +122 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
- package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
- package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
- package/dist/components/forms/__tests__/dictation.test.js +486 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/long-press.test.js +175 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/touch-density.test.js +66 -0
- package/dist/components/forms/audio-capture.d.ts +69 -0
- package/dist/components/forms/audio-capture.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.js +193 -0
- package/dist/components/forms/camera-capture-session.d.ts +81 -0
- package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
- package/dist/components/forms/camera-capture-session.js +130 -0
- package/dist/components/forms/control-interaction.d.ts +6 -0
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +12 -1
- package/dist/components/forms/control-proposals.d.ts +55 -0
- package/dist/components/forms/control-proposals.d.ts.map +1 -0
- package/dist/components/forms/control-proposals.js +188 -0
- package/dist/components/forms/dictation-transcribe.d.ts +36 -0
- package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
- package/dist/components/forms/dictation-transcribe.js +65 -0
- package/dist/components/forms/dictation.svelte.d.ts +200 -0
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
- package/dist/components/forms/dictation.svelte.js +686 -0
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
- package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
- package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
- package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
- package/dist/components/forms/form-retry/content-digest.js +169 -0
- package/dist/components/forms/form-retry/controller.d.ts +214 -0
- package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
- package/dist/components/forms/form-retry/controller.js +404 -0
- package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
- package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/form-draft.js +161 -0
- package/dist/components/forms/form-retry/index.d.ts +19 -0
- package/dist/components/forms/form-retry/index.d.ts.map +1 -0
- package/dist/components/forms/form-retry/index.js +16 -0
- package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
- package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submission-key.js +342 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submitted-draft.js +270 -0
- package/dist/components/forms/form-retry/types.d.ts +84 -0
- package/dist/components/forms/form-retry/types.d.ts.map +1 -0
- package/dist/components/forms/form-retry/types.js +13 -0
- package/dist/components/forms/index.d.ts +21 -2
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +22 -1
- package/dist/components/forms/insert-text.d.ts +12 -0
- package/dist/components/forms/insert-text.d.ts.map +1 -0
- package/dist/components/forms/insert-text.js +38 -0
- package/dist/components/forms/long-press.d.ts +88 -0
- package/dist/components/forms/long-press.d.ts.map +1 -0
- package/dist/components/forms/long-press.js +213 -0
- package/dist/components/forms/native-file-field.d.ts +94 -0
- package/dist/components/forms/native-file-field.d.ts.map +1 -0
- package/dist/components/forms/native-file-field.js +147 -0
- package/dist/components/forms/ready-beep.d.ts +24 -0
- package/dist/components/forms/ready-beep.d.ts.map +1 -0
- package/dist/components/forms/ready-beep.js +82 -0
- package/dist/components/forms/signature-pad-logic.d.ts +50 -0
- package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
- package/dist/components/forms/signature-pad-logic.js +48 -0
- package/dist/components/forms/types.d.ts +41 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/layout/PageHeader.svelte +151 -17
- package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
- package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/layout/page-header-context.d.ts +25 -0
- package/dist/components/layout/page-header-context.d.ts.map +1 -0
- package/dist/components/layout/page-header-context.js +16 -0
- package/dist/components/nav/FilterChips.svelte +8 -1
- package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
- package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
- package/dist/components/nav/Tabs.svelte +241 -0
- package/dist/components/nav/Tabs.svelte.d.ts +15 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
- package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
- package/dist/components/nav/index.d.ts +1 -0
- package/dist/components/nav/index.d.ts.map +1 -1
- package/dist/components/nav/index.js +1 -0
- package/dist/components/nav/tabs-overflow.d.ts +12 -0
- package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
- package/dist/components/nav/tabs-overflow.js +17 -0
- package/dist/components/nav/types.d.ts +8 -0
- package/dist/components/nav/types.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +16 -2
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -1
- package/dist/components/ui/Card.svelte +14 -5
- package/dist/components/ui/Dropdown.svelte +55 -1
- package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
- package/dist/components/ui/IconToggle.svelte +139 -0
- package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
- package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
- package/dist/components/ui/IconToggleGroup.svelte +56 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +1 -1
- package/dist/components/ui/Popover.svelte +17 -1
- package/dist/components/ui/Popover.svelte.d.ts +8 -0
- package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
- package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
- package/dist/components/ui/__tests__/Popover.test.js +44 -0
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/i18n/strings.ui.d.ts +65 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +72 -0
- package/dist/styles/tokens.css +2 -0
- package/dist/svelte/__tests__/playground.test.js +3 -1
- package/dist/svelte/playground/CalendarPreview.svelte +140 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/CapturePreview.svelte +56 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +20 -0
- package/dist/themes/ThemeProvider.svelte +9 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/themes/css-generator.d.ts.map +1 -1
- package/dist/themes/css-generator.js +3 -0
- package/dist/themes/styles/glass.css +4 -0
- package/dist/themes/styles/happyvertical.css +4 -0
- package/dist/themes/styles/material.css +4 -0
- package/dist/themes/styles/smrt.css +4 -0
- package/dist/themes/styles/studio.css +4 -0
- package/dist/types-generic.d.ts +2 -0
- package/dist/types-generic.d.ts.map +1 -1
- package/package.json +9 -3
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createFormRetry()`: the browser half of `runOnce()` for a SvelteKit
|
|
3
|
+
* enhanced form (#3291).
|
|
4
|
+
*
|
|
5
|
+
* One controller per form. It owns the submission key (`submission-key.ts`),
|
|
6
|
+
* the conditional reset (`form-draft.ts`) and, when asked, the
|
|
7
|
+
* restore-after-reload draft (`submitted-draft.ts`), and exposes them as:
|
|
8
|
+
*
|
|
9
|
+
* - `enhance(submit?)` — a submit function in SvelteKit's `SubmitFunction`
|
|
10
|
+
* shape, for `<form method="POST" use:enhance={retry.enhance()}>`;
|
|
11
|
+
* - `attach` / `action` — an attachment (`{@attach retry.attach}`) or action
|
|
12
|
+
* (`use:retry.action`) that keeps a hidden key field in the form and puts a
|
|
13
|
+
* restored draft back into its controls;
|
|
14
|
+
* - `subscribe` — the Svelte store contract, so `$retry.status` and
|
|
15
|
+
* `$retry.token` are reactive in a component without this module importing
|
|
16
|
+
* Svelte.
|
|
17
|
+
*
|
|
18
|
+
* It is framework-free TypeScript: no `@sveltejs/kit` and no `svelte` import.
|
|
19
|
+
*
|
|
20
|
+
* ## What happens to one submit
|
|
21
|
+
*
|
|
22
|
+
* 1. A file the restored draft says was chosen, but is not chosen now, refuses
|
|
23
|
+
* the submit (`status: 'files-required'`) — a native file input cannot be
|
|
24
|
+
* restored, and sending without it would silently be different content.
|
|
25
|
+
* 2. A submit while another is unresolved is refused (`status: 'busy'`) and
|
|
26
|
+
* nothing is sent; the form keeps what the person typed.
|
|
27
|
+
* 3. Otherwise the key is written into the outgoing `FormData` (so the body is
|
|
28
|
+
* right even if a hidden field is stale or missing) and what the person can
|
|
29
|
+
* see is snapshotted — before the page's own submit function runs, because
|
|
30
|
+
* kit built the body before calling it. With restore enabled, the submitted
|
|
31
|
+
* values are then kept beside the key.
|
|
32
|
+
* 4. The result settles the slot:
|
|
33
|
+
* - `error` (transport failure, lost response, thrown server error): the key,
|
|
34
|
+
* the form and the draft are all kept, and `update()` is NOT called —
|
|
35
|
+
* `enhance`'s default would apply the error and can unmount the form. The
|
|
36
|
+
* unchanged re-submit is the same claim.
|
|
37
|
+
* - `failure` (validation): applied without rotating; the draft is dropped
|
|
38
|
+
* because nothing was recorded.
|
|
39
|
+
* - `success`: the draft is dropped; the form is reset — and the key
|
|
40
|
+
* rotated — only if the fields are unchanged since the submit. A person who
|
|
41
|
+
* typed the next entry while the first was in flight keeps it, under the
|
|
42
|
+
* same key, and sending it is a different claim because its content
|
|
43
|
+
* differs. With the page's own result callback, the key rotates only if
|
|
44
|
+
* the form is actually reset while that callback runs.
|
|
45
|
+
* - `redirect`: a confirmed write only with `redirectConfirmsWrite: true`.
|
|
46
|
+
* - A late result for a submit already settled is ignored entirely.
|
|
47
|
+
*
|
|
48
|
+
* An aborted request (the page called `controller.abort()`, before or during
|
|
49
|
+
* the request — including from its own submit function) produces no result
|
|
50
|
+
* from `enhance`; it settles like a transport error so the form is not stuck
|
|
51
|
+
* refusing submits.
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
import { draftOf, formWasCleared } from './form-draft.js';
|
|
56
|
+
import { beginSubmit, isSubmissionKeyPersistent, readSubmissionKey, rotateSubmissionKey, settleSubmit, storageBase, } from './submission-key.js';
|
|
57
|
+
import { applyDraftToForm, clearSubmittedDraft, DEFAULT_DRAFT_MAX_AGE_MS, describeFormData, isChosenFile, readSubmittedDraft, saveSubmittedDraft, } from './submitted-draft.js';
|
|
58
|
+
const DEFAULT_FIELD = 'submissionKey';
|
|
59
|
+
/** Create the retry controller for one form. */
|
|
60
|
+
export function createFormRetry(options) {
|
|
61
|
+
const fieldName = options.fieldName ?? DEFAULT_FIELD;
|
|
62
|
+
const now = options.now ?? (() => new Date());
|
|
63
|
+
const location = {
|
|
64
|
+
form: options.form,
|
|
65
|
+
scope: options.scope,
|
|
66
|
+
storage: options.storage,
|
|
67
|
+
storageKey: options.storageKey,
|
|
68
|
+
mint: options.mint,
|
|
69
|
+
};
|
|
70
|
+
const restoreOptions = options.restore;
|
|
71
|
+
const draftName = restoreOptions?.storageKey ??
|
|
72
|
+
`${storageBase(options.form, options.scope)}:draft`;
|
|
73
|
+
const exclude = new Set([
|
|
74
|
+
fieldName,
|
|
75
|
+
...(restoreOptions?.exclude ?? []),
|
|
76
|
+
]);
|
|
77
|
+
// Left out of the edit comparison: the key field (it rotates by design)
|
|
78
|
+
// and whatever the page says changes on its own.
|
|
79
|
+
const notEdits = new Set([
|
|
80
|
+
fieldName,
|
|
81
|
+
...(options.ignoreFields ?? []),
|
|
82
|
+
]);
|
|
83
|
+
let slot = {
|
|
84
|
+
token: readSubmissionKey(location),
|
|
85
|
+
inFlight: null,
|
|
86
|
+
};
|
|
87
|
+
let restored = null;
|
|
88
|
+
if (restoreOptions) {
|
|
89
|
+
const maxAge = restoreOptions.maxAgeMs ?? DEFAULT_DRAFT_MAX_AGE_MS;
|
|
90
|
+
const fresh = (savedAt) => {
|
|
91
|
+
const current = now();
|
|
92
|
+
if (restoreOptions.fresh)
|
|
93
|
+
return restoreOptions.fresh(savedAt, current);
|
|
94
|
+
const age = current.getTime() - savedAt.getTime();
|
|
95
|
+
return age >= 0 && age <= maxAge;
|
|
96
|
+
};
|
|
97
|
+
restored = readSubmittedDraft(options.storage, draftName, {
|
|
98
|
+
token: slot.token,
|
|
99
|
+
owner: restoreOptions.owner,
|
|
100
|
+
fresh,
|
|
101
|
+
});
|
|
102
|
+
if (restored && restoreOptions.values && restored.values !== undefined) {
|
|
103
|
+
try {
|
|
104
|
+
restoreOptions.values.restore(restored.values);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// The page rejected the stored value (it is untrusted data): restore
|
|
108
|
+
// nothing rather than half a draft, and never break the page over it.
|
|
109
|
+
clearSubmittedDraft(options.storage, draftName);
|
|
110
|
+
restored = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let appliedRestore = false;
|
|
115
|
+
let state = {
|
|
116
|
+
token: slot.token,
|
|
117
|
+
inFlight: null,
|
|
118
|
+
status: 'idle',
|
|
119
|
+
restored: restored !== null,
|
|
120
|
+
filesToReselect: uniqueFields(restored),
|
|
121
|
+
persistent: isSubmissionKeyPersistent(location),
|
|
122
|
+
};
|
|
123
|
+
const listeners = new Set();
|
|
124
|
+
function setState(patch) {
|
|
125
|
+
state = {
|
|
126
|
+
...state,
|
|
127
|
+
...patch,
|
|
128
|
+
token: slot.token,
|
|
129
|
+
inFlight: slot.inFlight,
|
|
130
|
+
persistent: isSubmissionKeyPersistent(location),
|
|
131
|
+
};
|
|
132
|
+
for (const listener of listeners)
|
|
133
|
+
listener(state);
|
|
134
|
+
}
|
|
135
|
+
function writeSlot(next) {
|
|
136
|
+
slot = next;
|
|
137
|
+
}
|
|
138
|
+
function clearDraft() {
|
|
139
|
+
if (restoreOptions)
|
|
140
|
+
clearSubmittedDraft(options.storage, draftName);
|
|
141
|
+
}
|
|
142
|
+
function missingFiles(formData) {
|
|
143
|
+
return state.filesToReselect.filter((field) => !formData.getAll(field).some(isChosenFile));
|
|
144
|
+
}
|
|
145
|
+
function saveDraft(formData, token, skip) {
|
|
146
|
+
if (!restoreOptions)
|
|
147
|
+
return;
|
|
148
|
+
if (!restoreOptions.owner) {
|
|
149
|
+
// A draft can never be restored without an owner, so keeping one would
|
|
150
|
+
// only leave the person's values on shared hardware for nothing.
|
|
151
|
+
clearDraft();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const described = describeFormData(formData, skip);
|
|
155
|
+
let values;
|
|
156
|
+
if (restoreOptions.values) {
|
|
157
|
+
try {
|
|
158
|
+
values = restoreOptions.values.capture(formData);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
values = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
saveSubmittedDraft(options.storage, draftName, {
|
|
165
|
+
token,
|
|
166
|
+
owner: restoreOptions.owner,
|
|
167
|
+
savedAt: now(),
|
|
168
|
+
fields: described.fields,
|
|
169
|
+
files: described.files,
|
|
170
|
+
values,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function passwordFields(form) {
|
|
174
|
+
const names = [];
|
|
175
|
+
for (const element of Array.from(form.elements)) {
|
|
176
|
+
if (element instanceof HTMLInputElement &&
|
|
177
|
+
element.type === 'password' &&
|
|
178
|
+
element.name) {
|
|
179
|
+
names.push(element.name);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return names;
|
|
183
|
+
}
|
|
184
|
+
function enhance(submit) {
|
|
185
|
+
return async (input) => {
|
|
186
|
+
const { formData, formElement, cancel, controller } = input;
|
|
187
|
+
const missing = missingFiles(formData);
|
|
188
|
+
if (missing.length > 0) {
|
|
189
|
+
cancel();
|
|
190
|
+
setState({ status: 'files-required', filesToReselect: missing });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const begun = beginSubmit(slot);
|
|
194
|
+
if (!begun.start) {
|
|
195
|
+
cancel();
|
|
196
|
+
setState({ status: 'busy' });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
writeSlot(begun.state);
|
|
200
|
+
const sent = begun.sent;
|
|
201
|
+
formData.set(fieldName, sent);
|
|
202
|
+
// What was sent is what the form held when kit built `formData`, which
|
|
203
|
+
// it did before calling this function. Snapshot it now, before awaiting
|
|
204
|
+
// the page's submit function: an edit made while that is pending was
|
|
205
|
+
// never sent, and must read as an edit when the result arrives.
|
|
206
|
+
const submitted = draftOf(formElement, notEdits);
|
|
207
|
+
let innerCancelled = false;
|
|
208
|
+
let innerCallback;
|
|
209
|
+
if (submit) {
|
|
210
|
+
try {
|
|
211
|
+
const returned = await submit({
|
|
212
|
+
...input,
|
|
213
|
+
cancel: () => {
|
|
214
|
+
innerCancelled = true;
|
|
215
|
+
cancel();
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
if (typeof returned === 'function')
|
|
219
|
+
innerCallback = returned;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
// The page's own submit threw before anything was sent: release the
|
|
223
|
+
// slot so the form is not stuck refusing submits.
|
|
224
|
+
cancel();
|
|
225
|
+
writeSlot({ ...slot, inFlight: null });
|
|
226
|
+
setState({ status: 'idle' });
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (innerCancelled) {
|
|
231
|
+
writeSlot({ ...slot, inFlight: null });
|
|
232
|
+
setState({ status: 'idle' });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (restoreOptions) {
|
|
236
|
+
saveDraft(formData, sent, new Set([...exclude, ...passwordFields(formElement)]));
|
|
237
|
+
}
|
|
238
|
+
setState({ status: 'submitting', filesToReselect: [] });
|
|
239
|
+
let settled = false;
|
|
240
|
+
const onAbort = () => {
|
|
241
|
+
if (settled)
|
|
242
|
+
return;
|
|
243
|
+
settled = true;
|
|
244
|
+
const next = settleSubmit(slot, sent, 'error', {
|
|
245
|
+
formCleared: false,
|
|
246
|
+
mint: () => rotateSubmissionKey(location),
|
|
247
|
+
});
|
|
248
|
+
if (next.disposition === 'ignore')
|
|
249
|
+
return;
|
|
250
|
+
writeSlot(next.state);
|
|
251
|
+
setState({ status: 'transport-error' });
|
|
252
|
+
};
|
|
253
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
254
|
+
// The page's own submit function may already have aborted (without
|
|
255
|
+
// cancelling): the abort event has fired, and kit will report no result.
|
|
256
|
+
if (controller.signal.aborted)
|
|
257
|
+
onAbort();
|
|
258
|
+
return async (resultInput) => {
|
|
259
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
260
|
+
if (settled)
|
|
261
|
+
return;
|
|
262
|
+
settled = true;
|
|
263
|
+
const { result } = resultInput;
|
|
264
|
+
const formCleared = formWasCleared(formElement, submitted, notEdits);
|
|
265
|
+
// The default handling resets an unchanged form on success, so the key
|
|
266
|
+
// retires now. A page's own result callback decides the reset itself
|
|
267
|
+
// (`update({ reset: false })`, or no update at all), and values left on
|
|
268
|
+
// screen must keep their key: then the key retires only if the form is
|
|
269
|
+
// actually reset while the callback runs.
|
|
270
|
+
const retireOnReset = innerCallback !== undefined && result.type === 'success';
|
|
271
|
+
const next = settleSubmit(slot, sent, result.type, {
|
|
272
|
+
formCleared: formCleared && !retireOnReset,
|
|
273
|
+
mint: () => rotateSubmissionKey(location),
|
|
274
|
+
redirectConfirmsWrite: options.redirectConfirmsWrite,
|
|
275
|
+
});
|
|
276
|
+
if (next.disposition === 'ignore')
|
|
277
|
+
return;
|
|
278
|
+
writeSlot(next.state);
|
|
279
|
+
if (next.disposition === 'transport-error') {
|
|
280
|
+
// Keep the key, the form and the draft. Do NOT call update(): on an
|
|
281
|
+
// error result it applies the error and can unmount the form.
|
|
282
|
+
setState({ status: 'transport-error' });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (next.disposition === 'rotate-and-apply') {
|
|
286
|
+
clearDraft();
|
|
287
|
+
setState({
|
|
288
|
+
status: result.type === 'redirect' ? 'redirect' : 'success',
|
|
289
|
+
restored: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else if (result.type === 'failure') {
|
|
293
|
+
// Nothing was recorded, so there is nothing to retry after a reload.
|
|
294
|
+
clearDraft();
|
|
295
|
+
setState({ status: 'failure', restored: false });
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
setState({
|
|
299
|
+
status: result.type === 'redirect' ? 'redirect' : 'idle',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const update = (opts) => resultInput.update({ ...opts, reset: opts?.reset ?? formCleared });
|
|
303
|
+
if (!innerCallback) {
|
|
304
|
+
await update();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!(retireOnReset && formCleared)) {
|
|
308
|
+
await innerCallback({ ...resultInput, update });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Watch for the reset like the capture fields do: capture phase on the
|
|
312
|
+
// document, so a page listener that stops propagation cannot hide it.
|
|
313
|
+
const doc = formElement.ownerDocument;
|
|
314
|
+
const seen = { reset: null };
|
|
315
|
+
const onReset = (event) => {
|
|
316
|
+
if (event.target === formElement)
|
|
317
|
+
seen.reset = event;
|
|
318
|
+
};
|
|
319
|
+
doc.addEventListener('reset', onReset, true);
|
|
320
|
+
try {
|
|
321
|
+
await innerCallback({ ...resultInput, update });
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
doc.removeEventListener('reset', onReset, true);
|
|
325
|
+
// A submit started while the callback ran carries the current key:
|
|
326
|
+
// rotating under it would hand its resend a different claim. Its own
|
|
327
|
+
// result decides retirement instead.
|
|
328
|
+
if (seen.reset &&
|
|
329
|
+
!seen.reset.defaultPrevented &&
|
|
330
|
+
slot.inFlight === null) {
|
|
331
|
+
writeSlot({ token: rotateSubmissionKey(location), inFlight: null });
|
|
332
|
+
setState({});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function syncHiddenField(form) {
|
|
339
|
+
let field = Array.from(form.elements).find((element) => element instanceof HTMLInputElement && element.name === fieldName);
|
|
340
|
+
if (!field) {
|
|
341
|
+
field = form.ownerDocument.createElement('input');
|
|
342
|
+
field.type = 'hidden';
|
|
343
|
+
field.name = fieldName;
|
|
344
|
+
form.prepend(field);
|
|
345
|
+
}
|
|
346
|
+
field.value = slot.token;
|
|
347
|
+
return field;
|
|
348
|
+
}
|
|
349
|
+
function attach(form) {
|
|
350
|
+
syncHiddenField(form);
|
|
351
|
+
if (restored && !appliedRestore) {
|
|
352
|
+
appliedRestore = true;
|
|
353
|
+
applyDraftToForm(form, restored, exclude);
|
|
354
|
+
}
|
|
355
|
+
const unsubscribe = subscribe(() => {
|
|
356
|
+
syncHiddenField(form);
|
|
357
|
+
});
|
|
358
|
+
return unsubscribe;
|
|
359
|
+
}
|
|
360
|
+
function subscribe(listener) {
|
|
361
|
+
listeners.add(listener);
|
|
362
|
+
listener(state);
|
|
363
|
+
return () => {
|
|
364
|
+
listeners.delete(listener);
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function discard() {
|
|
368
|
+
if (slot.inFlight !== null)
|
|
369
|
+
return false;
|
|
370
|
+
clearDraft();
|
|
371
|
+
restored = null;
|
|
372
|
+
writeSlot({
|
|
373
|
+
token: rotateSubmissionKey(location),
|
|
374
|
+
inFlight: null,
|
|
375
|
+
});
|
|
376
|
+
setState({ status: 'idle', restored: false, filesToReselect: [] });
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
get token() {
|
|
381
|
+
return slot.token;
|
|
382
|
+
},
|
|
383
|
+
fieldName,
|
|
384
|
+
get state() {
|
|
385
|
+
return state;
|
|
386
|
+
},
|
|
387
|
+
get restored() {
|
|
388
|
+
return restored;
|
|
389
|
+
},
|
|
390
|
+
subscribe,
|
|
391
|
+
enhance,
|
|
392
|
+
attach,
|
|
393
|
+
action: (form) => {
|
|
394
|
+
const cleanup = attach(form);
|
|
395
|
+
return { destroy: cleanup };
|
|
396
|
+
},
|
|
397
|
+
discard,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function uniqueFields(draft) {
|
|
401
|
+
if (!draft)
|
|
402
|
+
return [];
|
|
403
|
+
return Array.from(new Set(draft.files.map((file) => file.field)));
|
|
404
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a form still holds what it submitted, so a successful submit knows
|
|
3
|
+
* if it may clear the fields (#3291).
|
|
4
|
+
*
|
|
5
|
+
* The problem this solves is created by the in-flight refusal in
|
|
6
|
+
* `submission-key.ts`. A submit over a slow link is unresolved for a while;
|
|
7
|
+
* the fields stay editable, so the person can type the NEXT entry into them.
|
|
8
|
+
* They cannot send it yet — that is the point — but when the first submit
|
|
9
|
+
* finally succeeds, `enhance`'s `update()` resets the form by default and the
|
|
10
|
+
* typed draft is gone. Nothing downstream can recover it: it never reached the
|
|
11
|
+
* server, so no claim has ever seen it.
|
|
12
|
+
*
|
|
13
|
+
* So the reset becomes conditional. If the fields still hold exactly what was
|
|
14
|
+
* submitted, clearing them is right — the ordinary case, and what makes the
|
|
15
|
+
* next entry a fresh form with a fresh key. If they do not, the person has
|
|
16
|
+
* moved on and their work is kept.
|
|
17
|
+
*
|
|
18
|
+
* Every control that posts a value is compared, hidden inputs included: a
|
|
19
|
+
* `Listbox`, `Combobox` or `MultiSelect` posts its selection through hidden
|
|
20
|
+
* inputs, and a selection changed while a submit is in flight is an edit the
|
|
21
|
+
* server has never seen. The ONE exception is the retry helper's own key field
|
|
22
|
+
* (and any name the caller adds to `ignore`): a confirmed write rotates it, so
|
|
23
|
+
* comparing it would make every form look edited and nothing would ever
|
|
24
|
+
* reset. A page-managed hidden field that changes after a submit (a rewritten
|
|
25
|
+
* nonce) therefore keeps the form — the safe direction — unless it is ignored.
|
|
26
|
+
*
|
|
27
|
+
* Files are compared as the browser would post them. A control can contribute
|
|
28
|
+
* a file no element shows: a capture field on the `formdata`-event fallback
|
|
29
|
+
* appends its file while the entry list is built and keeps an empty, unnamed
|
|
30
|
+
* input. So the draft also records every `File` entry of `new FormData(form)`
|
|
31
|
+
* (which fires `formdata`, as a real submit does) by name, type, size and
|
|
32
|
+
* modification time; a capture replaced mid-submit then reads as an edit.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
/** The values a person can see and change, keyed by position and name. */
|
|
37
|
+
export type FormDraft = ReadonlyMap<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* What `form` would post at this moment, as far as an edit can change it:
|
|
40
|
+
* every value-carrying control except the names in `ignore` (by default the
|
|
41
|
+
* `submissionKey` key field).
|
|
42
|
+
*/
|
|
43
|
+
export declare function draftOf(form: HTMLFormElement, ignore?: ReadonlySet<string>): FormDraft;
|
|
44
|
+
/**
|
|
45
|
+
* Has `form` been edited since `submitted` was taken? `true` means the person
|
|
46
|
+
* has typed something the server has never seen, so a reset would destroy it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function draftChanged(form: HTMLFormElement, submitted: FormDraft, ignore?: ReadonlySet<string>): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Did this submit leave the form EMPTY — the one question key retirement turns
|
|
51
|
+
* on (`settleSubmit()`)? Both of these must be no:
|
|
52
|
+
*
|
|
53
|
+
* - the person edited the fields while the submit was unresolved, so a reset
|
|
54
|
+
* would destroy work the server has never seen;
|
|
55
|
+
* - **the submitted form is no longer in the document.** A form remounted
|
|
56
|
+
* under a `{#key}` block, or unmounted when its panel closes, leaves a
|
|
57
|
+
* detached node that still holds exactly what it submitted, so an
|
|
58
|
+
* unchanged-check alone would retire the key — while the VISIBLE
|
|
59
|
+
* replacement may hold the same content, one tap from a second record under
|
|
60
|
+
* a fresh claim.
|
|
61
|
+
*
|
|
62
|
+
* A detached form therefore never counts as cleared. That is the safe
|
|
63
|
+
* direction: keeping a key can only make a later submit of the same content
|
|
64
|
+
* collapse onto the row that already exists, while retiring one wrongly is
|
|
65
|
+
* what records a duplicate.
|
|
66
|
+
*/
|
|
67
|
+
export declare function formWasCleared(form: HTMLFormElement, submitted: FormDraft, ignore?: ReadonlySet<string>): boolean;
|
|
68
|
+
//# sourceMappingURL=form-draft.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-draft.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-retry/form-draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAsDpD;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,WAAW,CAAC,MAAM,CAAmB,GAC5C,SAAS,CAeX;AAgCD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,SAAS,EACpB,MAAM,GAAE,WAAW,CAAC,MAAM,CAAmB,GAC5C,OAAO,CAOT;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,SAAS,EACpB,MAAM,GAAE,WAAW,CAAC,MAAM,CAAmB,GAC5C,OAAO,CAGT"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a form still holds what it submitted, so a successful submit knows
|
|
3
|
+
* if it may clear the fields (#3291).
|
|
4
|
+
*
|
|
5
|
+
* The problem this solves is created by the in-flight refusal in
|
|
6
|
+
* `submission-key.ts`. A submit over a slow link is unresolved for a while;
|
|
7
|
+
* the fields stay editable, so the person can type the NEXT entry into them.
|
|
8
|
+
* They cannot send it yet — that is the point — but when the first submit
|
|
9
|
+
* finally succeeds, `enhance`'s `update()` resets the form by default and the
|
|
10
|
+
* typed draft is gone. Nothing downstream can recover it: it never reached the
|
|
11
|
+
* server, so no claim has ever seen it.
|
|
12
|
+
*
|
|
13
|
+
* So the reset becomes conditional. If the fields still hold exactly what was
|
|
14
|
+
* submitted, clearing them is right — the ordinary case, and what makes the
|
|
15
|
+
* next entry a fresh form with a fresh key. If they do not, the person has
|
|
16
|
+
* moved on and their work is kept.
|
|
17
|
+
*
|
|
18
|
+
* Every control that posts a value is compared, hidden inputs included: a
|
|
19
|
+
* `Listbox`, `Combobox` or `MultiSelect` posts its selection through hidden
|
|
20
|
+
* inputs, and a selection changed while a submit is in flight is an edit the
|
|
21
|
+
* server has never seen. The ONE exception is the retry helper's own key field
|
|
22
|
+
* (and any name the caller adds to `ignore`): a confirmed write rotates it, so
|
|
23
|
+
* comparing it would make every form look edited and nothing would ever
|
|
24
|
+
* reset. A page-managed hidden field that changes after a submit (a rewritten
|
|
25
|
+
* nonce) therefore keeps the form — the safe direction — unless it is ignored.
|
|
26
|
+
*
|
|
27
|
+
* Files are compared as the browser would post them. A control can contribute
|
|
28
|
+
* a file no element shows: a capture field on the `formdata`-event fallback
|
|
29
|
+
* appends its file while the entry list is built and keeps an empty, unnamed
|
|
30
|
+
* input. So the draft also records every `File` entry of `new FormData(form)`
|
|
31
|
+
* (which fires `formdata`, as a real submit does) by name, type, size and
|
|
32
|
+
* modification time; a capture replaced mid-submit then reads as an edit.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The names left out of the comparison by default: the controller's default
|
|
38
|
+
* key field. `createFormRetry()` passes its configured field name (plus
|
|
39
|
+
* `ignoreFields`) instead.
|
|
40
|
+
*/
|
|
41
|
+
const DEFAULT_IGNORED = new Set(['submissionKey']);
|
|
42
|
+
function isEditableControl(element) {
|
|
43
|
+
if (!(element instanceof HTMLInputElement) &&
|
|
44
|
+
!(element instanceof HTMLTextAreaElement) &&
|
|
45
|
+
!(element instanceof HTMLSelectElement)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (element instanceof HTMLInputElement) {
|
|
49
|
+
const type = element.type;
|
|
50
|
+
if (type === 'submit' ||
|
|
51
|
+
type === 'button' ||
|
|
52
|
+
type === 'reset' ||
|
|
53
|
+
type === 'image') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function controlValue(element) {
|
|
60
|
+
if (element instanceof HTMLInputElement) {
|
|
61
|
+
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
62
|
+
return element.checked ? 'on' : '';
|
|
63
|
+
}
|
|
64
|
+
if (element.type === 'file') {
|
|
65
|
+
// A file input cannot be restored from script, so the chosen files'
|
|
66
|
+
// identity (name, size, type) is all that can honestly be compared.
|
|
67
|
+
return Array.from(element.files ?? [], (file) => `${file.name}\u0000${file.size}\u0000${file.type}`).join('\u0001');
|
|
68
|
+
}
|
|
69
|
+
return element.value;
|
|
70
|
+
}
|
|
71
|
+
if (element instanceof HTMLSelectElement && element.multiple) {
|
|
72
|
+
return Array.from(element.selectedOptions, (option) => option.value).join('\u0001');
|
|
73
|
+
}
|
|
74
|
+
return element.value;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* What `form` would post at this moment, as far as an edit can change it:
|
|
78
|
+
* every value-carrying control except the names in `ignore` (by default the
|
|
79
|
+
* `submissionKey` key field).
|
|
80
|
+
*/
|
|
81
|
+
export function draftOf(form, ignore = DEFAULT_IGNORED) {
|
|
82
|
+
const draft = new Map();
|
|
83
|
+
let index = 0;
|
|
84
|
+
for (const element of Array.from(form.elements)) {
|
|
85
|
+
if (!isEditableControl(element))
|
|
86
|
+
continue;
|
|
87
|
+
const controlName = element.name;
|
|
88
|
+
if (controlName && ignore.has(controlName))
|
|
89
|
+
continue;
|
|
90
|
+
const name = controlName || `#${index}`;
|
|
91
|
+
// Several controls can share a name (a radio group, repeated fields); the
|
|
92
|
+
// index keeps them distinct so a change in any one of them is visible.
|
|
93
|
+
draft.set(`${index}:${name}`, controlValue(element));
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
draft.set('files', postedFiles(form));
|
|
97
|
+
return draft;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The identity of every file `form` would post, in entry order. Bytes are
|
|
101
|
+
* never read. Empty when the entry list cannot be built here (no `FormData`,
|
|
102
|
+
* or a `formdata` listener is already building one for this form).
|
|
103
|
+
*/
|
|
104
|
+
function postedFiles(form) {
|
|
105
|
+
if (typeof FormData === 'undefined' || typeof File === 'undefined') {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = new FormData(form);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
const files = [];
|
|
116
|
+
entries.forEach((value, name) => {
|
|
117
|
+
// Skip the "no file chosen" entry (empty name, no bytes): it is minted
|
|
118
|
+
// afresh, with a new modification time, every time the list is built.
|
|
119
|
+
if (value instanceof File && (value.name !== '' || value.size > 0)) {
|
|
120
|
+
files.push([name, value.name, value.type, value.size, value.lastModified].join('\u0000'));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return files.join('\u0001');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Has `form` been edited since `submitted` was taken? `true` means the person
|
|
127
|
+
* has typed something the server has never seen, so a reset would destroy it.
|
|
128
|
+
*/
|
|
129
|
+
export function draftChanged(form, submitted, ignore = DEFAULT_IGNORED) {
|
|
130
|
+
const now = draftOf(form, ignore);
|
|
131
|
+
if (now.size !== submitted.size)
|
|
132
|
+
return true;
|
|
133
|
+
for (const [key, value] of now) {
|
|
134
|
+
if (submitted.get(key) !== value)
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Did this submit leave the form EMPTY — the one question key retirement turns
|
|
141
|
+
* on (`settleSubmit()`)? Both of these must be no:
|
|
142
|
+
*
|
|
143
|
+
* - the person edited the fields while the submit was unresolved, so a reset
|
|
144
|
+
* would destroy work the server has never seen;
|
|
145
|
+
* - **the submitted form is no longer in the document.** A form remounted
|
|
146
|
+
* under a `{#key}` block, or unmounted when its panel closes, leaves a
|
|
147
|
+
* detached node that still holds exactly what it submitted, so an
|
|
148
|
+
* unchanged-check alone would retire the key — while the VISIBLE
|
|
149
|
+
* replacement may hold the same content, one tap from a second record under
|
|
150
|
+
* a fresh claim.
|
|
151
|
+
*
|
|
152
|
+
* A detached form therefore never counts as cleared. That is the safe
|
|
153
|
+
* direction: keeping a key can only make a later submit of the same content
|
|
154
|
+
* collapse onto the row that already exists, while retiring one wrongly is
|
|
155
|
+
* what records a duplicate.
|
|
156
|
+
*/
|
|
157
|
+
export function formWasCleared(form, submitted, ignore = DEFAULT_IGNORED) {
|
|
158
|
+
if (!form.isConnected)
|
|
159
|
+
return false;
|
|
160
|
+
return !draftChanged(form, submitted, ignore);
|
|
161
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form retry — subpath `@happyvertical/smrt-ui/form-retry` (#3291).
|
|
3
|
+
*
|
|
4
|
+
* The browser half of `runOnce()` (`@happyvertical/smrt-core`): a per-tab,
|
|
5
|
+
* per-form submission key, retained values across a failed submit, a
|
|
6
|
+
* conditional reset that keeps the next entry typed while the first was in
|
|
7
|
+
* flight, and opt-in restore-after-reload. Framework-free and Svelte-free, so
|
|
8
|
+
* this entry imports under plain Node and in any bundler; the same exports are
|
|
9
|
+
* also re-exported from `@happyvertical/smrt-ui/forms`, beside `Form`.
|
|
10
|
+
*
|
|
11
|
+
* Guide: docs/content/form-retry.md.
|
|
12
|
+
*/
|
|
13
|
+
export { digestSubmissionContent } from './content-digest.js';
|
|
14
|
+
export { createFormRetry, type FormRetry, type FormRetryOptions, type FormRetryRestoreOptions, type FormRetryState, type FormRetryStatus, type FormRetryValuesHook, } from './controller.js';
|
|
15
|
+
export { draftChanged, draftOf, type FormDraft, formWasCleared, } from './form-draft.js';
|
|
16
|
+
export { beginSubmit, clearSubmissionKey, type FormRetryStorage, mintSubmissionKey, readSubmissionKey, rotateSubmissionKey, type SubmissionKeyLocation, type SubmissionSlot, type SubmitDisposition, type SubmitOutcome, settleSubmit, submitDisposition, } from './submission-key.js';
|
|
17
|
+
export type { SubmittedDraft, SubmittedFileInfo, } from './submitted-draft.js';
|
|
18
|
+
export type { FormRetryActionResult, FormRetryResultCallback, FormRetryResultInput, FormRetrySubmitFunction, FormRetrySubmitInput, FormRetryUpdateOptions, MaybePromise, } from './types.js';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-retry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,YAAY,EACZ,OAAO,EACP,KAAK,SAAS,EACd,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,YAAY,EACZ,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,cAAc,EACd,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,YAAY,GACb,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form retry — subpath `@happyvertical/smrt-ui/form-retry` (#3291).
|
|
3
|
+
*
|
|
4
|
+
* The browser half of `runOnce()` (`@happyvertical/smrt-core`): a per-tab,
|
|
5
|
+
* per-form submission key, retained values across a failed submit, a
|
|
6
|
+
* conditional reset that keeps the next entry typed while the first was in
|
|
7
|
+
* flight, and opt-in restore-after-reload. Framework-free and Svelte-free, so
|
|
8
|
+
* this entry imports under plain Node and in any bundler; the same exports are
|
|
9
|
+
* also re-exported from `@happyvertical/smrt-ui/forms`, beside `Form`.
|
|
10
|
+
*
|
|
11
|
+
* Guide: docs/content/form-retry.md.
|
|
12
|
+
*/
|
|
13
|
+
export { digestSubmissionContent } from './content-digest.js';
|
|
14
|
+
export { createFormRetry, } from './controller.js';
|
|
15
|
+
export { draftChanged, draftOf, formWasCleared, } from './form-draft.js';
|
|
16
|
+
export { beginSubmit, clearSubmissionKey, mintSubmissionKey, readSubmissionKey, rotateSubmissionKey, settleSubmit, submitDisposition, } from './submission-key.js';
|