@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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-form submission key: the browser half of `runOnce()` (#3291).
|
|
3
|
+
*
|
|
4
|
+
* `runOnce()` (`@happyvertical/smrt-core`, #3080) makes a write exactly-once
|
|
5
|
+
* when a retry carries the same claim. The claim is derived from the session's
|
|
6
|
+
* tenant and actor, this key, and a digest of the submitted content, so two
|
|
7
|
+
* submits carrying the same key and the same content record one row, and a
|
|
8
|
+
* submit carrying the same key with DIFFERENT content is a new submission
|
|
9
|
+
* (#3136).
|
|
10
|
+
*
|
|
11
|
+
* ## Why the key outlives the component
|
|
12
|
+
*
|
|
13
|
+
* The failure the key exists for is a lost response, and `enhance`'s error
|
|
14
|
+
* path can unmount the form. A key held only in component state would be
|
|
15
|
+
* re-minted on the way back and the retry would record a duplicate — the whole
|
|
16
|
+
* bug. So the key lives in storage: per tab (`sessionStorage`), per form (one
|
|
17
|
+
* slot per form, optionally scoped per record), cleared as soon as a submit is
|
|
18
|
+
* confirmed recorded, and gone when the tab closes.
|
|
19
|
+
*
|
|
20
|
+
* It is never `localStorage`: a tablet or kiosk is shared hardware, and one
|
|
21
|
+
* person's key must not still be sitting there for the next. (A key that does
|
|
22
|
+
* outlive one person's visit is harmless — the claim also covers actor and
|
|
23
|
+
* content — but there is no reason to keep it.)
|
|
24
|
+
*
|
|
25
|
+
* ## Every storage access is wrapped
|
|
26
|
+
*
|
|
27
|
+
* `sessionStorage` throws in private windows, with site data blocked, and in
|
|
28
|
+
* some embedded webviews; even reading the `sessionStorage` property can throw
|
|
29
|
+
* a `SecurityError`. Every access is guarded, and a refused write falls back
|
|
30
|
+
* to an in-memory store, which is read before the backing store so a value
|
|
31
|
+
* the store could not replace or remove is never handed back (a refused
|
|
32
|
+
* removal overwrites the value with a cleared marker; see `guardedRemove`).
|
|
33
|
+
* In the browser that store is module-level, so the key still survives a
|
|
34
|
+
* remount for the life of the page — only a full reload loses it, which is
|
|
35
|
+
* less idempotent, never wrong. During SSR there is no shared
|
|
36
|
+
* store at all — neither the module-level memory nor the default storage (Node
|
|
37
|
+
* 25+ has a process-wide `sessionStorage` global): either would hand one
|
|
38
|
+
* request's key to another, so each read off the browser mints a fresh,
|
|
39
|
+
* unshared key.
|
|
40
|
+
*
|
|
41
|
+
* ## One unresolved submit per key
|
|
42
|
+
*
|
|
43
|
+
* One key per form is only meaningful while at most one submission is
|
|
44
|
+
* unresolved under it. Without that, a person can submit A, change the form,
|
|
45
|
+
* and submit B before A answers: both carry key K, A's answer rotates the slot
|
|
46
|
+
* to K2, and B — a genuinely different write — is left holding a key nothing
|
|
47
|
+
* will reconcile; if B commits and loses its response, its retry carries K2,
|
|
48
|
+
* takes a different claim, and records a second row. So {@link beginSubmit}
|
|
49
|
+
* refuses a second submit while one is unresolved. That cancels the
|
|
50
|
+
* submission itself rather than hoping a disabled attribute was flushed in
|
|
51
|
+
* time, and it holds for a second DIFFERENT submission, not just a double tap.
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
/** The storage operations the helper uses: a subset of the DOM `Storage`. */
|
|
56
|
+
export interface FormRetryStorage {
|
|
57
|
+
/** Read a value, or `null` when nothing is stored. May throw. */
|
|
58
|
+
getItem(key: string): string | null;
|
|
59
|
+
/** Store a value. May throw (private window, quota). */
|
|
60
|
+
setItem(key: string, value: string): void;
|
|
61
|
+
/** Remove a value. May throw. */
|
|
62
|
+
removeItem(key: string): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Where a form's key and submitted draft live.
|
|
66
|
+
*
|
|
67
|
+
* `storage` is the backing store (default: this tab's `sessionStorage`,
|
|
68
|
+
* resolved lazily on every access because even reading the property can
|
|
69
|
+
* throw); pass `null` to keep everything in memory.
|
|
70
|
+
*/
|
|
71
|
+
export interface SubmissionKeyLocation {
|
|
72
|
+
/** One slot per FORM, not per page: two forms on one page hold two keys. */
|
|
73
|
+
form: string;
|
|
74
|
+
/**
|
|
75
|
+
* One key per record within a form kind (a form rendered once per row).
|
|
76
|
+
* One record's confirmed write must never rotate the key another record's
|
|
77
|
+
* unresolved retry still carries.
|
|
78
|
+
*/
|
|
79
|
+
scope?: string;
|
|
80
|
+
/** Backing store. Defaults to `sessionStorage`; `null` means memory only. */
|
|
81
|
+
storage?: FormRetryStorage | null;
|
|
82
|
+
/**
|
|
83
|
+
* Exact storage name for the key, overriding the derived
|
|
84
|
+
* `smrt:form-retry:<form>[:<scope>]:key`. For migrating an existing app
|
|
85
|
+
* whose tablets hold keys under an older name: renaming silently loses
|
|
86
|
+
* every unresolved key, and the next retry records a duplicate.
|
|
87
|
+
*/
|
|
88
|
+
storageKey?: string;
|
|
89
|
+
/** Mints a new key. Defaults to {@link mintSubmissionKey} (UUID v4). */
|
|
90
|
+
mint?: () => string;
|
|
91
|
+
}
|
|
92
|
+
/** Test seam: forget every in-memory key and draft. */
|
|
93
|
+
export declare function resetFormRetryMemory(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Read `key` from memory, then from `storage`. Memory holds a value only when
|
|
96
|
+
* its storage write was refused (a successful write and every removal drop the
|
|
97
|
+
* memory copy), so it is the newer of the two: a store that refused to replace
|
|
98
|
+
* or remove an old value must not hand that stale value back. A cleared value
|
|
99
|
+
* ({@link guardedRemove}) reads as `null` from either place, and a cleared
|
|
100
|
+
* memory entry hides whatever the store still holds.
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
export declare function guardedGet(storage: FormRetryStorage | null | undefined, key: string): string | null;
|
|
104
|
+
/**
|
|
105
|
+
* Write `key`, falling back to memory when storage refuses. Returns whether the
|
|
106
|
+
* value reached the backing store (it survives a reload).
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export declare function guardedSet(storage: FormRetryStorage | null | undefined, key: string, value: string): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Remove `key` from storage and memory alike, so no later read returns it.
|
|
112
|
+
*
|
|
113
|
+
* A store may refuse `removeItem()` while reads still succeed. The old value
|
|
114
|
+
* is then overwritten with a cleared marker, which survives a reload of the
|
|
115
|
+
* tab like any stored value. If that write is refused too, the marker is kept
|
|
116
|
+
* in memory, which reads consult first: the old value is never read back for
|
|
117
|
+
* the life of the page. A store that refuses both cannot be changed at all,
|
|
118
|
+
* so after a full reload it still holds the old value; nothing in the page
|
|
119
|
+
* can prevent that.
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
export declare function guardedRemove(storage: FormRetryStorage | null | undefined, key: string): void;
|
|
123
|
+
/** The derived storage base for a form (and scope). @internal */
|
|
124
|
+
export declare function storageBase(form: string, scope?: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* A random v4 UUID from `crypto.getRandomValues` — unguessable, and accepted
|
|
127
|
+
* by a server-side UUID parser.
|
|
128
|
+
*/
|
|
129
|
+
export declare function mintSubmissionKey(): string;
|
|
130
|
+
/**
|
|
131
|
+
* This tab's key for a form, minting and storing one on first use. The same
|
|
132
|
+
* value comes back across a remount or a reload until it is cleared, so every
|
|
133
|
+
* retry of one filled-in form carries one key.
|
|
134
|
+
*/
|
|
135
|
+
export declare function readSubmissionKey(location: SubmissionKeyLocation): string;
|
|
136
|
+
/**
|
|
137
|
+
* Whether the form's key currently lives in the backing store (and so
|
|
138
|
+
* survives a reload), rather than only in memory.
|
|
139
|
+
*/
|
|
140
|
+
export declare function isSubmissionKeyPersistent(location: SubmissionKeyLocation): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Drop the key once its submit is confirmed recorded, so the next fill of the
|
|
143
|
+
* form starts a claim of its own. This is the boundary between a resubmission
|
|
144
|
+
* and a deliberate repeat: adding the same line twice on purpose is a new fill
|
|
145
|
+
* and takes a new key; a double tap, a retry after a lost response, and two
|
|
146
|
+
* concurrent requests carry the key the form still holds.
|
|
147
|
+
*
|
|
148
|
+
* Call it only on a result the server confirmed recorded — never on a
|
|
149
|
+
* validation failure, whose submit wrote nothing and whose key is still free.
|
|
150
|
+
*/
|
|
151
|
+
export declare function clearSubmissionKey(location: SubmissionKeyLocation): void;
|
|
152
|
+
/**
|
|
153
|
+
* Clear the key and mint the next one, returning it — for an inline form that
|
|
154
|
+
* stays on screen after a confirmed write.
|
|
155
|
+
*
|
|
156
|
+
* The fresh key is written over the old one directly rather than read back:
|
|
157
|
+
* a store may refuse `removeItem()` while reads still succeed, and reading
|
|
158
|
+
* after a failed clear would return the retired key. If the replacement write
|
|
159
|
+
* is refused too, the fresh key lives in memory, which {@link guardedGet}
|
|
160
|
+
* consults before the store.
|
|
161
|
+
*/
|
|
162
|
+
export declare function rotateSubmissionKey(location: SubmissionKeyLocation): string;
|
|
163
|
+
/**
|
|
164
|
+
* One form's submission state: the key the next submit carries, and the key of
|
|
165
|
+
* the submit that has not answered yet (`null` when idle).
|
|
166
|
+
*/
|
|
167
|
+
export interface SubmissionSlot {
|
|
168
|
+
/** The key the next submit carries. */
|
|
169
|
+
token: string;
|
|
170
|
+
/** The key of the unresolved submit, or `null` when the form is idle. */
|
|
171
|
+
inFlight: string | null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Start a submit, or refuse it because one is still unresolved.
|
|
175
|
+
*
|
|
176
|
+
* `start: false` means the caller must `cancel()` the submission: nothing has
|
|
177
|
+
* been sent and the form keeps its content. A double tap lands here too — the
|
|
178
|
+
* second activation is cancelled in the browser, which costs nothing, because
|
|
179
|
+
* the server-side claim still covers every request the browser or the network
|
|
180
|
+
* retries on its own.
|
|
181
|
+
*/
|
|
182
|
+
export declare function beginSubmit(state: SubmissionSlot): {
|
|
183
|
+
state: SubmissionSlot;
|
|
184
|
+
start: boolean;
|
|
185
|
+
sent: string;
|
|
186
|
+
};
|
|
187
|
+
/** What a result means for the form. */
|
|
188
|
+
export type SubmitDisposition = 'ignore' | 'transport-error' | 'rotate-and-apply' | 'apply';
|
|
189
|
+
/** The outcome kinds a result can have (SvelteKit's `ActionResult['type']`). */
|
|
190
|
+
export type SubmitOutcome = 'success' | 'failure' | 'redirect' | 'error';
|
|
191
|
+
/**
|
|
192
|
+
* The rotation policy as one pure function.
|
|
193
|
+
*
|
|
194
|
+
* `sent` is the key the submit carried; `live` is the key of the slot's
|
|
195
|
+
* unresolved submit, or `null` when it has none left to settle.
|
|
196
|
+
*
|
|
197
|
+
* - **A stale result is ignored outright.** A double tap can send two requests
|
|
198
|
+
* under one key, so two results come back, and by the time the second lands
|
|
199
|
+
* the first has already settled the slot. Acting on it would reset the form
|
|
200
|
+
* and rotate AGAIN, out from under a submission started since.
|
|
201
|
+
* - **A transport error keeps the key and the form.** It wrote nothing that
|
|
202
|
+
* the browser can know about, and the claim covers the content, so the
|
|
203
|
+
* unchanged re-submit must be able to reproduce it.
|
|
204
|
+
* - **A confirmed write applies**, and retires the key only if the form is
|
|
205
|
+
* cleared by that apply — see {@link settleSubmit}.
|
|
206
|
+
* - **A validation failure applies without rotating**: nothing was written, so
|
|
207
|
+
* the key is still free and the corrected re-submit keeps the same key.
|
|
208
|
+
* - **A redirect** is a confirmed write only when the caller says so
|
|
209
|
+
* (`redirectConfirmsWrite`); otherwise it is treated like a validation
|
|
210
|
+
* result, because it cannot vouch for a write (a redirect to a sign-in page
|
|
211
|
+
* wrote nothing).
|
|
212
|
+
*/
|
|
213
|
+
export declare function submitDisposition(sent: string, live: string | null, outcome: SubmitOutcome, options?: {
|
|
214
|
+
redirectConfirmsWrite?: boolean;
|
|
215
|
+
}): SubmitDisposition;
|
|
216
|
+
/**
|
|
217
|
+
* Settle one result against the slot, returning the new slot and what to do.
|
|
218
|
+
*
|
|
219
|
+
* ## A key is retired when its FORM IS CLEARED, not merely on success
|
|
220
|
+
*
|
|
221
|
+
* Rotating on success alone leaves a window where the form still SHOWS
|
|
222
|
+
* content that is already recorded while holding a fresh key — after an edit
|
|
223
|
+
* the conditional reset preserved, or after a remount put the submitted values
|
|
224
|
+
* back. A further submit from that form would then take a new claim and record
|
|
225
|
+
* a genuine second row from content nobody re-entered. Keeping the key instead
|
|
226
|
+
* is right, and safe because the claim covers the CONTENT too:
|
|
227
|
+
*
|
|
228
|
+
* - the form still shows what was recorded, so re-submitting it derives the
|
|
229
|
+
* SAME claim and collapses onto the row that already exists;
|
|
230
|
+
* - the form shows something genuinely different, so it derives a DIFFERENT
|
|
231
|
+
* claim and is recorded, which is what it should be.
|
|
232
|
+
*
|
|
233
|
+
* Only an emptied form has nothing left to reconcile, so only then does the
|
|
234
|
+
* next entry deserve a key of its own.
|
|
235
|
+
*/
|
|
236
|
+
export declare function settleSubmit(state: SubmissionSlot, sent: string, outcome: SubmitOutcome, options: {
|
|
237
|
+
formCleared: boolean;
|
|
238
|
+
mint: () => string;
|
|
239
|
+
redirectConfirmsWrite?: boolean;
|
|
240
|
+
}): {
|
|
241
|
+
state: SubmissionSlot;
|
|
242
|
+
disposition: SubmitDisposition;
|
|
243
|
+
};
|
|
244
|
+
//# sourceMappingURL=submission-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submission-key.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/form-retry/submission-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,wDAAwD;IACxD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,iCAAiC;IACjC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAClC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,MAAM,CAAC;CACrB;AAmBD,uDAAuD;AACvD,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAgCD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC5C,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,CAaf;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC5C,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GACZ,OAAO,CAaT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC5C,GAAG,EAAE,MAAM,GACV,IAAI,CAkBN;AAED,iEAAiE;AACjE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGhE;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAS1C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAOzE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAYT;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAExE;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAK3E;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG;IAClD,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CASA;AAED,wCAAwC;AACxC,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,iBAAiB,GACjB,kBAAkB,GAClB,OAAO,CAAC;AAEZ,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE;IAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAO,GAChD,iBAAiB,CAQnB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE;IACP,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,GACA;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,WAAW,EAAE,iBAAiB,CAAA;CAAE,CAS3D"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-form submission key: the browser half of `runOnce()` (#3291).
|
|
3
|
+
*
|
|
4
|
+
* `runOnce()` (`@happyvertical/smrt-core`, #3080) makes a write exactly-once
|
|
5
|
+
* when a retry carries the same claim. The claim is derived from the session's
|
|
6
|
+
* tenant and actor, this key, and a digest of the submitted content, so two
|
|
7
|
+
* submits carrying the same key and the same content record one row, and a
|
|
8
|
+
* submit carrying the same key with DIFFERENT content is a new submission
|
|
9
|
+
* (#3136).
|
|
10
|
+
*
|
|
11
|
+
* ## Why the key outlives the component
|
|
12
|
+
*
|
|
13
|
+
* The failure the key exists for is a lost response, and `enhance`'s error
|
|
14
|
+
* path can unmount the form. A key held only in component state would be
|
|
15
|
+
* re-minted on the way back and the retry would record a duplicate — the whole
|
|
16
|
+
* bug. So the key lives in storage: per tab (`sessionStorage`), per form (one
|
|
17
|
+
* slot per form, optionally scoped per record), cleared as soon as a submit is
|
|
18
|
+
* confirmed recorded, and gone when the tab closes.
|
|
19
|
+
*
|
|
20
|
+
* It is never `localStorage`: a tablet or kiosk is shared hardware, and one
|
|
21
|
+
* person's key must not still be sitting there for the next. (A key that does
|
|
22
|
+
* outlive one person's visit is harmless — the claim also covers actor and
|
|
23
|
+
* content — but there is no reason to keep it.)
|
|
24
|
+
*
|
|
25
|
+
* ## Every storage access is wrapped
|
|
26
|
+
*
|
|
27
|
+
* `sessionStorage` throws in private windows, with site data blocked, and in
|
|
28
|
+
* some embedded webviews; even reading the `sessionStorage` property can throw
|
|
29
|
+
* a `SecurityError`. Every access is guarded, and a refused write falls back
|
|
30
|
+
* to an in-memory store, which is read before the backing store so a value
|
|
31
|
+
* the store could not replace or remove is never handed back (a refused
|
|
32
|
+
* removal overwrites the value with a cleared marker; see `guardedRemove`).
|
|
33
|
+
* In the browser that store is module-level, so the key still survives a
|
|
34
|
+
* remount for the life of the page — only a full reload loses it, which is
|
|
35
|
+
* less idempotent, never wrong. During SSR there is no shared
|
|
36
|
+
* store at all — neither the module-level memory nor the default storage (Node
|
|
37
|
+
* 25+ has a process-wide `sessionStorage` global): either would hand one
|
|
38
|
+
* request's key to another, so each read off the browser mints a fresh,
|
|
39
|
+
* unshared key.
|
|
40
|
+
*
|
|
41
|
+
* ## One unresolved submit per key
|
|
42
|
+
*
|
|
43
|
+
* One key per form is only meaningful while at most one submission is
|
|
44
|
+
* unresolved under it. Without that, a person can submit A, change the form,
|
|
45
|
+
* and submit B before A answers: both carry key K, A's answer rotates the slot
|
|
46
|
+
* to K2, and B — a genuinely different write — is left holding a key nothing
|
|
47
|
+
* will reconcile; if B commits and loses its response, its retry carries K2,
|
|
48
|
+
* takes a different claim, and records a second row. So {@link beginSubmit}
|
|
49
|
+
* refuses a second submit while one is unresolved. That cancels the
|
|
50
|
+
* submission itself rather than hoping a disabled attribute was flushed in
|
|
51
|
+
* time, and it holds for a second DIFFERENT submission, not just a double tap.
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
const PREFIX = 'smrt:form-retry';
|
|
56
|
+
/** True in a browser, where a module-level memory store is per-person. */
|
|
57
|
+
function inBrowser() {
|
|
58
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The in-memory fallback. Module-level in the browser so a key survives a
|
|
62
|
+
* remount; never shared on a server (see the module doc).
|
|
63
|
+
*/
|
|
64
|
+
const browserMemory = new Map();
|
|
65
|
+
function memoryStore() {
|
|
66
|
+
return inBrowser() ? browserMemory : null;
|
|
67
|
+
}
|
|
68
|
+
/** Test seam: forget every in-memory key and draft. */
|
|
69
|
+
export function resetFormRetryMemory() {
|
|
70
|
+
browserMemory.clear();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* This tab's `sessionStorage`, or `null` where reading it throws — and `null`
|
|
74
|
+
* off the browser: Node 25+ exposes a process-wide in-memory `sessionStorage`
|
|
75
|
+
* global, which during SSR would hand one request's key to another.
|
|
76
|
+
*/
|
|
77
|
+
function defaultStorage() {
|
|
78
|
+
if (!inBrowser())
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
return (globalThis.sessionStorage ??
|
|
82
|
+
null);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function resolveStorage(storage) {
|
|
89
|
+
return storage === undefined ? defaultStorage() : storage;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* What a clear writes when the store refuses `removeItem()`: a value no key or
|
|
93
|
+
* draft can take, which every read treats as absent. It is written over the
|
|
94
|
+
* old value so a store that still reads cannot hand that value back.
|
|
95
|
+
*/
|
|
96
|
+
const CLEARED = '\u0000smrt:form-retry:cleared';
|
|
97
|
+
/**
|
|
98
|
+
* Read `key` from memory, then from `storage`. Memory holds a value only when
|
|
99
|
+
* its storage write was refused (a successful write and every removal drop the
|
|
100
|
+
* memory copy), so it is the newer of the two: a store that refused to replace
|
|
101
|
+
* or remove an old value must not hand that stale value back. A cleared value
|
|
102
|
+
* ({@link guardedRemove}) reads as `null` from either place, and a cleared
|
|
103
|
+
* memory entry hides whatever the store still holds.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export function guardedGet(storage, key) {
|
|
107
|
+
const remembered = memoryStore()?.get(key);
|
|
108
|
+
if (remembered !== undefined) {
|
|
109
|
+
return remembered === CLEARED ? null : remembered;
|
|
110
|
+
}
|
|
111
|
+
const store = resolveStorage(storage);
|
|
112
|
+
if (!store)
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
const value = store.getItem(key) ?? null;
|
|
116
|
+
return value === CLEARED ? null : value;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Write `key`, falling back to memory when storage refuses. Returns whether the
|
|
124
|
+
* value reached the backing store (it survives a reload).
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
export function guardedSet(storage, key, value) {
|
|
128
|
+
const store = resolveStorage(storage);
|
|
129
|
+
if (store) {
|
|
130
|
+
try {
|
|
131
|
+
store.setItem(key, value);
|
|
132
|
+
memoryStore()?.delete(key);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Refused: fall through to memory.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
memoryStore()?.set(key, value);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Remove `key` from storage and memory alike, so no later read returns it.
|
|
144
|
+
*
|
|
145
|
+
* A store may refuse `removeItem()` while reads still succeed. The old value
|
|
146
|
+
* is then overwritten with a cleared marker, which survives a reload of the
|
|
147
|
+
* tab like any stored value. If that write is refused too, the marker is kept
|
|
148
|
+
* in memory, which reads consult first: the old value is never read back for
|
|
149
|
+
* the life of the page. A store that refuses both cannot be changed at all,
|
|
150
|
+
* so after a full reload it still holds the old value; nothing in the page
|
|
151
|
+
* can prevent that.
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
export function guardedRemove(storage, key) {
|
|
155
|
+
const memory = memoryStore();
|
|
156
|
+
memory?.delete(key);
|
|
157
|
+
const store = resolveStorage(storage);
|
|
158
|
+
if (!store)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
store.removeItem(key);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Refused: overwrite the old value instead.
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
store.setItem(key, CLEARED);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Refused too: hide the old value for the life of the page.
|
|
173
|
+
}
|
|
174
|
+
memory?.set(key, CLEARED);
|
|
175
|
+
}
|
|
176
|
+
/** The derived storage base for a form (and scope). @internal */
|
|
177
|
+
export function storageBase(form, scope) {
|
|
178
|
+
const base = `${PREFIX}:${encodeURIComponent(form)}`;
|
|
179
|
+
return scope === undefined ? base : `${base}:${encodeURIComponent(scope)}`;
|
|
180
|
+
}
|
|
181
|
+
function keyName(location) {
|
|
182
|
+
return (location.storageKey ?? `${storageBase(location.form, location.scope)}:key`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A random v4 UUID from `crypto.getRandomValues` — unguessable, and accepted
|
|
186
|
+
* by a server-side UUID parser.
|
|
187
|
+
*/
|
|
188
|
+
export function mintSubmissionKey() {
|
|
189
|
+
const bytes = new Uint8Array(16);
|
|
190
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
191
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
192
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
193
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
194
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* This tab's key for a form, minting and storing one on first use. The same
|
|
198
|
+
* value comes back across a remount or a reload until it is cleared, so every
|
|
199
|
+
* retry of one filled-in form carries one key.
|
|
200
|
+
*/
|
|
201
|
+
export function readSubmissionKey(location) {
|
|
202
|
+
const name = keyName(location);
|
|
203
|
+
const existing = guardedGet(location.storage, name);
|
|
204
|
+
if (existing)
|
|
205
|
+
return existing;
|
|
206
|
+
const minted = (location.mint ?? mintSubmissionKey)();
|
|
207
|
+
guardedSet(location.storage, name, minted);
|
|
208
|
+
return minted;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Whether the form's key currently lives in the backing store (and so
|
|
212
|
+
* survives a reload), rather than only in memory.
|
|
213
|
+
*/
|
|
214
|
+
export function isSubmissionKeyPersistent(location) {
|
|
215
|
+
const store = resolveStorage(location.storage);
|
|
216
|
+
if (!store)
|
|
217
|
+
return false;
|
|
218
|
+
// A key held in memory is one the store refused to write: whatever the
|
|
219
|
+
// store still holds under that name is stale.
|
|
220
|
+
if (memoryStore()?.has(keyName(location)))
|
|
221
|
+
return false;
|
|
222
|
+
try {
|
|
223
|
+
const value = store.getItem(keyName(location));
|
|
224
|
+
return value !== null && value !== CLEARED;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Drop the key once its submit is confirmed recorded, so the next fill of the
|
|
232
|
+
* form starts a claim of its own. This is the boundary between a resubmission
|
|
233
|
+
* and a deliberate repeat: adding the same line twice on purpose is a new fill
|
|
234
|
+
* and takes a new key; a double tap, a retry after a lost response, and two
|
|
235
|
+
* concurrent requests carry the key the form still holds.
|
|
236
|
+
*
|
|
237
|
+
* Call it only on a result the server confirmed recorded — never on a
|
|
238
|
+
* validation failure, whose submit wrote nothing and whose key is still free.
|
|
239
|
+
*/
|
|
240
|
+
export function clearSubmissionKey(location) {
|
|
241
|
+
guardedRemove(location.storage, keyName(location));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Clear the key and mint the next one, returning it — for an inline form that
|
|
245
|
+
* stays on screen after a confirmed write.
|
|
246
|
+
*
|
|
247
|
+
* The fresh key is written over the old one directly rather than read back:
|
|
248
|
+
* a store may refuse `removeItem()` while reads still succeed, and reading
|
|
249
|
+
* after a failed clear would return the retired key. If the replacement write
|
|
250
|
+
* is refused too, the fresh key lives in memory, which {@link guardedGet}
|
|
251
|
+
* consults before the store.
|
|
252
|
+
*/
|
|
253
|
+
export function rotateSubmissionKey(location) {
|
|
254
|
+
clearSubmissionKey(location);
|
|
255
|
+
const minted = (location.mint ?? mintSubmissionKey)();
|
|
256
|
+
guardedSet(location.storage, keyName(location), minted);
|
|
257
|
+
return minted;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Start a submit, or refuse it because one is still unresolved.
|
|
261
|
+
*
|
|
262
|
+
* `start: false` means the caller must `cancel()` the submission: nothing has
|
|
263
|
+
* been sent and the form keeps its content. A double tap lands here too — the
|
|
264
|
+
* second activation is cancelled in the browser, which costs nothing, because
|
|
265
|
+
* the server-side claim still covers every request the browser or the network
|
|
266
|
+
* retries on its own.
|
|
267
|
+
*/
|
|
268
|
+
export function beginSubmit(state) {
|
|
269
|
+
if (state.inFlight !== null) {
|
|
270
|
+
return { state, start: false, sent: state.inFlight };
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
state: { ...state, inFlight: state.token },
|
|
274
|
+
start: true,
|
|
275
|
+
sent: state.token,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The rotation policy as one pure function.
|
|
280
|
+
*
|
|
281
|
+
* `sent` is the key the submit carried; `live` is the key of the slot's
|
|
282
|
+
* unresolved submit, or `null` when it has none left to settle.
|
|
283
|
+
*
|
|
284
|
+
* - **A stale result is ignored outright.** A double tap can send two requests
|
|
285
|
+
* under one key, so two results come back, and by the time the second lands
|
|
286
|
+
* the first has already settled the slot. Acting on it would reset the form
|
|
287
|
+
* and rotate AGAIN, out from under a submission started since.
|
|
288
|
+
* - **A transport error keeps the key and the form.** It wrote nothing that
|
|
289
|
+
* the browser can know about, and the claim covers the content, so the
|
|
290
|
+
* unchanged re-submit must be able to reproduce it.
|
|
291
|
+
* - **A confirmed write applies**, and retires the key only if the form is
|
|
292
|
+
* cleared by that apply — see {@link settleSubmit}.
|
|
293
|
+
* - **A validation failure applies without rotating**: nothing was written, so
|
|
294
|
+
* the key is still free and the corrected re-submit keeps the same key.
|
|
295
|
+
* - **A redirect** is a confirmed write only when the caller says so
|
|
296
|
+
* (`redirectConfirmsWrite`); otherwise it is treated like a validation
|
|
297
|
+
* result, because it cannot vouch for a write (a redirect to a sign-in page
|
|
298
|
+
* wrote nothing).
|
|
299
|
+
*/
|
|
300
|
+
export function submitDisposition(sent, live, outcome, options = {}) {
|
|
301
|
+
if (live === null || sent !== live)
|
|
302
|
+
return 'ignore';
|
|
303
|
+
if (outcome === 'error')
|
|
304
|
+
return 'transport-error';
|
|
305
|
+
if (outcome === 'success')
|
|
306
|
+
return 'rotate-and-apply';
|
|
307
|
+
if (outcome === 'redirect' && options.redirectConfirmsWrite) {
|
|
308
|
+
return 'rotate-and-apply';
|
|
309
|
+
}
|
|
310
|
+
return 'apply';
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Settle one result against the slot, returning the new slot and what to do.
|
|
314
|
+
*
|
|
315
|
+
* ## A key is retired when its FORM IS CLEARED, not merely on success
|
|
316
|
+
*
|
|
317
|
+
* Rotating on success alone leaves a window where the form still SHOWS
|
|
318
|
+
* content that is already recorded while holding a fresh key — after an edit
|
|
319
|
+
* the conditional reset preserved, or after a remount put the submitted values
|
|
320
|
+
* back. A further submit from that form would then take a new claim and record
|
|
321
|
+
* a genuine second row from content nobody re-entered. Keeping the key instead
|
|
322
|
+
* is right, and safe because the claim covers the CONTENT too:
|
|
323
|
+
*
|
|
324
|
+
* - the form still shows what was recorded, so re-submitting it derives the
|
|
325
|
+
* SAME claim and collapses onto the row that already exists;
|
|
326
|
+
* - the form shows something genuinely different, so it derives a DIFFERENT
|
|
327
|
+
* claim and is recorded, which is what it should be.
|
|
328
|
+
*
|
|
329
|
+
* Only an emptied form has nothing left to reconcile, so only then does the
|
|
330
|
+
* next entry deserve a key of its own.
|
|
331
|
+
*/
|
|
332
|
+
export function settleSubmit(state, sent, outcome, options) {
|
|
333
|
+
const disposition = submitDisposition(sent, state.inFlight, outcome, {
|
|
334
|
+
redirectConfirmsWrite: options.redirectConfirmsWrite,
|
|
335
|
+
});
|
|
336
|
+
if (disposition === 'ignore')
|
|
337
|
+
return { state, disposition };
|
|
338
|
+
if (disposition === 'rotate-and-apply' && options.formCleared) {
|
|
339
|
+
return { state: { token: options.mint(), inFlight: null }, disposition };
|
|
340
|
+
}
|
|
341
|
+
return { state: { ...state, inFlight: null }, disposition };
|
|
342
|
+
}
|