@morscherlab/mint-sdk 1.0.0-beta.2 → 1.0.0-beta.3
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 +218 -6
- package/dist/__tests__/components/ActionItem.test.d.ts +1 -0
- package/dist/__tests__/components/AppAvatarMenu.test.d.ts +1 -0
- package/dist/__tests__/components/AppPageSelector.test.d.ts +1 -0
- package/dist/__tests__/components/AppPillNav.test.d.ts +1 -0
- package/dist/__tests__/components/AppPluginSwitcher.test.d.ts +1 -0
- package/dist/__tests__/components/AppToastContainer.test.d.ts +1 -0
- package/dist/__tests__/components/BaseRadioGroup.test.d.ts +1 -0
- package/dist/__tests__/components/BaseSelect.test.d.ts +1 -0
- package/dist/__tests__/components/BaseTabs.test.d.ts +1 -0
- package/dist/__tests__/components/BatchProgressList.test.d.ts +1 -0
- package/dist/__tests__/components/BioTemplateExperimentWorkspaceView.test.d.ts +1 -0
- package/dist/__tests__/components/BioTemplatePackWorkspaceView.test.d.ts +1 -0
- package/dist/__tests__/components/BioTemplatePresetWorkspaceView.test.d.ts +1 -0
- package/dist/__tests__/components/BioTemplateRenderer.test.d.ts +1 -0
- package/dist/__tests__/components/Breadcrumb.test.d.ts +1 -0
- package/dist/__tests__/components/CalendarGridPanel.test.d.ts +1 -0
- package/dist/__tests__/components/ConcentrationInput.test.d.ts +1 -0
- package/dist/__tests__/components/ControlWorkspaceView.test.d.ts +1 -0
- package/dist/__tests__/components/DatePicker.test.d.ts +1 -0
- package/dist/__tests__/components/DateTimePicker.test.d.ts +1 -0
- package/dist/__tests__/components/EmptyState.test.d.ts +1 -0
- package/dist/__tests__/components/ExperimentPopover.test.d.ts +1 -0
- package/dist/__tests__/components/FormBuilder.test.d.ts +1 -0
- package/dist/__tests__/components/FormCompatibility.test.d.ts +1 -0
- package/dist/__tests__/components/GroupAssigner.test.d.ts +1 -0
- package/dist/__tests__/components/GroupingModal.test.d.ts +1 -0
- package/dist/__tests__/components/MultiSelect.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepEditor.test.d.ts +1 -0
- package/dist/__tests__/components/ReagentList.test.d.ts +1 -0
- package/dist/__tests__/components/SampleHierarchyTree.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.test.d.ts +1 -0
- package/dist/__tests__/components/SegmentedControl.test.d.ts +1 -0
- package/dist/__tests__/components/SettingsButton.test.d.ts +1 -0
- package/dist/__tests__/components/SettingsModal.test.d.ts +1 -0
- package/dist/__tests__/components/TagsInput.test.d.ts +1 -0
- package/dist/__tests__/components/ThemeToggle.test.d.ts +1 -0
- package/dist/__tests__/components/TimePicker.test.d.ts +1 -0
- package/dist/__tests__/composables/useBioTemplatePackWorkspace.test.d.ts +1 -0
- package/dist/__tests__/composables/useBioTemplatePresetWorkspace.test.d.ts +1 -0
- package/dist/__tests__/composables/useBioTemplateWorkspace.test.d.ts +1 -0
- package/dist/__tests__/composables/useCalendarGrid.test.d.ts +1 -0
- package/dist/__tests__/composables/useControlSchema.test.d.ts +1 -0
- package/dist/__tests__/composables/useDebouncedWatch.test.d.ts +1 -0
- package/dist/__tests__/composables/useDropdownState.test.d.ts +1 -0
- package/dist/__tests__/composables/useEventListener.test.d.ts +1 -0
- package/dist/__tests__/composables/useExpansionSet.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentData.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentSelector.test.d.ts +1 -0
- package/dist/__tests__/composables/useGroupAssignment.test.d.ts +1 -0
- package/dist/__tests__/composables/useListSelection.test.d.ts +1 -0
- package/dist/__tests__/composables/usePluginClient.test.d.ts +1 -0
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +1 -0
- package/dist/__tests__/composables/useRequestSyncState.test.d.ts +1 -0
- package/dist/__tests__/composables/useSampleGroups.test.d.ts +1 -0
- package/dist/__tests__/composables/useSelectionLimit.test.d.ts +1 -0
- package/dist/__tests__/composables/useSortedItems.test.d.ts +1 -0
- package/dist/__tests__/composables/useTemplateCollection.test.d.ts +1 -0
- package/dist/__tests__/composables/useTextSearch.test.d.ts +1 -0
- package/dist/__tests__/composables/useTheme.test.d.ts +1 -0
- package/dist/__tests__/composables/useTimeUtils.test.d.ts +1 -0
- package/dist/__tests__/docs/frontendDocsCatalog.test.d.ts +1 -0
- package/dist/__tests__/templates/templates.test.d.ts +1 -0
- package/dist/{auth-DsI0rQ7_.js → auth-QQj2kkze.js} +12 -5
- package/dist/auth-QQj2kkze.js.map +1 -0
- package/dist/components/ActionItem.vue.d.ts +32 -0
- package/dist/components/AppAvatarMenu.vue.d.ts +2 -7
- package/dist/components/AppPageSelector.vue.d.ts +3 -6
- package/dist/components/AppPillNav.vue.d.ts +2 -2
- package/dist/components/AppSidebar.vue.d.ts +56 -3
- package/dist/components/AppToastContainer.vue.d.ts +2 -0
- package/dist/components/AppTopBar.vue.d.ts +41 -10
- package/dist/components/BaseButton.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseInput.vue.d.ts +2 -2
- package/dist/components/BasePill.vue.d.ts +2 -2
- package/dist/components/BaseRadioGroup.vue.d.ts +3 -3
- package/dist/components/BaseSelect.vue.d.ts +3 -3
- package/dist/components/BaseTabs.vue.d.ts +2 -2
- package/dist/components/BaseTextarea.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +117 -0
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +92 -0
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +82 -0
- package/dist/components/BioTemplateRenderer.vue.d.ts +29 -0
- package/dist/components/Breadcrumb.vue.d.ts +2 -2
- package/dist/components/Calendar.vue.d.ts +1 -1
- package/dist/components/CalendarGridPanel.vue.d.ts +25 -0
- package/dist/components/CollapsibleCard.vue.d.ts +1 -1
- package/dist/components/ConcentrationInput.vue.d.ts +2 -2
- package/dist/components/ConfirmDialog.vue.d.ts +2 -2
- package/dist/components/ControlWorkspaceView.vue.d.ts +130 -0
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +3 -3
- package/dist/components/Divider.vue.d.ts +1 -1
- package/dist/components/DropdownButton.vue.d.ts +3 -3
- package/dist/components/EmptyState.vue.d.ts +1 -2
- package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
- package/dist/components/ExperimentTimeline.vue.d.ts +2 -2
- package/dist/components/FileUploader.vue.d.ts +1 -1
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/FormActions.vue.d.ts +4 -4
- package/dist/components/FormBuilder.vue.d.ts +22 -8
- package/dist/components/FormFieldRenderer.vue.d.ts +7 -10
- package/dist/components/FormSection.vue.d.ts +11 -24
- package/dist/components/FormulaInput.vue.d.ts +2 -2
- package/dist/components/MoleculeInput.vue.d.ts +2 -2
- package/dist/components/MultiSelect.vue.d.ts +3 -3
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStepEditor.vue.d.ts +3 -1
- package/dist/components/RackEditor.vue.d.ts +2 -2
- package/dist/components/SampleLegend.vue.d.ts +2 -2
- package/dist/components/ScheduleCalendar.vue.d.ts +2 -2
- package/dist/components/SegmentedControl.vue.d.ts +2 -2
- package/dist/components/SequenceInput.vue.d.ts +3 -3
- package/dist/components/SettingsButton.vue.d.ts +2 -2
- package/dist/components/SettingsModal.vue.d.ts +13 -5
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +3 -2
- package/dist/components/TimePicker.vue.d.ts +3 -3
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +2 -2
- package/dist/components/WellPlate.vue.d.ts +8 -8
- package/dist/components/index.d.ts +11 -1
- package/dist/components/index.js +3 -3
- package/dist/components/internal/FormFieldRendererInternal.vue.d.ts +31 -0
- package/dist/components/internal/FormSectionRenderer.vue.d.ts +43 -0
- package/dist/{components-_XqPEhP9.js → components-D_Sr0adg.js} +7290 -6518
- package/dist/components-D_Sr0adg.js.map +1 -0
- package/dist/composables/index.d.ts +21 -2
- package/dist/composables/index.js +4 -3
- package/dist/composables/platformContextHelpers.d.ts +14 -0
- package/dist/composables/useBioTemplateComponents.d.ts +20 -0
- package/dist/composables/useBioTemplateControls.d.ts +6 -0
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +45 -0
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +74 -0
- package/dist/composables/useBioTemplateWorkspace.d.ts +50 -0
- package/dist/composables/useCalendarGrid.d.ts +26 -0
- package/dist/composables/useControlSchema.d.ts +321 -0
- package/dist/composables/useDebouncedWatch.d.ts +20 -0
- package/dist/composables/useDropdownState.d.ts +19 -0
- package/dist/composables/useEventListener.d.ts +13 -0
- package/dist/composables/useExpansionSet.d.ts +21 -0
- package/dist/composables/useExperimentData.d.ts +10 -0
- package/dist/composables/useExperimentSave.d.ts +31 -2
- package/dist/composables/useExperimentSelector.d.ts +20 -0
- package/dist/composables/useForm.d.ts +2 -0
- package/dist/composables/useGroupAssignment.d.ts +31 -0
- package/dist/composables/useListSelection.d.ts +35 -0
- package/dist/composables/usePlatformContext.d.ts +21 -3
- package/dist/composables/usePluginApi.d.ts +7 -14
- package/dist/composables/usePluginClient.d.ts +109 -0
- package/dist/composables/usePluginConfig.d.ts +12 -0
- package/dist/composables/useRequestSyncState.d.ts +34 -0
- package/dist/composables/useSampleGroups.d.ts +32 -0
- package/dist/composables/useSelectionLimit.d.ts +17 -0
- package/dist/composables/useSortedItems.d.ts +32 -0
- package/dist/composables/useTemplateCollection.d.ts +58 -0
- package/dist/composables/useTextSearch.d.ts +18 -0
- package/dist/composables/useTimeUtils.d.ts +8 -0
- package/dist/{composables-tiZqLu1M.js → composables-C3dpXQN5.js} +228 -146
- package/dist/composables-C3dpXQN5.js.map +1 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +6 -5
- package/dist/install.d.ts +7 -2
- package/dist/install.js +2 -2
- package/dist/install.js.map +1 -1
- package/dist/stores/index.js +1 -1
- package/dist/stores/settings.d.ts +4 -1
- package/dist/styles.css +5235 -5977
- package/dist/templates/adapters.d.ts +43 -0
- package/dist/templates/builders.d.ts +63 -0
- package/dist/templates/catalog.d.ts +188 -0
- package/dist/templates/componentBindings.d.ts +58 -0
- package/dist/templates/controlSchemas.d.ts +25 -0
- package/dist/templates/index.d.ts +15 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/lookup.d.ts +4 -0
- package/dist/templates/packs.d.ts +18 -0
- package/dist/templates/presets.d.ts +90 -0
- package/dist/templates/types.d.ts +531 -0
- package/dist/templates-50NPjaxL.js +9333 -0
- package/dist/templates-50NPjaxL.js.map +1 -0
- package/dist/types/components.d.ts +26 -4
- package/dist/types/form-builder.d.ts +6 -8
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +7 -1
- package/dist/useScheduleDrag-D4oWdh41.js +4371 -0
- package/dist/useScheduleDrag-D4oWdh41.js.map +1 -0
- package/dist/utils/formModelSync.d.ts +5 -0
- package/dist/utils/items.d.ts +8 -0
- package/dist/utils/options.d.ts +6 -0
- package/dist/utils/pluginIcon.d.ts +9 -0
- package/package.json +7 -2
- package/src/__tests__/components/ActionItem.test.ts +99 -0
- package/src/__tests__/components/AppAvatarMenu.test.ts +27 -0
- package/src/__tests__/components/AppPageSelector.test.ts +134 -0
- package/src/__tests__/components/AppPillNav.test.ts +78 -0
- package/src/__tests__/components/AppPluginSwitcher.test.ts +44 -0
- package/src/__tests__/components/AppSidebar.test.ts +370 -0
- package/src/__tests__/components/AppToastContainer.test.ts +48 -0
- package/src/__tests__/components/AppTopBar.test.ts +383 -0
- package/src/__tests__/components/BaseRadioGroup.test.ts +25 -0
- package/src/__tests__/components/BaseSelect.test.ts +21 -0
- package/src/__tests__/components/BaseTabs.test.ts +25 -0
- package/src/__tests__/components/BatchProgressList.test.ts +52 -0
- package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +153 -0
- package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +161 -0
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +281 -0
- package/src/__tests__/components/BioTemplateRenderer.test.ts +71 -0
- package/src/__tests__/components/Breadcrumb.test.ts +23 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +36 -0
- package/src/__tests__/components/ConcentrationInput.test.ts +45 -0
- package/src/__tests__/components/ControlWorkspaceView.test.ts +1031 -0
- package/src/__tests__/components/DataFrame.test.ts +11 -0
- package/src/__tests__/components/DatePicker.test.ts +45 -0
- package/src/__tests__/components/DateTimePicker.test.ts +48 -0
- package/src/__tests__/components/DropdownButton.test.ts +23 -0
- package/src/__tests__/components/EmptyState.test.ts +23 -0
- package/src/__tests__/components/ExperimentPopover.test.ts +56 -0
- package/src/__tests__/components/FormBuilder.test.ts +296 -0
- package/src/__tests__/components/FormCompatibility.test.ts +94 -0
- package/src/__tests__/components/GroupAssigner.test.ts +30 -0
- package/src/__tests__/components/GroupingModal.test.ts +73 -0
- package/src/__tests__/components/MultiSelect.test.ts +48 -0
- package/src/__tests__/components/ProtocolStepEditor.test.ts +33 -0
- package/src/__tests__/components/ReagentList.test.ts +82 -0
- package/src/__tests__/components/SampleHierarchyTree.test.ts +53 -0
- package/src/__tests__/components/SampleSelector.test.ts +60 -0
- package/src/__tests__/components/SegmentedControl.test.ts +24 -0
- package/src/__tests__/components/SettingsButton.test.ts +44 -0
- package/src/__tests__/components/SettingsModal.test.ts +296 -0
- package/src/__tests__/components/TagsInput.test.ts +75 -0
- package/src/__tests__/components/ThemeToggle.test.ts +47 -0
- package/src/__tests__/components/TimePicker.test.ts +38 -0
- package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +122 -0
- package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +199 -0
- package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +99 -0
- package/src/__tests__/composables/useCalendarGrid.test.ts +38 -0
- package/src/__tests__/composables/useControlSchema.test.ts +919 -0
- package/src/__tests__/composables/useDebouncedWatch.test.ts +93 -0
- package/src/__tests__/composables/useDropdownState.test.ts +95 -0
- package/src/__tests__/composables/useEventListener.test.ts +116 -0
- package/src/__tests__/composables/useExpansionSet.test.ts +62 -0
- package/src/__tests__/composables/useExperimentData.test.ts +4 -0
- package/src/__tests__/composables/useExperimentSave.test.ts +203 -8
- package/src/__tests__/composables/useExperimentSelector.test.ts +164 -0
- package/src/__tests__/composables/useForm.test.ts +58 -0
- package/src/__tests__/composables/useFormBuilder.test.ts +77 -0
- package/src/__tests__/composables/useGroupAssignment.test.ts +73 -0
- package/src/__tests__/composables/useListSelection.test.ts +66 -0
- package/src/__tests__/composables/usePluginClient.test.ts +444 -0
- package/src/__tests__/composables/usePluginConfig.test.ts +5 -0
- package/src/__tests__/composables/useRequestSyncState.test.ts +92 -0
- package/src/__tests__/composables/useSampleGroups.test.ts +66 -0
- package/src/__tests__/composables/useSelectionLimit.test.ts +41 -0
- package/src/__tests__/composables/useSortedItems.test.ts +87 -0
- package/src/__tests__/composables/useTemplateCollection.test.ts +147 -0
- package/src/__tests__/composables/useTextSearch.test.ts +55 -0
- package/src/__tests__/composables/useTheme.test.ts +91 -0
- package/src/__tests__/composables/useTimeUtils.test.ts +35 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +229 -0
- package/src/__tests__/fixtures/templates/dose-response.json +81 -0
- package/src/__tests__/fixtures/templates/plate-map.json +54 -0
- package/src/__tests__/fixtures/templates/qpcr-plate.json +96 -0
- package/src/__tests__/fixtures/templates/sample-sheet.json +71 -0
- package/src/__tests__/templates/templates.test.ts +1043 -0
- package/src/components/ActionItem.vue +82 -0
- package/src/components/AppAvatarMenu.vue +15 -69
- package/src/components/AppLayout.story.vue +25 -25
- package/src/components/AppPageSelector.vue +63 -94
- package/src/components/AppPillNav.vue +44 -39
- package/src/components/AppPluginSwitcher.vue +41 -145
- package/src/components/AppSidebar.story.vue +94 -0
- package/src/components/AppSidebar.vue +187 -12
- package/src/components/{ToastNotification.story.vue → AppToastContainer.story.vue} +6 -6
- package/src/components/AppToastContainer.vue +62 -0
- package/src/components/AppTopBar.story.vue +7 -30
- package/src/components/AppTopBar.vue +251 -57
- package/src/components/BaseModal.vue +3 -5
- package/src/components/BaseRadioGroup.vue +7 -3
- package/src/components/BaseSelect.vue +11 -7
- package/src/components/BaseTabs.vue +6 -4
- package/src/components/BatchProgressList.vue +5 -8
- package/src/components/BioTemplateExperimentWorkspaceView.story.vue +123 -0
- package/src/components/BioTemplateExperimentWorkspaceView.vue +337 -0
- package/src/components/BioTemplatePackWorkspaceView.story.vue +107 -0
- package/src/components/BioTemplatePackWorkspaceView.vue +176 -0
- package/src/components/BioTemplatePresetWorkspaceView.story.vue +151 -0
- package/src/components/BioTemplatePresetWorkspaceView.vue +392 -0
- package/src/components/BioTemplateRenderer.story.vue +57 -0
- package/src/components/BioTemplateRenderer.vue +269 -0
- package/src/components/Breadcrumb.vue +14 -8
- package/src/components/CalendarGridPanel.vue +120 -0
- package/src/components/ConcentrationInput.vue +27 -64
- package/src/components/ControlWorkspaceView.story.vue +336 -0
- package/src/components/ControlWorkspaceView.vue +347 -0
- package/src/components/DataFrame.vue +34 -50
- package/src/components/DatePicker.vue +59 -192
- package/src/components/DateTimePicker.vue +50 -171
- package/src/components/DropdownButton.vue +14 -32
- package/src/components/EmptyState.vue +4 -2
- package/src/components/ExperimentPopover.vue +5 -22
- package/src/components/FormBuilder.vue +124 -27
- package/src/components/FormFieldRenderer.vue +15 -38
- package/src/components/FormSection.vue +20 -73
- package/src/components/GroupAssigner.vue +24 -56
- package/src/components/GroupingModal.story.vue +3 -3
- package/src/components/GroupingModal.vue +30 -391
- package/src/components/MultiSelect.vue +17 -12
- package/src/components/PlateMapEditor.vue +3 -8
- package/src/components/PluginIcon.vue +2 -22
- package/src/components/ProtocolStepEditor.vue +13 -22
- package/src/components/ReagentList.vue +25 -33
- package/src/components/SampleHierarchyTree.vue +12 -23
- package/src/components/SampleSelector.vue +42 -122
- package/src/components/SegmentedControl.vue +7 -3
- package/src/components/SettingsButton.story.vue +1 -1
- package/src/components/SettingsButton.vue +15 -27
- package/src/components/SettingsModal.story.vue +1 -1
- package/src/components/SettingsModal.vue +120 -29
- package/src/components/TagsInput.vue +29 -14
- package/src/components/ThemeToggle.vue +9 -7
- package/src/components/TimePicker.vue +19 -41
- package/src/components/ToastNotification.vue +4 -57
- package/src/components/Tooltip.vue +7 -12
- package/src/components/WellEditPopup.vue +3 -8
- package/src/components/WellPlate.vue +4 -10
- package/src/components/index.ts +11 -1
- package/src/components/internal/FormFieldRendererInternal.vue +50 -0
- package/src/components/internal/FormSectionRenderer.vue +78 -0
- package/src/composables/index.ts +212 -0
- package/src/composables/platformContextHelpers.ts +74 -0
- package/src/composables/useBioTemplateComponents.ts +93 -0
- package/src/composables/useBioTemplateControls.ts +41 -0
- package/src/composables/useBioTemplatePackWorkspace.ts +181 -0
- package/src/composables/useBioTemplatePresetWorkspace.ts +337 -0
- package/src/composables/useBioTemplateWorkspace.ts +139 -0
- package/src/composables/useCalendarGrid.ts +140 -0
- package/src/composables/useControlSchema.ts +1274 -0
- package/src/composables/useDebouncedWatch.ts +119 -0
- package/src/composables/useDropdownState.ts +83 -0
- package/src/composables/useEventListener.ts +111 -0
- package/src/composables/useExpansionSet.ts +117 -0
- package/src/composables/useExperimentData.ts +20 -11
- package/src/composables/useExperimentSave.ts +202 -50
- package/src/composables/useExperimentSelector.ts +86 -72
- package/src/composables/useForm.ts +49 -4
- package/src/composables/useFormBuilder.ts +93 -42
- package/src/composables/useGroupAssignment.ts +148 -0
- package/src/composables/useListSelection.ts +158 -0
- package/src/composables/usePluginApi.ts +7 -14
- package/src/composables/usePluginClient.ts +425 -0
- package/src/composables/usePluginConfig.ts +34 -13
- package/src/composables/useRequestSyncState.ts +126 -0
- package/src/composables/useSampleGroups.ts +126 -0
- package/src/composables/useSelectionLimit.ts +57 -0
- package/src/composables/useSortedItems.ts +118 -0
- package/src/composables/useTemplateCollection.ts +229 -0
- package/src/composables/useTextSearch.ts +60 -0
- package/src/composables/useTheme.ts +2 -28
- package/src/composables/useTimeUtils.ts +26 -2
- package/src/composables/useWellPlateEditor.ts +13 -9
- package/src/index.ts +224 -4
- package/src/install.ts +11 -4
- package/src/stores/settings.ts +13 -9
- package/src/styles/components/app-page-selector.css +23 -0
- package/src/styles/components/app-pill-nav.css +7 -0
- package/src/styles/components/app-top-bar.css +34 -0
- package/src/styles/components/concentration-input.css +3 -142
- package/src/styles/components/empty-state.css +0 -16
- package/src/styles/components/settings-button.css +3 -66
- package/src/styles/components/theme-toggle.css +3 -66
- package/src/styles/index.css +0 -1
- package/src/templates/adapters.ts +785 -0
- package/src/templates/builders.ts +2149 -0
- package/src/templates/catalog.ts +245 -0
- package/src/templates/componentBindings.ts +615 -0
- package/src/templates/controlSchemas.ts +718 -0
- package/src/templates/index.ts +314 -0
- package/src/templates/lookup.ts +18 -0
- package/src/templates/packs.ts +156 -0
- package/src/templates/presets.ts +146 -0
- package/src/templates/types.ts +668 -0
- package/src/types/components.ts +41 -4
- package/src/types/form-builder.ts +7 -2
- package/src/types/index.ts +14 -0
- package/src/types/platform.ts +7 -1
- package/src/utils/formModelSync.ts +52 -0
- package/src/utils/items.ts +28 -0
- package/src/utils/options.ts +23 -0
- package/src/utils/pluginIcon.ts +30 -0
- package/dist/auth-DsI0rQ7_.js.map +0 -1
- package/dist/components-_XqPEhP9.js.map +0 -1
- package/dist/composables-tiZqLu1M.js.map +0 -1
- package/dist/useScheduleDrag-CA9sGNJG.js +0 -7181
- package/dist/useScheduleDrag-CA9sGNJG.js.map +0 -1
- package/src/styles/components/grouping-modal.css +0 -323
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { computed, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import type { SampleGroup } from '../types'
|
|
3
|
+
import { deriveShade } from '../utils/color'
|
|
4
|
+
|
|
5
|
+
export type SampleGroupSource<T> =
|
|
6
|
+
| T
|
|
7
|
+
| Ref<T>
|
|
8
|
+
| ComputedRef<T>
|
|
9
|
+
| (() => T)
|
|
10
|
+
|
|
11
|
+
export interface DisplaySampleSubGroup extends SampleGroup {
|
|
12
|
+
displayColor: string
|
|
13
|
+
displayBg: string
|
|
14
|
+
displayBorder: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SampleMajorGroup {
|
|
18
|
+
name: string
|
|
19
|
+
color: string
|
|
20
|
+
subGroups: DisplaySampleSubGroup[]
|
|
21
|
+
allSamples: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseSampleGroupsOptions {
|
|
25
|
+
groups: SampleGroupSource<readonly SampleGroup[]>
|
|
26
|
+
samples?: SampleGroupSource<readonly string[]>
|
|
27
|
+
separator?: SampleGroupSource<string | null | undefined>
|
|
28
|
+
fallbackColor?: SampleGroupSource<string | null | undefined>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UseSampleGroupsReturn {
|
|
32
|
+
groups: ComputedRef<SampleGroup[]>
|
|
33
|
+
separator: ComputedRef<string>
|
|
34
|
+
hierarchicalGroups: ComputedRef<SampleMajorGroup[]>
|
|
35
|
+
showHierarchy: ComputedRef<boolean>
|
|
36
|
+
groupedSamples: ComputedRef<Set<string>>
|
|
37
|
+
ungroupedSamples: ComputedRef<string[]>
|
|
38
|
+
findGroup: (groupName: string) => SampleGroup | undefined
|
|
39
|
+
getGroupColor: (groupName: string) => string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Shared sample-group hierarchy, color, and ungrouped-sample derivation for lab selectors. */
|
|
43
|
+
export function useSampleGroups(options: UseSampleGroupsOptions): UseSampleGroupsReturn {
|
|
44
|
+
const groups = computed(() => [...toValue(options.groups)])
|
|
45
|
+
const samples = computed(() => [...(toValue(options.samples) ?? [])])
|
|
46
|
+
const fallbackColor = computed(() => toValue(options.fallbackColor) || '#3B82F6')
|
|
47
|
+
const separator = computed(() => {
|
|
48
|
+
const explicit = toValue(options.separator)
|
|
49
|
+
if (explicit) return explicit
|
|
50
|
+
return groups.value.some(group => group.name.includes('/')) ? '/' : '_'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const hierarchicalGroups = computed<SampleMajorGroup[]>(() => {
|
|
54
|
+
if (groups.value.length === 0) return []
|
|
55
|
+
const groupedByMajor = new Map<string, SampleGroup[]>()
|
|
56
|
+
|
|
57
|
+
for (const group of groups.value) {
|
|
58
|
+
const majorName = getMajorGroupName(group.name, separator.value)
|
|
59
|
+
const existing = groupedByMajor.get(majorName)
|
|
60
|
+
if (existing) {
|
|
61
|
+
existing.push(group)
|
|
62
|
+
} else {
|
|
63
|
+
groupedByMajor.set(majorName, [group])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [...groupedByMajor.entries()].map(([majorName, subGroups]) => {
|
|
68
|
+
const color = subGroups[0]?.color || fallbackColor.value
|
|
69
|
+
const displaySubs = subGroups.map((subGroup, index) => {
|
|
70
|
+
const displayColor = deriveShade(color, index, subGroups.length)
|
|
71
|
+
return {
|
|
72
|
+
...subGroup,
|
|
73
|
+
displayColor,
|
|
74
|
+
displayBg: displayColor + '20',
|
|
75
|
+
displayBorder: displayColor + '40',
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name: majorName,
|
|
81
|
+
color,
|
|
82
|
+
subGroups: displaySubs,
|
|
83
|
+
allSamples: subGroups.flatMap(group => group.samples),
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const showHierarchy = computed(() =>
|
|
89
|
+
hierarchicalGroups.value.some(major =>
|
|
90
|
+
major.subGroups.length > 1 ||
|
|
91
|
+
(major.subGroups.length === 1 && major.name !== major.subGroups[0].name)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const groupedSamples = computed(() =>
|
|
96
|
+
new Set(groups.value.flatMap(group => group.samples))
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const ungroupedSamples = computed(() =>
|
|
100
|
+
samples.value.filter(sample => !groupedSamples.value.has(sample))
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
function findGroup(groupName: string): SampleGroup | undefined {
|
|
104
|
+
return groups.value.find(group => group.name === groupName)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getGroupColor(groupName: string): string {
|
|
108
|
+
return findGroup(groupName)?.color || fallbackColor.value
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
groups,
|
|
113
|
+
separator,
|
|
114
|
+
hierarchicalGroups,
|
|
115
|
+
showHierarchy,
|
|
116
|
+
groupedSamples,
|
|
117
|
+
ungroupedSamples,
|
|
118
|
+
findGroup,
|
|
119
|
+
getGroupColor,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getMajorGroupName(groupName: string, separator: string): string {
|
|
124
|
+
const parts = groupName.split(separator)
|
|
125
|
+
return parts.length > 1 ? parts[0] : groupName
|
|
126
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { computed, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type SelectionLimitSource<T> =
|
|
4
|
+
| T
|
|
5
|
+
| Ref<T>
|
|
6
|
+
| ComputedRef<T>
|
|
7
|
+
| (() => T)
|
|
8
|
+
|
|
9
|
+
export interface UseSelectionLimitOptions {
|
|
10
|
+
count: SelectionLimitSource<number>
|
|
11
|
+
max?: SelectionLimitSource<number | null | undefined>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseSelectionLimitReturn {
|
|
15
|
+
count: ComputedRef<number>
|
|
16
|
+
max: ComputedRef<number | undefined>
|
|
17
|
+
isLimited: ComputedRef<boolean>
|
|
18
|
+
isAtLimit: ComputedRef<boolean>
|
|
19
|
+
canAddMore: ComputedRef<boolean>
|
|
20
|
+
remaining: ComputedRef<number | undefined>
|
|
21
|
+
canAdd: (amount?: number, fromCount?: number) => boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Shared cap logic for chip, tag, and multi-select controls. */
|
|
25
|
+
export function useSelectionLimit(options: UseSelectionLimitOptions): UseSelectionLimitReturn {
|
|
26
|
+
const count = computed(() => Math.max(0, Math.floor(toValue(options.count) || 0)))
|
|
27
|
+
const max = computed(() => normalizeMax(toValue(options.max)))
|
|
28
|
+
const isLimited = computed(() => max.value !== undefined)
|
|
29
|
+
const remaining = computed(() => {
|
|
30
|
+
if (max.value === undefined) return undefined
|
|
31
|
+
return Math.max(0, max.value - count.value)
|
|
32
|
+
})
|
|
33
|
+
const isAtLimit = computed(() => remaining.value === 0)
|
|
34
|
+
const canAddMore = computed(() => canAdd(1))
|
|
35
|
+
|
|
36
|
+
function canAdd(amount = 1, fromCount = count.value): boolean {
|
|
37
|
+
const normalizedAmount = Math.max(0, Math.floor(amount))
|
|
38
|
+
const normalizedCount = Math.max(0, Math.floor(fromCount))
|
|
39
|
+
if (max.value === undefined) return true
|
|
40
|
+
return normalizedCount + normalizedAmount <= max.value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
count,
|
|
45
|
+
max,
|
|
46
|
+
isLimited,
|
|
47
|
+
isAtLimit,
|
|
48
|
+
canAddMore,
|
|
49
|
+
remaining,
|
|
50
|
+
canAdd,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeMax(value: number | null | undefined): number | undefined {
|
|
55
|
+
if (value === null || value === undefined || !Number.isFinite(value)) return undefined
|
|
56
|
+
return Math.max(0, Math.floor(value))
|
|
57
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { computed, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type SortedItemsSource<T> =
|
|
4
|
+
| T
|
|
5
|
+
| Ref<T>
|
|
6
|
+
| ComputedRef<T>
|
|
7
|
+
| (() => T)
|
|
8
|
+
|
|
9
|
+
export type SortOrder = 'asc' | 'desc'
|
|
10
|
+
|
|
11
|
+
export interface SortDescriptor<TKey extends string = string> {
|
|
12
|
+
key: TKey
|
|
13
|
+
direction?: SortOrder | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CompareSortValuesOptions {
|
|
17
|
+
caseSensitive?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SortComparatorContext<TItem, TKey extends string = string> {
|
|
21
|
+
key: TKey
|
|
22
|
+
direction: SortOrder
|
|
23
|
+
a: TItem
|
|
24
|
+
b: TItem
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SortComparator<TItem, TKey extends string = string> = (
|
|
28
|
+
aValue: unknown,
|
|
29
|
+
bValue: unknown,
|
|
30
|
+
context: SortComparatorContext<TItem, TKey>,
|
|
31
|
+
) => number
|
|
32
|
+
|
|
33
|
+
export interface UseSortedItemsOptions<TItem, TKey extends string = string> {
|
|
34
|
+
items: SortedItemsSource<readonly TItem[]>
|
|
35
|
+
sort: SortedItemsSource<SortDescriptor<TKey> | null | undefined>
|
|
36
|
+
enabled?: SortedItemsSource<boolean | null | undefined>
|
|
37
|
+
caseSensitive?: SortedItemsSource<boolean | null | undefined>
|
|
38
|
+
getValue: (item: TItem, key: TKey) => unknown
|
|
39
|
+
compare?: SortComparator<TItem, TKey>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseSortedItemsReturn<TItem, TKey extends string = string> {
|
|
43
|
+
sort: ComputedRef<SortDescriptor<TKey> | null | undefined>
|
|
44
|
+
sortedItems: ComputedRef<TItem[]>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Shared sorting for SDK tables and lists with stable empty-value handling. */
|
|
48
|
+
export function useSortedItems<TItem, TKey extends string = string>(
|
|
49
|
+
options: UseSortedItemsOptions<TItem, TKey>,
|
|
50
|
+
): UseSortedItemsReturn<TItem, TKey> {
|
|
51
|
+
const sort = computed(() => toValue(options.sort))
|
|
52
|
+
const enabled = computed(() => toValue(options.enabled) ?? true)
|
|
53
|
+
|
|
54
|
+
const sortedItems = computed(() => {
|
|
55
|
+
const items = [...toValue(options.items)]
|
|
56
|
+
const activeSort = sort.value
|
|
57
|
+
|
|
58
|
+
if (!enabled.value || !activeSort?.key || !activeSort.direction) return items
|
|
59
|
+
|
|
60
|
+
const direction = activeSort.direction
|
|
61
|
+
const caseSensitive = toValue(options.caseSensitive) ?? true
|
|
62
|
+
|
|
63
|
+
return items.sort((a, b) => {
|
|
64
|
+
const aValue = options.getValue(a, activeSort.key)
|
|
65
|
+
const bValue = options.getValue(b, activeSort.key)
|
|
66
|
+
|
|
67
|
+
if (options.compare) {
|
|
68
|
+
return options.compare(aValue, bValue, {
|
|
69
|
+
key: activeSort.key,
|
|
70
|
+
direction,
|
|
71
|
+
a,
|
|
72
|
+
b,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return compareSortValues(aValue, bValue, direction, { caseSensitive })
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
sort,
|
|
82
|
+
sortedItems,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function compareSortValues(
|
|
87
|
+
aValue: unknown,
|
|
88
|
+
bValue: unknown,
|
|
89
|
+
direction: SortOrder = 'asc',
|
|
90
|
+
options: CompareSortValuesOptions = {},
|
|
91
|
+
): number {
|
|
92
|
+
const aEmpty = aValue === null || aValue === undefined
|
|
93
|
+
const bEmpty = bValue === null || bValue === undefined
|
|
94
|
+
|
|
95
|
+
if (aEmpty && bEmpty) return 0
|
|
96
|
+
if (aEmpty) return 1
|
|
97
|
+
if (bEmpty) return -1
|
|
98
|
+
|
|
99
|
+
const aComparable = toComparableSortValue(aValue, options)
|
|
100
|
+
const bComparable = toComparableSortValue(bValue, options)
|
|
101
|
+
|
|
102
|
+
let comparison = 0
|
|
103
|
+
if (typeof aComparable === 'number' && typeof bComparable === 'number') {
|
|
104
|
+
comparison = aComparable - bComparable
|
|
105
|
+
} else {
|
|
106
|
+
comparison = String(aComparable).localeCompare(String(bComparable))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return direction === 'asc' ? comparison : -comparison
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toComparableSortValue(value: unknown, options: CompareSortValuesOptions): unknown {
|
|
113
|
+
if (value instanceof Date) return value.getTime()
|
|
114
|
+
if (typeof value === 'string' && options.caseSensitive === false) {
|
|
115
|
+
return value.toLowerCase()
|
|
116
|
+
}
|
|
117
|
+
return value
|
|
118
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import { useExperimentSave, type UseExperimentSaveOptions } from './useExperimentSave'
|
|
3
|
+
import {
|
|
4
|
+
createTemplateCollection,
|
|
5
|
+
ensureTemplateFromCollection,
|
|
6
|
+
extractTemplateCollection,
|
|
7
|
+
type BioTemplateEnvelope,
|
|
8
|
+
type TemplateCollection,
|
|
9
|
+
type TemplateCollectionEnvelope,
|
|
10
|
+
} from '../templates'
|
|
11
|
+
|
|
12
|
+
export type TemplateCollectionInput =
|
|
13
|
+
| TemplateCollectionEnvelope
|
|
14
|
+
| BioTemplateEnvelope<unknown>
|
|
15
|
+
| Array<BioTemplateEnvelope<unknown>>
|
|
16
|
+
|
|
17
|
+
export interface UseTemplateCollectionOptions extends UseExperimentSaveOptions {
|
|
18
|
+
/** Initial template collection or factory; defaults to an empty collection. */
|
|
19
|
+
initial?: TemplateCollectionInput | (() => TemplateCollectionInput)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseTemplateCollectionReturn {
|
|
23
|
+
/** Current experiment id from platform injection, URL query, or URL path. */
|
|
24
|
+
currentExperimentId: ComputedRef<number | undefined>
|
|
25
|
+
/** Whether a current experiment id is available. */
|
|
26
|
+
hasCurrentExperiment: ComputedRef<boolean>
|
|
27
|
+
/** Full template collection envelope ready to save into design_data. */
|
|
28
|
+
templateCollection: Ref<TemplateCollectionEnvelope>
|
|
29
|
+
/** Template map keyed by template_id. */
|
|
30
|
+
templates: ComputedRef<TemplateCollection>
|
|
31
|
+
/** Collection-level metadata, or an empty object. */
|
|
32
|
+
metadata: ComputedRef<Record<string, unknown>>
|
|
33
|
+
/** Template ids currently present in the collection. */
|
|
34
|
+
templateIds: ComputedRef<string[]>
|
|
35
|
+
/** Whether the collection contains at least one template. */
|
|
36
|
+
hasTemplates: ComputedRef<boolean>
|
|
37
|
+
/** Whether a load or delete operation is in progress. */
|
|
38
|
+
isLoading: Ref<boolean>
|
|
39
|
+
/** Whether a save operation is in progress. */
|
|
40
|
+
isSaving: Ref<boolean>
|
|
41
|
+
/** Error message from the last failed operation, or null. */
|
|
42
|
+
error: Ref<string | null>
|
|
43
|
+
/** Timestamp of the last successful load, or null. */
|
|
44
|
+
lastLoadedAt: Ref<Date | null>
|
|
45
|
+
/** Timestamp of the last successful save, or null. */
|
|
46
|
+
lastSavedAt: Ref<Date | null>
|
|
47
|
+
/** Replace local state from a template envelope, collection envelope, or raw design_data. */
|
|
48
|
+
apply: (value: unknown, metadata?: Record<string, unknown>) => TemplateCollectionEnvelope
|
|
49
|
+
/** Reset local state to the configured initial collection. */
|
|
50
|
+
reset: () => TemplateCollectionEnvelope
|
|
51
|
+
/** Add or replace one template in the local collection. */
|
|
52
|
+
setTemplate: (template: BioTemplateEnvelope<unknown>) => TemplateCollectionEnvelope
|
|
53
|
+
/** Remove one template from the local collection. */
|
|
54
|
+
removeTemplate: (templateId: string) => boolean
|
|
55
|
+
/** Read one template from the local collection, returning null when missing. */
|
|
56
|
+
getTemplate: <TTemplate extends BioTemplateEnvelope<unknown> = BioTemplateEnvelope<unknown>>(
|
|
57
|
+
templateId: string,
|
|
58
|
+
) => TTemplate | null
|
|
59
|
+
/** Read one template from the local collection, throwing when missing or mismatched. */
|
|
60
|
+
requireTemplate: <TTemplate extends BioTemplateEnvelope<unknown> = BioTemplateEnvelope<unknown>>(
|
|
61
|
+
templateId: string,
|
|
62
|
+
) => TTemplate
|
|
63
|
+
/** Load template design_data for an experiment and apply it locally. */
|
|
64
|
+
load: (experimentId: number) => Promise<TemplateCollectionEnvelope | null>
|
|
65
|
+
/** Load template design_data for the current experiment and apply it locally. */
|
|
66
|
+
loadCurrent: () => Promise<TemplateCollectionEnvelope | null>
|
|
67
|
+
/** Save the local template collection as experiment design_data. */
|
|
68
|
+
save: (experimentId: number) => Promise<boolean>
|
|
69
|
+
/** Save the local template collection as current experiment design_data. */
|
|
70
|
+
saveCurrent: () => Promise<boolean>
|
|
71
|
+
/** Require and return the current experiment id, or throw when missing. */
|
|
72
|
+
requireCurrentExperimentId: () => number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Manages a biology template collection in the current experiment's design_data. */
|
|
76
|
+
export function useTemplateCollection(
|
|
77
|
+
options: UseTemplateCollectionOptions = {},
|
|
78
|
+
): UseTemplateCollectionReturn {
|
|
79
|
+
const experimentSave = useExperimentSave(options)
|
|
80
|
+
const templateCollection = ref<TemplateCollectionEnvelope>(createInitialCollection(options))
|
|
81
|
+
const templates = computed(() => extractTemplateCollection(templateCollection.value))
|
|
82
|
+
const metadata = computed(() => templateCollection.value.metadata ?? {})
|
|
83
|
+
const templateIds = computed(() => Object.keys(templates.value))
|
|
84
|
+
const hasTemplates = computed(() => templateIds.value.length > 0)
|
|
85
|
+
|
|
86
|
+
function apply(value: unknown, metadata?: Record<string, unknown>): TemplateCollectionEnvelope {
|
|
87
|
+
templateCollection.value = normalizeTemplateCollection(value, metadata)
|
|
88
|
+
return templateCollection.value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reset(): TemplateCollectionEnvelope {
|
|
92
|
+
templateCollection.value = createInitialCollection(options)
|
|
93
|
+
return templateCollection.value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setTemplate(template: BioTemplateEnvelope<unknown>): TemplateCollectionEnvelope {
|
|
97
|
+
const nextTemplates = {
|
|
98
|
+
...templates.value,
|
|
99
|
+
[template.template_id]: template,
|
|
100
|
+
}
|
|
101
|
+
templateCollection.value = createTemplateCollectionEnvelope(
|
|
102
|
+
Object.values(nextTemplates),
|
|
103
|
+
templateCollection.value.metadata,
|
|
104
|
+
)
|
|
105
|
+
return templateCollection.value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function removeTemplate(templateId: string): boolean {
|
|
109
|
+
if (!templates.value[templateId]) return false
|
|
110
|
+
const nextTemplates = { ...templates.value }
|
|
111
|
+
delete nextTemplates[templateId]
|
|
112
|
+
templateCollection.value = createTemplateCollectionEnvelope(
|
|
113
|
+
Object.values(nextTemplates),
|
|
114
|
+
templateCollection.value.metadata,
|
|
115
|
+
)
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getTemplate<
|
|
120
|
+
TTemplate extends BioTemplateEnvelope<unknown> = BioTemplateEnvelope<unknown>,
|
|
121
|
+
>(templateId: string): TTemplate | null {
|
|
122
|
+
return (templates.value[templateId] as TTemplate | undefined) ?? null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function requireTemplate<
|
|
126
|
+
TTemplate extends BioTemplateEnvelope<unknown> = BioTemplateEnvelope<unknown>,
|
|
127
|
+
>(templateId: string): TTemplate {
|
|
128
|
+
return ensureTemplateFromCollection<TTemplate>(templateCollection.value, templateId)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function load(experimentId: number): Promise<TemplateCollectionEnvelope | null> {
|
|
132
|
+
const data = await experimentSave.loadDesign(experimentId)
|
|
133
|
+
return data === null ? null : apply(data)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function loadCurrent(): Promise<TemplateCollectionEnvelope | null> {
|
|
137
|
+
const data = await experimentSave.loadCurrentDesign()
|
|
138
|
+
return data === null ? null : apply(data)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function save(experimentId: number): Promise<boolean> {
|
|
142
|
+
return experimentSave.saveDesign(
|
|
143
|
+
experimentId,
|
|
144
|
+
templateCollection.value as unknown as Record<string, unknown>,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function saveCurrent(): Promise<boolean> {
|
|
149
|
+
return experimentSave.saveCurrentDesign(
|
|
150
|
+
templateCollection.value as unknown as Record<string, unknown>,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
currentExperimentId: experimentSave.currentExperimentId,
|
|
156
|
+
hasCurrentExperiment: experimentSave.hasCurrentExperiment,
|
|
157
|
+
templateCollection,
|
|
158
|
+
templates,
|
|
159
|
+
metadata,
|
|
160
|
+
templateIds,
|
|
161
|
+
hasTemplates,
|
|
162
|
+
isLoading: experimentSave.isLoading,
|
|
163
|
+
isSaving: experimentSave.isSaving,
|
|
164
|
+
error: experimentSave.error,
|
|
165
|
+
lastLoadedAt: experimentSave.lastLoadedAt,
|
|
166
|
+
lastSavedAt: experimentSave.lastSavedAt,
|
|
167
|
+
apply,
|
|
168
|
+
reset,
|
|
169
|
+
setTemplate,
|
|
170
|
+
removeTemplate,
|
|
171
|
+
getTemplate,
|
|
172
|
+
requireTemplate,
|
|
173
|
+
load,
|
|
174
|
+
loadCurrent,
|
|
175
|
+
save,
|
|
176
|
+
saveCurrent,
|
|
177
|
+
requireCurrentExperimentId: experimentSave.requireCurrentExperimentId,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function createInitialCollection(options: UseTemplateCollectionOptions): TemplateCollectionEnvelope {
|
|
182
|
+
const initial = typeof options.initial === 'function' ? options.initial() : options.initial
|
|
183
|
+
if (initial === undefined) return { templates: {} }
|
|
184
|
+
return normalizeTemplateCollection(initial)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeTemplateCollection(
|
|
188
|
+
value: unknown,
|
|
189
|
+
metadata?: Record<string, unknown>,
|
|
190
|
+
): TemplateCollectionEnvelope {
|
|
191
|
+
if (Array.isArray(value)) {
|
|
192
|
+
return createTemplateCollectionEnvelope(value, metadata)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return createTemplateCollectionEnvelope(
|
|
196
|
+
Object.values(extractTemplateCollection(value)),
|
|
197
|
+
readCollectionMetadata(value) ?? metadata,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createTemplateCollectionEnvelope(
|
|
202
|
+
templates: Array<BioTemplateEnvelope<unknown>>,
|
|
203
|
+
metadata?: Record<string, unknown>,
|
|
204
|
+
): TemplateCollectionEnvelope {
|
|
205
|
+
return metadata === undefined
|
|
206
|
+
? createTemplateCollection(templates)
|
|
207
|
+
: createTemplateCollection(templates, metadata)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readCollectionMetadata(value: unknown): Record<string, unknown> | undefined {
|
|
211
|
+
if (
|
|
212
|
+
typeof value !== 'object'
|
|
213
|
+
|| value === null
|
|
214
|
+
|| Array.isArray(value)
|
|
215
|
+
|| !('metadata' in value)
|
|
216
|
+
) {
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const metadata = (value as { metadata?: unknown }).metadata
|
|
221
|
+
if (
|
|
222
|
+
typeof metadata === 'object'
|
|
223
|
+
&& metadata !== null
|
|
224
|
+
&& !Array.isArray(metadata)
|
|
225
|
+
) {
|
|
226
|
+
return metadata as Record<string, unknown>
|
|
227
|
+
}
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { computed, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type TextSearchSource<T> =
|
|
4
|
+
| T
|
|
5
|
+
| Ref<T>
|
|
6
|
+
| ComputedRef<T>
|
|
7
|
+
| (() => T)
|
|
8
|
+
|
|
9
|
+
export type TextSearchCandidate = unknown | readonly unknown[]
|
|
10
|
+
|
|
11
|
+
export interface UseTextSearchOptions<TItem> {
|
|
12
|
+
items: TextSearchSource<readonly TItem[]>
|
|
13
|
+
query: TextSearchSource<string | null | undefined>
|
|
14
|
+
enabled?: TextSearchSource<boolean | null | undefined>
|
|
15
|
+
getText: (item: TItem) => TextSearchCandidate
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseTextSearchReturn<TItem> {
|
|
19
|
+
query: ComputedRef<string>
|
|
20
|
+
filteredItems: ComputedRef<TItem[]>
|
|
21
|
+
matches: (item: TItem, query?: string | null | undefined) => boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Shared text-search filtering for tables, lists, selectors, and chip inputs. */
|
|
25
|
+
export function useTextSearch<TItem>(options: UseTextSearchOptions<TItem>): UseTextSearchReturn<TItem> {
|
|
26
|
+
const query = computed(() => normalizeSearchQuery(toValue(options.query)))
|
|
27
|
+
const enabled = computed(() => toValue(options.enabled) ?? true)
|
|
28
|
+
|
|
29
|
+
function matches(item: TItem, overrideQuery?: string | null | undefined): boolean {
|
|
30
|
+
const activeQuery = overrideQuery === undefined ? query.value : normalizeSearchQuery(overrideQuery)
|
|
31
|
+
if (!activeQuery) return true
|
|
32
|
+
return candidateMatchesSearch(options.getText(item), activeQuery)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const filteredItems = computed(() => {
|
|
36
|
+
const items = [...toValue(options.items)]
|
|
37
|
+
if (!enabled.value || !query.value) return items
|
|
38
|
+
return items.filter(item => matches(item))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
query,
|
|
43
|
+
filteredItems,
|
|
44
|
+
matches,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeSearchQuery(value: string | null | undefined): string {
|
|
49
|
+
return (value ?? '').trim().toLowerCase()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function candidateMatchesSearch(candidate: TextSearchCandidate, query: string): boolean {
|
|
53
|
+
const normalizedQuery = normalizeSearchQuery(query)
|
|
54
|
+
if (!normalizedQuery) return true
|
|
55
|
+
const values = Array.isArray(candidate) ? candidate : [candidate]
|
|
56
|
+
return values.some((value) => {
|
|
57
|
+
if (value === null || value === undefined) return false
|
|
58
|
+
return String(value).toLowerCase().includes(normalizedQuery)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, type Ref } from 'vue'
|
|
2
2
|
import { useSettingsStore } from '../stores/settings'
|
|
3
3
|
|
|
4
4
|
export interface UseThemeReturn {
|
|
@@ -14,33 +14,7 @@ export function useTheme(): UseThemeReturn {
|
|
|
14
14
|
// Load persisted theme from localStorage (idempotent — safe to call multiple times)
|
|
15
15
|
settings.initialize()
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
19
|
-
: null
|
|
20
|
-
|
|
21
|
-
const isDark = computed(() => {
|
|
22
|
-
if (settings.theme === 'system') {
|
|
23
|
-
return mql?.matches ?? false
|
|
24
|
-
}
|
|
25
|
-
return settings.theme === 'dark'
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
// React to system preference changes when in 'system' mode
|
|
29
|
-
function onSystemChange() {
|
|
30
|
-
if (settings.theme === 'system') {
|
|
31
|
-
// Force reactivity by toggling theme to itself — the computed re-evaluates
|
|
32
|
-
// because mql.matches changed, but Vue needs a trigger. We nudge the store.
|
|
33
|
-
settings.theme = 'system'
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
mql?.addEventListener('change', onSystemChange)
|
|
38
|
-
|
|
39
|
-
if (getCurrentInstance()) {
|
|
40
|
-
onUnmounted(() => {
|
|
41
|
-
mql?.removeEventListener('change', onSystemChange)
|
|
42
|
-
})
|
|
43
|
-
}
|
|
17
|
+
const isDark = computed(() => settings.isDark())
|
|
44
18
|
|
|
45
19
|
function toggleTheme() {
|
|
46
20
|
settings.theme = isDark.value ? 'light' : 'dark'
|
|
@@ -14,6 +14,11 @@ export function formatTime(hour: number, minute: number, format: '12h' | '24h' =
|
|
|
14
14
|
return `${h12}:${String(minute).padStart(2, '0')} ${period}`
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function formatTimeSlot(time: string, format: '12h' | '24h' = '24h'): string {
|
|
18
|
+
const { hour, minute } = parseTime(time)
|
|
19
|
+
return formatTime(hour, minute, format)
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function generateTimeSlots(start: string, end: string, stepMinutes: number): string[] {
|
|
18
23
|
const slots: string[] = []
|
|
19
24
|
const s = parseTime(start)
|
|
@@ -102,12 +107,27 @@ export function compareTime(a: string, b: string): number {
|
|
|
102
107
|
return 0
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
function
|
|
110
|
+
export function findNearestTimeSlotIndex(time: string, slots: readonly string[]): number {
|
|
111
|
+
if (slots.length === 0) return -1
|
|
112
|
+
const target = toMinutes(time)
|
|
113
|
+
let bestIndex = 0
|
|
114
|
+
let bestDiff = Infinity
|
|
115
|
+
for (let index = 0; index < slots.length; index++) {
|
|
116
|
+
const diff = Math.abs(toMinutes(slots[index]) - target)
|
|
117
|
+
if (diff < bestDiff) {
|
|
118
|
+
bestDiff = diff
|
|
119
|
+
bestIndex = index
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return bestIndex
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function toMinutes(time: string): number {
|
|
106
126
|
const { hour, minute } = parseTime(time)
|
|
107
127
|
return hour * 60 + minute
|
|
108
128
|
}
|
|
109
129
|
|
|
110
|
-
function fromMinutes(total: number): string {
|
|
130
|
+
export function fromMinutes(total: number): string {
|
|
111
131
|
const hour = Math.floor(total / 60) % 24
|
|
112
132
|
const minute = total % 60
|
|
113
133
|
return formatTime(hour, minute)
|
|
@@ -118,6 +138,7 @@ export function useTimeUtils() {
|
|
|
118
138
|
return {
|
|
119
139
|
parseTime,
|
|
120
140
|
formatTime,
|
|
141
|
+
formatTimeSlot,
|
|
121
142
|
generateTimeSlots,
|
|
122
143
|
rangesOverlap,
|
|
123
144
|
durationMinutes,
|
|
@@ -127,5 +148,8 @@ export function useTimeUtils() {
|
|
|
127
148
|
snapToSlot,
|
|
128
149
|
addMinutes,
|
|
129
150
|
compareTime,
|
|
151
|
+
findNearestTimeSlotIndex,
|
|
152
|
+
toMinutes,
|
|
153
|
+
fromMinutes,
|
|
130
154
|
}
|
|
131
155
|
}
|