@morscherlab/mint-sdk 1.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -0
- package/dist/__stories__/experiment-helpers.d.ts +25 -0
- package/dist/__tests__/components/AppLayout.test.d.ts +1 -0
- package/dist/__tests__/components/AppSidebar.test.d.ts +1 -0
- package/dist/__tests__/components/AppTopBar.test.d.ts +1 -0
- package/dist/__tests__/components/BaseInput.test.d.ts +1 -0
- package/dist/__tests__/components/BasePill.test.d.ts +1 -0
- package/dist/__tests__/components/Calendar.test.d.ts +1 -0
- package/dist/__tests__/components/CollapsibleCard.test.d.ts +1 -0
- package/dist/__tests__/components/DataFrame.test.d.ts +1 -0
- package/dist/__tests__/components/DropdownButton.test.d.ts +1 -0
- package/dist/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
- package/dist/__tests__/composables/useAppExperiment.test.d.ts +1 -0
- package/dist/__tests__/composables/useAuth.test.d.ts +1 -0
- package/dist/__tests__/composables/useAutoGroup.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentData.test.d.ts +13 -0
- package/dist/__tests__/composables/useExperimentSave.test.d.ts +1 -0
- package/dist/__tests__/composables/useForm.test.d.ts +1 -0
- package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformContext.test.d.ts +1 -0
- package/dist/__tests__/composables/usePluginApi.test.d.ts +13 -0
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +14 -0
- package/dist/__tests__/utils/color.test.d.ts +1 -0
- package/dist/auth-BYmxZdJl.js +297 -0
- package/dist/auth-BYmxZdJl.js.map +1 -0
- package/dist/components/AlertBox.vue.d.ts +34 -0
- package/dist/components/AppAvatarMenu.vue.d.ts +58 -0
- package/dist/components/AppContainer.vue.d.ts +28 -0
- package/dist/components/AppLayout.vue.d.ts +31 -0
- package/dist/components/AppPageSelector.vue.d.ts +43 -0
- package/dist/components/AppPillNav.vue.d.ts +11 -0
- package/dist/components/AppPluginSwitcher.vue.d.ts +38 -0
- package/dist/components/AppSidebar.vue.d.ts +47 -0
- package/dist/components/AppTopBar.vue.d.ts +111 -0
- package/dist/components/AuditTrail.vue.d.ts +38 -0
- package/dist/components/AutoGroupModal.vue.d.ts +124 -0
- package/dist/components/Avatar.vue.d.ts +14 -0
- package/dist/components/BaseButton.vue.d.ts +37 -0
- package/dist/components/BaseCheckbox.vue.d.ts +17 -0
- package/dist/components/BaseInput.vue.d.ts +34 -0
- package/dist/components/BaseModal.vue.d.ts +46 -0
- package/dist/components/BasePill.vue.d.ts +57 -0
- package/dist/components/BaseRadioGroup.vue.d.ts +21 -0
- package/dist/components/BaseSelect.vue.d.ts +20 -0
- package/dist/components/BaseSlider.vue.d.ts +22 -0
- package/dist/components/BaseTabs.vue.d.ts +14 -0
- package/dist/components/BaseTextarea.vue.d.ts +30 -0
- package/dist/components/BaseToggle.vue.d.ts +19 -0
- package/dist/components/BatchProgressList.vue.d.ts +43 -0
- package/dist/components/Breadcrumb.vue.d.ts +33 -0
- package/dist/components/Calendar.vue.d.ts +107 -0
- package/dist/components/ChartContainer.vue.d.ts +31 -0
- package/dist/components/ChemicalFormula.vue.d.ts +8 -0
- package/dist/components/CollapsibleCard.vue.d.ts +41 -0
- package/dist/components/ColorSlider.vue.d.ts +34 -0
- package/dist/components/ConcentrationInput.vue.d.ts +25 -0
- package/dist/components/ConfirmDialog.vue.d.ts +42 -0
- package/dist/components/DataFrame.vue.d.ts +107 -0
- package/dist/components/DatePicker.vue.d.ts +25 -0
- package/dist/components/DateTimePicker.vue.d.ts +30 -0
- package/dist/components/Divider.vue.d.ts +14 -0
- package/dist/components/DoseCalculator.vue.d.ts +19 -0
- package/dist/components/DropdownButton.vue.d.ts +47 -0
- package/dist/components/EmptyState.vue.d.ts +36 -0
- package/dist/components/ExperimentCodeBadge.vue.d.ts +14 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +29 -0
- package/dist/components/ExperimentPopover.vue.d.ts +32 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +28 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +44 -0
- package/dist/components/FileUploader.vue.d.ts +40 -0
- package/dist/components/FitPanel.vue.d.ts +46 -0
- package/dist/components/FormActions.vue.d.ts +33 -0
- package/dist/components/FormBuilder.vue.d.ts +287 -0
- package/dist/components/FormField.vue.d.ts +28 -0
- package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
- package/dist/components/FormSection.vue.d.ts +43 -0
- package/dist/components/FormulaInput.vue.d.ts +25 -0
- package/dist/components/GroupAssigner.vue.d.ts +25 -0
- package/dist/components/GroupingModal.vue.d.ts +12 -0
- package/dist/components/IconButton.vue.d.ts +34 -0
- package/dist/components/LoadingSpinner.vue.d.ts +12 -0
- package/dist/components/MoleculeInput.vue.d.ts +27 -0
- package/dist/components/MultiSelect.vue.d.ts +19 -0
- package/dist/components/NumberInput.vue.d.ts +22 -0
- package/dist/components/PlateMapEditor.vue.d.ts +50 -0
- package/dist/components/ProgressBar.vue.d.ts +23 -0
- package/dist/components/ProtocolStepEditor.vue.d.ts +24 -0
- package/dist/components/RackEditor.vue.d.ts +40 -0
- package/dist/components/ReagentEditor.vue.d.ts +30 -0
- package/dist/components/ReagentList.vue.d.ts +32 -0
- package/dist/components/ResourceCard.vue.d.ts +50 -0
- package/dist/components/SampleHierarchyTree.vue.d.ts +26 -0
- package/dist/components/SampleLegend.vue.d.ts +32 -0
- package/dist/components/SampleSelector.vue.d.ts +29 -0
- package/dist/components/ScheduleCalendar.vue.d.ts +110 -0
- package/dist/components/ScientificNumber.vue.d.ts +14 -0
- package/dist/components/SegmentedControl.vue.d.ts +20 -0
- package/dist/components/SequenceInput.vue.d.ts +54 -0
- package/dist/components/SettingsButton.vue.d.ts +30 -0
- package/dist/components/SettingsModal.vue.d.ts +36 -0
- package/dist/components/Skeleton.vue.d.ts +11 -0
- package/dist/components/StatusIndicator.vue.d.ts +13 -0
- package/dist/components/StepWizard.vue.d.ts +65 -0
- package/dist/components/TagsInput.vue.d.ts +39 -0
- package/dist/components/ThemeToggle.vue.d.ts +7 -0
- package/dist/components/TimePicker.vue.d.ts +29 -0
- package/dist/components/TimeRangeInput.vue.d.ts +27 -0
- package/dist/components/ToastNotification.vue.d.ts +2 -0
- package/dist/components/Tooltip.vue.d.ts +35 -0
- package/dist/components/UnitInput.vue.d.ts +39 -0
- package/dist/components/WellEditPopup.vue.d.ts +25 -0
- package/dist/components/WellPlate.vue.d.ts +73 -0
- package/dist/components/index.d.ts +87 -0
- package/dist/components/index.js +3 -0
- package/dist/components-CKf-UpGi.js +15089 -0
- package/dist/components-CKf-UpGi.js.map +1 -0
- package/dist/composables/experiment-utils.d.ts +8 -0
- package/dist/composables/formBuilderRegistry.d.ts +13 -0
- package/dist/composables/index.d.ts +28 -0
- package/dist/composables/index.js +3 -0
- package/dist/composables/useApi.d.ts +20 -0
- package/dist/composables/useAppExperiment.d.ts +37 -0
- package/dist/composables/useAsync.d.ts +128 -0
- package/dist/composables/useAuth.d.ts +47 -0
- package/dist/composables/useAutoGroup.d.ts +106 -0
- package/dist/composables/useChemicalFormula.d.ts +21 -0
- package/dist/composables/useConcentrationUnits.d.ts +29 -0
- package/dist/composables/useDoseCalculator.d.ts +58 -0
- package/dist/composables/useExperimentData.d.ts +18 -0
- package/dist/composables/useExperimentSave.d.ts +36 -0
- package/dist/composables/useExperimentSelector.d.ts +30 -0
- package/dist/composables/useForm.d.ts +92 -0
- package/dist/composables/useFormBuilder.d.ts +24 -0
- package/dist/composables/usePasskey.d.ts +10 -0
- package/dist/composables/usePlatformContext.d.ts +131 -0
- package/dist/composables/usePluginApi.d.ts +29 -0
- package/dist/composables/usePluginConfig.d.ts +13 -0
- package/dist/composables/useProtocolTemplates.d.ts +44 -0
- package/dist/composables/useRackEditor.d.ts +31 -0
- package/dist/composables/useReagentSeries.d.ts +23 -0
- package/dist/composables/useScheduleDrag.d.ts +78 -0
- package/dist/composables/useSequenceUtils.d.ts +14 -0
- package/dist/composables/useTheme.d.ts +8 -0
- package/dist/composables/useTimeUtils.d.ts +29 -0
- package/dist/composables/useToast.d.ts +22 -0
- package/dist/composables/useWellPlateEditor.d.ts +33 -0
- package/dist/composables-D0QfFzq1.js +805 -0
- package/dist/composables-D0QfFzq1.js.map +1 -0
- package/dist/histoire.setup.d.ts +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/install.d.ts +16 -0
- package/dist/install.js +23 -0
- package/dist/install.js.map +1 -0
- package/dist/stores/auth.d.ts +146 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/index.js +2 -0
- package/dist/stores/settings.d.ts +75 -0
- package/dist/styles.css +29728 -0
- package/dist/tailwind.preset.d.ts +58 -0
- package/dist/tailwind.preset.js +66 -0
- package/dist/tailwind.preset.js.map +1 -0
- package/dist/types/auth.d.ts +42 -0
- package/dist/types/auto-group.d.ts +34 -0
- package/dist/types/components.d.ts +528 -0
- package/dist/types/form-builder.d.ts +167 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +0 -0
- package/dist/types/platform.d.ts +75 -0
- package/dist/useScheduleDrag-DAJueTbK.js +7181 -0
- package/dist/useScheduleDrag-DAJueTbK.js.map +1 -0
- package/dist/utils/color.d.ts +24 -0
- package/package.json +114 -0
- package/src/__stories__/experiment-helpers.ts +83 -0
- package/src/__tests__/components/AppLayout.test.ts +163 -0
- package/src/__tests__/components/AppSidebar.test.ts +292 -0
- package/src/__tests__/components/AppTopBar.test.ts +683 -0
- package/src/__tests__/components/BaseInput.test.ts +99 -0
- package/src/__tests__/components/BasePill.test.ts +291 -0
- package/src/__tests__/components/Calendar.test.ts +566 -0
- package/src/__tests__/components/CollapsibleCard.test.ts +524 -0
- package/src/__tests__/components/DataFrame.test.ts +767 -0
- package/src/__tests__/components/DropdownButton.test.ts +471 -0
- package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
- package/src/__tests__/composables/useAppExperiment.test.ts +560 -0
- package/src/__tests__/composables/useAuth.test.ts +188 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +860 -0
- package/src/__tests__/composables/useExperimentData.test.ts +127 -0
- package/src/__tests__/composables/useExperimentSave.test.ts +347 -0
- package/src/__tests__/composables/useForm.test.ts +205 -0
- package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
- package/src/__tests__/composables/usePlatformContext.test.ts +116 -0
- package/src/__tests__/composables/usePluginApi.test.ts +81 -0
- package/src/__tests__/composables/usePluginConfig.test.ts +176 -0
- package/src/__tests__/utils/color.test.ts +96 -0
- package/src/components/AlertBox.story.vue +204 -0
- package/src/components/AlertBox.vue +88 -0
- package/src/components/AppAvatarMenu.story.vue +155 -0
- package/src/components/AppAvatarMenu.vue +184 -0
- package/src/components/AppContainer.story.vue +104 -0
- package/src/components/AppContainer.vue +34 -0
- package/src/components/AppLayout.story.vue +292 -0
- package/src/components/AppLayout.vue +75 -0
- package/src/components/AppPageSelector.vue +159 -0
- package/src/components/AppPillNav.vue +66 -0
- package/src/components/AppPluginSwitcher.vue +241 -0
- package/src/components/AppSidebar.story.vue +309 -0
- package/src/components/AppSidebar.vue +119 -0
- package/src/components/AppTopBar.story.vue +304 -0
- package/src/components/AppTopBar.vue +661 -0
- package/src/components/AuditTrail.story.vue +163 -0
- package/src/components/AuditTrail.vue +151 -0
- package/src/components/AutoGroupModal.story.vue +273 -0
- package/src/components/AutoGroupModal.vue +566 -0
- package/src/components/Avatar.story.vue +115 -0
- package/src/components/Avatar.vue +79 -0
- package/src/components/BaseButton.story.vue +96 -0
- package/src/components/BaseButton.vue +73 -0
- package/src/components/BaseCheckbox.story.vue +73 -0
- package/src/components/BaseCheckbox.vue +69 -0
- package/src/components/BaseInput.story.vue +98 -0
- package/src/components/BaseInput.vue +74 -0
- package/src/components/BaseModal.story.vue +237 -0
- package/src/components/BaseModal.vue +182 -0
- package/src/components/BasePill.story.vue +142 -0
- package/src/components/BasePill.vue +89 -0
- package/src/components/BaseRadioGroup.story.vue +145 -0
- package/src/components/BaseRadioGroup.vue +124 -0
- package/src/components/BaseSelect.story.vue +120 -0
- package/src/components/BaseSelect.vue +71 -0
- package/src/components/BaseSlider.story.vue +122 -0
- package/src/components/BaseSlider.vue +126 -0
- package/src/components/BaseTabs.story.vue +127 -0
- package/src/components/BaseTabs.vue +59 -0
- package/src/components/BaseTextarea.story.vue +91 -0
- package/src/components/BaseTextarea.vue +62 -0
- package/src/components/BaseToggle.story.vue +81 -0
- package/src/components/BaseToggle.vue +76 -0
- package/src/components/BatchProgressList.story.vue +92 -0
- package/src/components/BatchProgressList.vue +184 -0
- package/src/components/Breadcrumb.story.vue +106 -0
- package/src/components/Breadcrumb.vue +75 -0
- package/src/components/Calendar.story.vue +106 -0
- package/src/components/Calendar.vue +363 -0
- package/src/components/ChartContainer.story.vue +113 -0
- package/src/components/ChartContainer.vue +64 -0
- package/src/components/ChemicalFormula.story.vue +102 -0
- package/src/components/ChemicalFormula.vue +39 -0
- package/src/components/CollapsibleCard.story.vue +135 -0
- package/src/components/CollapsibleCard.vue +167 -0
- package/src/components/ColorSlider.story.vue +120 -0
- package/src/components/ColorSlider.vue +164 -0
- package/src/components/ConcentrationInput.story.vue +77 -0
- package/src/components/ConcentrationInput.vue +185 -0
- package/src/components/ConfirmDialog.story.vue +248 -0
- package/src/components/ConfirmDialog.vue +93 -0
- package/src/components/DataFrame.story.vue +148 -0
- package/src/components/DataFrame.vue +419 -0
- package/src/components/DatePicker.story.vue +119 -0
- package/src/components/DatePicker.vue +330 -0
- package/src/components/DateTimePicker.story.vue +112 -0
- package/src/components/DateTimePicker.vue +392 -0
- package/src/components/Divider.story.vue +80 -0
- package/src/components/Divider.vue +49 -0
- package/src/components/DoseCalculator.story.vue +68 -0
- package/src/components/DoseCalculator.vue +476 -0
- package/src/components/DropdownButton.story.vue +102 -0
- package/src/components/DropdownButton.vue +181 -0
- package/src/components/EmptyState.story.vue +135 -0
- package/src/components/EmptyState.vue +69 -0
- package/src/components/ExperimentCodeBadge.story.vue +77 -0
- package/src/components/ExperimentCodeBadge.vue +64 -0
- package/src/components/ExperimentDataViewer.story.vue +174 -0
- package/src/components/ExperimentDataViewer.vue +288 -0
- package/src/components/ExperimentPopover.story.vue +384 -0
- package/src/components/ExperimentPopover.vue +241 -0
- package/src/components/ExperimentSelectorModal.story.vue +391 -0
- package/src/components/ExperimentSelectorModal.vue +387 -0
- package/src/components/ExperimentTimeline.story.vue +161 -0
- package/src/components/ExperimentTimeline.vue +382 -0
- package/src/components/FileUploader.story.vue +107 -0
- package/src/components/FileUploader.vue +386 -0
- package/src/components/FitPanel.story.vue +125 -0
- package/src/components/FitPanel.vue +120 -0
- package/src/components/FormActions.vue +92 -0
- package/src/components/FormBuilder.vue +214 -0
- package/src/components/FormField.story.vue +132 -0
- package/src/components/FormField.vue +59 -0
- package/src/components/FormFieldRenderer.vue +58 -0
- package/src/components/FormSection.vue +90 -0
- package/src/components/FormulaInput.story.vue +96 -0
- package/src/components/FormulaInput.vue +125 -0
- package/src/components/GroupAssigner.story.vue +83 -0
- package/src/components/GroupAssigner.vue +284 -0
- package/src/components/GroupingModal.story.vue +52 -0
- package/src/components/GroupingModal.vue +422 -0
- package/src/components/IconButton.story.vue +135 -0
- package/src/components/IconButton.vue +73 -0
- package/src/components/LoadingSpinner.story.vue +70 -0
- package/src/components/LoadingSpinner.vue +50 -0
- package/src/components/MoleculeInput.story.vue +66 -0
- package/src/components/MoleculeInput.vue +426 -0
- package/src/components/MultiSelect.story.vue +132 -0
- package/src/components/MultiSelect.vue +118 -0
- package/src/components/NumberInput.story.vue +122 -0
- package/src/components/NumberInput.vue +160 -0
- package/src/components/PlateMapEditor.story.vue +92 -0
- package/src/components/PlateMapEditor.vue +513 -0
- package/src/components/ProgressBar.story.vue +148 -0
- package/src/components/ProgressBar.vue +114 -0
- package/src/components/ProtocolStepEditor.story.vue +69 -0
- package/src/components/ProtocolStepEditor.vue +522 -0
- package/src/components/RackEditor.story.vue +100 -0
- package/src/components/RackEditor.vue +371 -0
- package/src/components/ReagentEditor.story.vue +153 -0
- package/src/components/ReagentEditor.vue +418 -0
- package/src/components/ReagentList.story.vue +137 -0
- package/src/components/ReagentList.vue +463 -0
- package/src/components/ResourceCard.story.vue +150 -0
- package/src/components/ResourceCard.vue +161 -0
- package/src/components/SampleHierarchyTree.story.vue +161 -0
- package/src/components/SampleHierarchyTree.vue +256 -0
- package/src/components/SampleLegend.story.vue +91 -0
- package/src/components/SampleLegend.vue +119 -0
- package/src/components/SampleSelector.story.vue +111 -0
- package/src/components/SampleSelector.vue +1033 -0
- package/src/components/ScheduleCalendar.story.vue +195 -0
- package/src/components/ScheduleCalendar.vue +569 -0
- package/src/components/ScientificNumber.story.vue +127 -0
- package/src/components/ScientificNumber.vue +197 -0
- package/src/components/SegmentedControl.story.vue +132 -0
- package/src/components/SegmentedControl.vue +79 -0
- package/src/components/SequenceInput.story.vue +119 -0
- package/src/components/SequenceInput.vue +209 -0
- package/src/components/SettingsButton.story.vue +58 -0
- package/src/components/SettingsButton.vue +76 -0
- package/src/components/SettingsModal.story.vue +145 -0
- package/src/components/SettingsModal.vue +146 -0
- package/src/components/Skeleton.story.vue +141 -0
- package/src/components/Skeleton.vue +74 -0
- package/src/components/StatusIndicator.story.vue +99 -0
- package/src/components/StatusIndicator.vue +40 -0
- package/src/components/StepWizard.story.vue +155 -0
- package/src/components/StepWizard.vue +223 -0
- package/src/components/TagsInput.story.vue +155 -0
- package/src/components/TagsInput.vue +265 -0
- package/src/components/ThemeToggle.story.vue +36 -0
- package/src/components/ThemeToggle.vue +54 -0
- package/src/components/TimePicker.story.vue +96 -0
- package/src/components/TimePicker.vue +273 -0
- package/src/components/TimeRangeInput.story.vue +104 -0
- package/src/components/TimeRangeInput.vue +122 -0
- package/src/components/ToastNotification.story.vue +157 -0
- package/src/components/ToastNotification.vue +62 -0
- package/src/components/Tooltip.story.vue +138 -0
- package/src/components/Tooltip.vue +119 -0
- package/src/components/UnitInput.story.vue +194 -0
- package/src/components/UnitInput.vue +213 -0
- package/src/components/WellEditPopup.vue +234 -0
- package/src/components/WellPlate.story.vue +282 -0
- package/src/components/WellPlate.vue +830 -0
- package/src/components/index.ts +118 -0
- package/src/composables/experiment-utils.ts +57 -0
- package/src/composables/formBuilderRegistry.ts +79 -0
- package/src/composables/index.ts +140 -0
- package/src/composables/useApi.ts +167 -0
- package/src/composables/useAppExperiment.ts +159 -0
- package/src/composables/useAsync.ts +323 -0
- package/src/composables/useAuth.ts +445 -0
- package/src/composables/useAutoGroup.ts +641 -0
- package/src/composables/useChemicalFormula.ts +275 -0
- package/src/composables/useConcentrationUnits.ts +246 -0
- package/src/composables/useDoseCalculator.ts +370 -0
- package/src/composables/useExperimentData.ts +86 -0
- package/src/composables/useExperimentSave.ts +192 -0
- package/src/composables/useExperimentSelector.ts +292 -0
- package/src/composables/useForm.ts +416 -0
- package/src/composables/useFormBuilder.ts +383 -0
- package/src/composables/usePasskey.ts +216 -0
- package/src/composables/usePlatformContext.ts +299 -0
- package/src/composables/usePluginApi.ts +39 -0
- package/src/composables/usePluginConfig.ts +93 -0
- package/src/composables/useProtocolTemplates.ts +518 -0
- package/src/composables/useRackEditor.ts +222 -0
- package/src/composables/useReagentSeries.ts +91 -0
- package/src/composables/useScheduleDrag.ts +245 -0
- package/src/composables/useSequenceUtils.ts +105 -0
- package/src/composables/useTheme.ts +58 -0
- package/src/composables/useTimeUtils.ts +131 -0
- package/src/composables/useToast.ts +40 -0
- package/src/composables/useWellPlateEditor.ts +421 -0
- package/src/histoire.setup.ts +17 -0
- package/src/index.ts +367 -0
- package/src/install.ts +32 -0
- package/src/stores/auth.ts +152 -0
- package/src/stores/index.ts +2 -0
- package/src/stores/settings.ts +218 -0
- package/src/styles/components/alert-box.css +150 -0
- package/src/styles/components/app-avatar-menu.css +155 -0
- package/src/styles/components/app-container.css +33 -0
- package/src/styles/components/app-layout.css +98 -0
- package/src/styles/components/app-page-selector.css +191 -0
- package/src/styles/components/app-pill-nav.css +57 -0
- package/src/styles/components/app-plugin-switcher.css +209 -0
- package/src/styles/components/app-sidebar.css +145 -0
- package/src/styles/components/app-top-bar.css +492 -0
- package/src/styles/components/audit-trail.css +143 -0
- package/src/styles/components/auto-group-modal.css +644 -0
- package/src/styles/components/avatar.css +73 -0
- package/src/styles/components/batch-progress-list.css +196 -0
- package/src/styles/components/breadcrumb.css +64 -0
- package/src/styles/components/button.css +188 -0
- package/src/styles/components/calendar.css +192 -0
- package/src/styles/components/chart-container.css +69 -0
- package/src/styles/components/checkbox.css +123 -0
- package/src/styles/components/chemical-formula.css +46 -0
- package/src/styles/components/collapsible-card.css +253 -0
- package/src/styles/components/color-slider.css +110 -0
- package/src/styles/components/concentration-input.css +156 -0
- package/src/styles/components/confirm-dialog.css +183 -0
- package/src/styles/components/dataframe.css +382 -0
- package/src/styles/components/date-picker.css +243 -0
- package/src/styles/components/datetime-picker.css +229 -0
- package/src/styles/components/divider.css +63 -0
- package/src/styles/components/dose-calculator.css +301 -0
- package/src/styles/components/dropdown-button.css +280 -0
- package/src/styles/components/empty-state.css +151 -0
- package/src/styles/components/experiment-code-badge.css +33 -0
- package/src/styles/components/experiment-data-viewer.css +138 -0
- package/src/styles/components/experiment-popover.css +562 -0
- package/src/styles/components/experiment-selector-modal.css +285 -0
- package/src/styles/components/experiment-timeline.css +529 -0
- package/src/styles/components/file-uploader.css +310 -0
- package/src/styles/components/fit-panel.css +67 -0
- package/src/styles/components/form-builder.css +69 -0
- package/src/styles/components/form-field.css +48 -0
- package/src/styles/components/formula-input.css +103 -0
- package/src/styles/components/group-assigner.css +200 -0
- package/src/styles/components/grouping-modal.css +323 -0
- package/src/styles/components/icon-button.css +192 -0
- package/src/styles/components/input.css +66 -0
- package/src/styles/components/loading-spinner.css +67 -0
- package/src/styles/components/modal.css +350 -0
- package/src/styles/components/molecule-input.css +186 -0
- package/src/styles/components/multi-select.css +131 -0
- package/src/styles/components/number-input.css +199 -0
- package/src/styles/components/pill.css +188 -0
- package/src/styles/components/plate-map-editor.css +464 -0
- package/src/styles/components/progress-bar.css +133 -0
- package/src/styles/components/protocol-step-editor.css +449 -0
- package/src/styles/components/rack-editor.css +265 -0
- package/src/styles/components/radio-group.css +240 -0
- package/src/styles/components/reagent-editor.css +510 -0
- package/src/styles/components/reagent-list.css +407 -0
- package/src/styles/components/resource-card.css +360 -0
- package/src/styles/components/sample-hierarchy-tree.css +314 -0
- package/src/styles/components/sample-legend.css +201 -0
- package/src/styles/components/sample-selector.css +751 -0
- package/src/styles/components/schedule-calendar.css +478 -0
- package/src/styles/components/scientific-number.css +63 -0
- package/src/styles/components/segmented-control.css +197 -0
- package/src/styles/components/select.css +77 -0
- package/src/styles/components/sequence-input.css +184 -0
- package/src/styles/components/settings-button.css +94 -0
- package/src/styles/components/settings-modal.css +95 -0
- package/src/styles/components/skeleton.css +49 -0
- package/src/styles/components/slider.css +74 -0
- package/src/styles/components/status-indicator.css +66 -0
- package/src/styles/components/step-wizard.css +192 -0
- package/src/styles/components/tabs.css +95 -0
- package/src/styles/components/tags-input.css +195 -0
- package/src/styles/components/textarea.css +82 -0
- package/src/styles/components/theme-toggle.css +69 -0
- package/src/styles/components/time-picker.css +171 -0
- package/src/styles/components/time-range-input.css +42 -0
- package/src/styles/components/toast.css +91 -0
- package/src/styles/components/toggle.css +146 -0
- package/src/styles/components/tooltip.css +91 -0
- package/src/styles/components/unit-input.css +123 -0
- package/src/styles/components/well-edit-popup.css +252 -0
- package/src/styles/components/well-plate.css +307 -0
- package/src/styles/index.css +87 -0
- package/src/styles/variables.css +1117 -0
- package/src/tailwind.preset.ts +61 -0
- package/src/types/auth.ts +55 -0
- package/src/types/auto-group.ts +40 -0
- package/src/types/components.ts +710 -0
- package/src/types/form-builder.ts +197 -0
- package/src/types/index.ts +207 -0
- package/src/types/platform.ts +116 -0
- package/src/utils/color.ts +96 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
import { useForm, type FieldRules, type UseFormReturn } from './useForm'
|
|
3
|
+
import { getFieldRegistryEntry, getTypeDefault } from './formBuilderRegistry'
|
|
4
|
+
import type {
|
|
5
|
+
FormSchema,
|
|
6
|
+
FormFieldSchema,
|
|
7
|
+
FormSectionSchema,
|
|
8
|
+
FieldCondition,
|
|
9
|
+
FieldValidation,
|
|
10
|
+
FormEnhancements,
|
|
11
|
+
UseFormBuilderReturn,
|
|
12
|
+
} from '../types/form-builder'
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Condition evaluator
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Evaluate a JSON-serializable field condition against the current form data.
|
|
20
|
+
*
|
|
21
|
+
* Supports logical operators (`and`, `or`, `not`) and comparison operators
|
|
22
|
+
* (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
|
|
23
|
+
* `contains`). Returns `true` if the condition passes.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluateCondition(
|
|
26
|
+
condition: FieldCondition,
|
|
27
|
+
data: Record<string, unknown>,
|
|
28
|
+
): boolean {
|
|
29
|
+
if ('and' in condition) {
|
|
30
|
+
return condition.and.every((c) => evaluateCondition(c, data))
|
|
31
|
+
}
|
|
32
|
+
if ('or' in condition) {
|
|
33
|
+
return condition.or.some((c) => evaluateCondition(c, data))
|
|
34
|
+
}
|
|
35
|
+
if ('not' in condition) {
|
|
36
|
+
return !evaluateCondition(condition.not, data)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const value = data[condition.field]
|
|
40
|
+
|
|
41
|
+
if ('eq' in condition) return value === condition.eq
|
|
42
|
+
if ('neq' in condition) return value !== condition.neq
|
|
43
|
+
if ('gt' in condition) return typeof value === 'number' && value > condition.gt
|
|
44
|
+
if ('lt' in condition) return typeof value === 'number' && value < condition.lt
|
|
45
|
+
if ('gte' in condition) return typeof value === 'number' && value >= condition.gte
|
|
46
|
+
if ('lte' in condition) return typeof value === 'number' && value <= condition.lte
|
|
47
|
+
if ('in' in condition) return condition.in.includes(value)
|
|
48
|
+
if ('notIn' in condition) return !condition.notIn.includes(value)
|
|
49
|
+
if ('truthy' in condition) return !!value
|
|
50
|
+
if ('falsy' in condition) return !value
|
|
51
|
+
if ('contains' in condition) {
|
|
52
|
+
if (typeof value === 'string') return value.includes(condition.contains)
|
|
53
|
+
if (Array.isArray(value)) return value.includes(condition.contains)
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Schema helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/** Return all sections across steps (wizard) or directly from a flat schema. */
|
|
65
|
+
function collectSections(schema: FormSchema): FormSectionSchema[] {
|
|
66
|
+
return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Return all field schemas in schema order, flattening sections and steps. */
|
|
70
|
+
function flattenFields(schema: FormSchema): FormFieldSchema[] {
|
|
71
|
+
return collectSections(schema).flatMap((section) => section.fields)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
|
|
75
|
+
function convertValidation(v: FieldValidation): FieldRules {
|
|
76
|
+
const rules: FieldRules = {}
|
|
77
|
+
|
|
78
|
+
if (v.required !== undefined) rules.required = v.required
|
|
79
|
+
if (v.minLength !== undefined) rules.minLength = v.minLength
|
|
80
|
+
if (v.maxLength !== undefined) rules.maxLength = v.maxLength
|
|
81
|
+
if (v.min !== undefined) rules.min = v.min
|
|
82
|
+
if (v.max !== undefined) rules.max = v.max
|
|
83
|
+
if (v.email !== undefined) rules.email = v.email
|
|
84
|
+
|
|
85
|
+
if (v.pattern !== undefined) {
|
|
86
|
+
rules.pattern =
|
|
87
|
+
typeof v.pattern === 'string'
|
|
88
|
+
? new RegExp(v.pattern)
|
|
89
|
+
: { value: new RegExp(v.pattern.value), message: v.pattern.message }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return rules
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// useFormBuilder
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drive a `FormSchema` as reactive form state.
|
|
101
|
+
*
|
|
102
|
+
* Builds initial values from schema defaults and `initialData`, derives
|
|
103
|
+
* validation rules from `FieldValidation` descriptors and enhancement
|
|
104
|
+
* validators, evaluates `FieldCondition` expressions for field/section
|
|
105
|
+
* visibility, and wires wizard step navigation when `schema.steps` is set.
|
|
106
|
+
*
|
|
107
|
+
* @param schema - Declarative form or wizard schema.
|
|
108
|
+
* @param initialData - Values that override schema defaults.
|
|
109
|
+
* @param enhancements - TypeScript-only callbacks (dynamic options, validators,
|
|
110
|
+
* submit handler, transform, field-change watcher).
|
|
111
|
+
*/
|
|
112
|
+
/** Drives a declarative FormSchema as reactive state with conditional fields, wizard steps, and validation. */
|
|
113
|
+
export function useFormBuilder<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
114
|
+
schema: FormSchema,
|
|
115
|
+
initialData?: Partial<T>,
|
|
116
|
+
enhancements?: FormEnhancements<T>,
|
|
117
|
+
): UseFormBuilderReturn<T> {
|
|
118
|
+
const fields = flattenFields(schema)
|
|
119
|
+
const sections = collectSections(schema)
|
|
120
|
+
|
|
121
|
+
// -- Build initial values --------------------------------------------------
|
|
122
|
+
const initialValues = {} as Record<string, unknown>
|
|
123
|
+
for (const field of fields) {
|
|
124
|
+
const key = field.name
|
|
125
|
+
if (initialData && key in initialData) {
|
|
126
|
+
initialValues[key] = (initialData as Record<string, unknown>)[key]
|
|
127
|
+
} else if (field.defaultValue !== undefined) {
|
|
128
|
+
initialValues[key] = field.defaultValue
|
|
129
|
+
} else {
|
|
130
|
+
initialValues[key] = getTypeDefault(field.type)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -- Build validation rules ------------------------------------------------
|
|
135
|
+
const rules: Partial<Record<string, FieldRules>> = {}
|
|
136
|
+
for (const field of fields) {
|
|
137
|
+
const base: FieldRules = field.validation ? convertValidation(field.validation) : {}
|
|
138
|
+
const enhancement = enhancements?.fields?.[field.name as keyof T]
|
|
139
|
+
|
|
140
|
+
// Wrap validators to skip hidden fields
|
|
141
|
+
const customValidators: Array<(value: unknown, formData: Record<string, unknown>) => string | undefined | null> = []
|
|
142
|
+
|
|
143
|
+
if (enhancement?.validate) {
|
|
144
|
+
const fn = enhancement.validate
|
|
145
|
+
customValidators.push((value, formData) => fn(value, formData as T))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (customValidators.length > 0) {
|
|
149
|
+
base.custom = customValidators
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Wrap all rules to skip hidden fields
|
|
153
|
+
if (Object.keys(base).length > 0) {
|
|
154
|
+
rules[field.name] = base
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// -- Create form -----------------------------------------------------------
|
|
159
|
+
const form = useForm(initialValues as T, rules as Partial<Record<keyof T, FieldRules>>)
|
|
160
|
+
|
|
161
|
+
// -- Visibility ------------------------------------------------------------
|
|
162
|
+
function isFieldVisible(name: string): boolean {
|
|
163
|
+
const enhancement = enhancements?.fields?.[name as keyof T]
|
|
164
|
+
if (enhancement?.visible) {
|
|
165
|
+
return enhancement.visible(form.data as T)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const field = fields.find((f) => f.name === name)
|
|
169
|
+
if (field?.condition) {
|
|
170
|
+
return evaluateCondition(field.condition, form.data as Record<string, unknown>)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isSectionVisible(id: string): boolean {
|
|
177
|
+
const section = sections.find((s) => s.id === id)
|
|
178
|
+
if (section?.condition) {
|
|
179
|
+
return evaluateCondition(section.condition, form.data as Record<string, unknown>)
|
|
180
|
+
}
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -- Resolved field props --------------------------------------------------
|
|
185
|
+
function getResolvedFieldProps(field: FormFieldSchema): Record<string, unknown> {
|
|
186
|
+
const entry = getFieldRegistryEntry(field.type)
|
|
187
|
+
const formProps = form.getFieldProps(field.name as keyof T)
|
|
188
|
+
|
|
189
|
+
const merged: Record<string, unknown> = {
|
|
190
|
+
...entry.defaults,
|
|
191
|
+
...(field.props ?? {}),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Dynamic enhancement props
|
|
195
|
+
const enhancement = enhancements?.fields?.[field.name as keyof T]
|
|
196
|
+
if (enhancement?.props) {
|
|
197
|
+
Object.assign(merged, enhancement.props(form.data as T))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Dynamic options
|
|
201
|
+
const options = getFieldOptions(field.name)
|
|
202
|
+
if (options) {
|
|
203
|
+
merged.options = options
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Form field bindings
|
|
207
|
+
if (entry.vModel) {
|
|
208
|
+
merged.modelValue = formProps.modelValue
|
|
209
|
+
merged['onUpdate:modelValue'] = formProps['onUpdate:modelValue']
|
|
210
|
+
}
|
|
211
|
+
merged.onBlur = formProps.onBlur
|
|
212
|
+
|
|
213
|
+
// Error as boolean for components that use boolean error prop
|
|
214
|
+
const errorMsg = formProps.error
|
|
215
|
+
if (errorMsg) {
|
|
216
|
+
merged.error = true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Schema-level props
|
|
220
|
+
if (field.placeholder) merged.placeholder = field.placeholder
|
|
221
|
+
if (field.size) merged.size = field.size
|
|
222
|
+
if (field.disabled) merged.disabled = true
|
|
223
|
+
if (field.readonly) merged.readonly = true
|
|
224
|
+
|
|
225
|
+
// Radio group needs a name prop
|
|
226
|
+
if (field.type === 'radio' && !merged.name) {
|
|
227
|
+
merged.name = field.name
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return merged
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getFieldOptions(name: string): { label: string; value: unknown }[] | undefined {
|
|
234
|
+
const enhancement = enhancements?.fields?.[name as keyof T]
|
|
235
|
+
if (enhancement?.options) {
|
|
236
|
+
return enhancement.options(form.data as T)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const field = fields.find((f) => f.name === name)
|
|
240
|
+
if (field?.props?.options) {
|
|
241
|
+
return field.props.options as { label: string; value: unknown }[]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return undefined
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// -- Wizard state ----------------------------------------------------------
|
|
248
|
+
const currentStep = ref(0)
|
|
249
|
+
|
|
250
|
+
const isCurrentStepValid = computed(() => {
|
|
251
|
+
if (!schema.steps) return form.isValid.value
|
|
252
|
+
|
|
253
|
+
const step = schema.steps[currentStep.value]
|
|
254
|
+
if (!step) return true
|
|
255
|
+
|
|
256
|
+
for (const section of step.sections) {
|
|
257
|
+
if (!isSectionVisible(section.id)) continue
|
|
258
|
+
for (const field of section.fields) {
|
|
259
|
+
if (!isFieldVisible(field.name)) continue
|
|
260
|
+
if (!form.validateField(field.name)) return false
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return true
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
function goNext(): boolean {
|
|
267
|
+
if (!schema.steps) return false
|
|
268
|
+
|
|
269
|
+
// Touch and validate all visible fields in current step
|
|
270
|
+
const step = schema.steps[currentStep.value]
|
|
271
|
+
if (!step) return false
|
|
272
|
+
|
|
273
|
+
let valid = true
|
|
274
|
+
for (const section of step.sections) {
|
|
275
|
+
if (!isSectionVisible(section.id)) continue
|
|
276
|
+
for (const field of section.fields) {
|
|
277
|
+
if (!isFieldVisible(field.name)) continue
|
|
278
|
+
form.setFieldTouched(field.name, true)
|
|
279
|
+
if (!form.validateField(field.name)) valid = false
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!valid) return false
|
|
284
|
+
|
|
285
|
+
if (currentStep.value < schema.steps.length - 1) {
|
|
286
|
+
currentStep.value++
|
|
287
|
+
}
|
|
288
|
+
return true
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function goBack(): void {
|
|
292
|
+
if (currentStep.value > 0) {
|
|
293
|
+
currentStep.value--
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function goToStep(index: number): void {
|
|
298
|
+
if (!schema.steps) return
|
|
299
|
+
if (index >= 0 && index < schema.steps.length) {
|
|
300
|
+
currentStep.value = index
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// -- Validate (skip hidden fields) -----------------------------------------
|
|
305
|
+
function validate(): boolean {
|
|
306
|
+
let allValid = true
|
|
307
|
+
for (const field of fields) {
|
|
308
|
+
if (!isFieldVisible(field.name)) continue
|
|
309
|
+
form.setFieldTouched(field.name, true)
|
|
310
|
+
if (!form.validateField(field.name)) {
|
|
311
|
+
allValid = false
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return allValid
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// -- Submit ----------------------------------------------------------------
|
|
318
|
+
async function submit(): Promise<void> {
|
|
319
|
+
if (!validate()) return
|
|
320
|
+
|
|
321
|
+
// Build submission data excluding hidden fields
|
|
322
|
+
let submitData = {} as Record<string, unknown>
|
|
323
|
+
for (const field of fields) {
|
|
324
|
+
if (isFieldVisible(field.name)) {
|
|
325
|
+
submitData[field.name] = (form.data as Record<string, unknown>)[field.name]
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (enhancements?.transform) {
|
|
330
|
+
submitData = enhancements.transform(submitData as T) as Record<string, unknown>
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (enhancements?.onSubmit) {
|
|
334
|
+
form.isSubmitting.value = true
|
|
335
|
+
try {
|
|
336
|
+
await enhancements.onSubmit(submitData as T)
|
|
337
|
+
} finally {
|
|
338
|
+
form.isSubmitting.value = false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// -- Reset -----------------------------------------------------------------
|
|
344
|
+
function reset(values?: Partial<T>): void {
|
|
345
|
+
form.reset(values)
|
|
346
|
+
currentStep.value = 0
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// -- onFieldChange wiring --------------------------------------------------
|
|
350
|
+
if (enhancements?.onFieldChange) {
|
|
351
|
+
const callback = enhancements.onFieldChange
|
|
352
|
+
watch(
|
|
353
|
+
() => ({ ...(form.data as Record<string, unknown>) }),
|
|
354
|
+
(newData, oldData) => {
|
|
355
|
+
if (!oldData) return
|
|
356
|
+
for (const key of Object.keys(newData)) {
|
|
357
|
+
if (newData[key] !== oldData[key]) {
|
|
358
|
+
callback(key as keyof T, newData[key], newData as T)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{ deep: true },
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
form: form as UseFormReturn<T>,
|
|
368
|
+
rules: rules as Partial<Record<keyof T, FieldRules>>,
|
|
369
|
+
isFieldVisible,
|
|
370
|
+
isSectionVisible,
|
|
371
|
+
fields,
|
|
372
|
+
getResolvedFieldProps,
|
|
373
|
+
getFieldOptions,
|
|
374
|
+
currentStep,
|
|
375
|
+
isCurrentStepValid,
|
|
376
|
+
goNext,
|
|
377
|
+
goBack,
|
|
378
|
+
goToStep,
|
|
379
|
+
validate,
|
|
380
|
+
reset,
|
|
381
|
+
submit,
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { useAuthStore } from '../stores/auth'
|
|
3
|
+
import { useSettingsStore } from '../stores/settings'
|
|
4
|
+
import type { CredentialInfo } from '../types'
|
|
5
|
+
|
|
6
|
+
// Lazy-load @simplewebauthn/browser so plugins that never call usePasskey
|
|
7
|
+
// don't need the optional peer dep installed (avoids build failures).
|
|
8
|
+
async function loadWebAuthn() {
|
|
9
|
+
try {
|
|
10
|
+
return await import('@simplewebauthn/browser')
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'@simplewebauthn/browser is required for passkey support. Install it: bun add @simplewebauthn/browser',
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PasskeyLoginResponse {
|
|
19
|
+
access_token: string
|
|
20
|
+
token_type: string
|
|
21
|
+
expires_in: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Registers and authenticates passkeys using WebAuthn, lazily loading the browser dependency. */
|
|
25
|
+
export function usePasskey() {
|
|
26
|
+
const authStore = useAuthStore()
|
|
27
|
+
const settingsStore = useSettingsStore()
|
|
28
|
+
|
|
29
|
+
function getApiBaseUrl(): string {
|
|
30
|
+
return settingsStore.getApiBaseUrl()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function isSupported(): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
const { browserSupportsWebAuthn } = await loadWebAuthn()
|
|
36
|
+
return browserSupportsWebAuthn()
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function registerPasskey(deviceName?: string): Promise<boolean> {
|
|
43
|
+
const webauthn = await loadWebAuthn()
|
|
44
|
+
if (!webauthn.browserSupportsWebAuthn()) {
|
|
45
|
+
authStore.setError('WebAuthn is not supported in this browser')
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
authStore.setLoading(true)
|
|
50
|
+
authStore.setError(null)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const optionsResponse = await axios.get<{ options: string }>(
|
|
54
|
+
`${getApiBaseUrl()}/auth/passkey/register/options`,
|
|
55
|
+
{
|
|
56
|
+
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
57
|
+
withCredentials: true,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const options = JSON.parse(optionsResponse.data.options)
|
|
62
|
+
|
|
63
|
+
const credential = await webauthn.startRegistration(options)
|
|
64
|
+
|
|
65
|
+
await axios.post(
|
|
66
|
+
`${getApiBaseUrl()}/auth/passkey/register/verify`,
|
|
67
|
+
{
|
|
68
|
+
credential: JSON.stringify(credential),
|
|
69
|
+
device_name: deviceName,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
73
|
+
withCredentials: true,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
authStore.setAuthConfig({
|
|
78
|
+
...authStore.authConfig,
|
|
79
|
+
passkeyRegistered: true,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
85
|
+
authStore.setError(error.response.data.detail || 'Passkey registration failed')
|
|
86
|
+
} else if (error instanceof Error) {
|
|
87
|
+
if (error.name === 'NotAllowedError') {
|
|
88
|
+
authStore.setError('Registration was cancelled or timed out')
|
|
89
|
+
} else if (error.name === 'InvalidStateError') {
|
|
90
|
+
authStore.setError('This authenticator is already registered')
|
|
91
|
+
} else {
|
|
92
|
+
authStore.setError(error.message)
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
authStore.setError('Passkey registration failed')
|
|
96
|
+
}
|
|
97
|
+
return false
|
|
98
|
+
} finally {
|
|
99
|
+
authStore.setLoading(false)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loginWithPasskey(): Promise<boolean> {
|
|
104
|
+
const webauthn = await loadWebAuthn()
|
|
105
|
+
if (!webauthn.browserSupportsWebAuthn()) {
|
|
106
|
+
authStore.setError('WebAuthn is not supported in this browser')
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
authStore.setLoading(true)
|
|
111
|
+
authStore.setError(null)
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const optionsResponse = await axios.get<{ options: string }>(
|
|
115
|
+
`${getApiBaseUrl()}/auth/passkey/login/options`,
|
|
116
|
+
{ withCredentials: true }
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const options = JSON.parse(optionsResponse.data.options)
|
|
120
|
+
|
|
121
|
+
const credential = await webauthn.startAuthentication(options)
|
|
122
|
+
|
|
123
|
+
const response = await axios.post<PasskeyLoginResponse>(
|
|
124
|
+
`${getApiBaseUrl()}/auth/passkey/login/verify`,
|
|
125
|
+
{ credential: JSON.stringify(credential) },
|
|
126
|
+
{ withCredentials: true }
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
authStore.setToken(response.data.access_token, response.data.expires_in)
|
|
130
|
+
|
|
131
|
+
return true
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
134
|
+
if (error.response.status === 404) {
|
|
135
|
+
authStore.setError('No passkeys registered. Please login with password first.')
|
|
136
|
+
} else {
|
|
137
|
+
authStore.setError(error.response.data.detail || 'Passkey login failed')
|
|
138
|
+
}
|
|
139
|
+
} else if (error instanceof Error) {
|
|
140
|
+
if (error.name === 'NotAllowedError') {
|
|
141
|
+
authStore.setError('Authentication was cancelled or timed out')
|
|
142
|
+
} else {
|
|
143
|
+
authStore.setError(error.message)
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
authStore.setError('Passkey login failed')
|
|
147
|
+
}
|
|
148
|
+
return false
|
|
149
|
+
} finally {
|
|
150
|
+
authStore.setLoading(false)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function listCredentials(): Promise<CredentialInfo[]> {
|
|
155
|
+
try {
|
|
156
|
+
const response = await axios.get<{ credentials: CredentialInfo[] }>(
|
|
157
|
+
`${getApiBaseUrl()}/auth/passkey/credentials`,
|
|
158
|
+
{
|
|
159
|
+
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
return response.data.credentials
|
|
163
|
+
} catch {
|
|
164
|
+
return []
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function deleteCredential(credentialId: string): Promise<boolean> {
|
|
169
|
+
try {
|
|
170
|
+
await axios.delete(
|
|
171
|
+
`${getApiBaseUrl()}/auth/passkey/credentials/${encodeURIComponent(credentialId)}`,
|
|
172
|
+
{
|
|
173
|
+
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const remaining = await listCredentials()
|
|
178
|
+
if (remaining.length === 0) {
|
|
179
|
+
authStore.setAuthConfig({
|
|
180
|
+
...authStore.authConfig,
|
|
181
|
+
passkeyRegistered: false,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true
|
|
186
|
+
} catch {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function deleteAllCredentials(): Promise<boolean> {
|
|
192
|
+
try {
|
|
193
|
+
await axios.delete(`${getApiBaseUrl()}/auth/passkey/credentials`, {
|
|
194
|
+
headers: { Authorization: `Bearer ${authStore.token}` },
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
authStore.setAuthConfig({
|
|
198
|
+
...authStore.authConfig,
|
|
199
|
+
passkeyRegistered: false,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return true
|
|
203
|
+
} catch {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
isSupported,
|
|
210
|
+
registerPasskey,
|
|
211
|
+
loginWithPasskey,
|
|
212
|
+
listCredentials,
|
|
213
|
+
deleteCredential,
|
|
214
|
+
deleteAllCredentials,
|
|
215
|
+
}
|
|
216
|
+
}
|