@morscherlab/mint-sdk 1.2.0-beta.7 → 1.2.0-beta.9
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 +30 -0
- package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-oL0fg_A0.js} +5 -3
- package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-BeEOomw_.js} +4 -4
- package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
- package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-Dphh3RrI.js} +4 -4
- package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
- package/dist/{EmptyState-NBqf2_8z.js → Skeleton-DxtwJ5gt.js} +110 -92
- package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
- package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
- package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
- package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
- package/dist/components/ActionMenu.vue.d.ts +1 -1
- package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
- package/dist/components/AuditTrail.vue.d.ts +13 -0
- package/dist/components/BatchProgressList.vue.d.ts +17 -0
- package/dist/components/DataFrame.vue.d.ts +14 -2
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +2 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
- package/dist/components/ExperimentPopover.vue.d.ts +1 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
- package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
- package/dist/components/ReagentList.vue.d.ts +29 -1
- package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
- package/dist/components/SampleSelector.drag.d.ts +13 -0
- package/dist/components/SampleSelector.tree.d.ts +2 -0
- package/dist/components/SampleSelector.vue.d.ts +34 -1
- package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
- package/dist/components/TimePicker.vue.d.ts +1 -0
- package/dist/components/WellPlate.geometry.d.ts +15 -0
- package/dist/components/WellPlate.interaction.d.ts +13 -0
- package/dist/components/WellPlate.vue.d.ts +29 -0
- package/dist/components/index.js +6 -6
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
- package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
- package/dist/{components-B80gV0P6.js → components-CgAUzF8F.js} +1930 -1688
- package/dist/components-CgAUzF8F.js.map +1 -0
- package/dist/composables/controlSchemaTypes.d.ts +34 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +9 -9
- package/dist/composables/useDropdownState.d.ts +18 -2
- package/dist/composables/useEventStream.d.ts +9 -0
- package/dist/composables/useListReorder.d.ts +90 -0
- package/dist/composables/useMenuKeyboard.d.ts +2 -2
- package/dist/composables/usePluginConfig.d.ts +3 -1
- package/dist/composables/useRequestSyncState.d.ts +42 -2
- package/dist/composables/useScheduleDrag.d.ts +1 -0
- package/dist/{composables-C8mmD5ta.js → composables-B_pia3hO.js} +13 -69
- package/dist/composables-B_pia3hO.js.map +1 -0
- package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
- package/dist/errors-_W0-LbA6.js.map +1 -0
- package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
- package/dist/experiment-Cwl9AYxw.js.map +1 -0
- package/dist/experiment-utils-DV3DrvE_.js.map +1 -1
- package/dist/index.js +14 -14
- package/dist/install.js +2 -7
- package/dist/install.js.map +1 -1
- package/dist/permissions.d.ts +42 -1
- package/dist/permissions.js +20 -10
- package/dist/permissions.js.map +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +3 -3
- package/dist/stores/settings.d.ts +31 -1
- package/dist/styles.css +9572 -27877
- package/dist/templates/index.js +2 -4
- package/dist/{templates-CK72aKO1.js → templates-Qq113pyZ.js} +291 -85
- package/dist/templates-Qq113pyZ.js.map +1 -0
- package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-aMZlb4Sc.js} +197 -1089
- package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
- package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-DitCtrTT.js} +21 -5
- package/dist/useDropdownState-DitCtrTT.js.map +1 -0
- package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-I8HC8hUu.js} +3 -3
- package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
- package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-TCxNnqeb.js} +2 -2
- package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
- package/dist/{useProtocolTemplates-Dq5Pzwyd.js → useProtocolTemplates-BmX_iVBR.js} +185 -157
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
- package/dist/utils/color.d.ts +13 -0
- package/dist/utils/errors.d.ts +9 -0
- package/package.json +1 -1
- package/src/__tests__/components/ActionMenu.test.ts +94 -0
- package/src/__tests__/components/DataFrame.test.ts +183 -11
- package/src/__tests__/components/DataStates.test.ts +176 -0
- package/src/__tests__/components/DatePicker.test.ts +76 -0
- package/src/__tests__/components/DateTimePicker.test.ts +36 -0
- package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
- package/src/__tests__/components/GroupAssigner.test.ts +53 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
- package/src/__tests__/components/RackEditor.test.ts +35 -0
- package/src/__tests__/components/ReagentList.test.ts +64 -2
- package/src/__tests__/components/RetiredStyles.test.ts +31 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
- package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
- package/src/__tests__/components/SegmentedControl.test.ts +88 -0
- package/src/__tests__/components/TimePicker.test.ts +61 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
- package/src/__tests__/composables/useDropdownState.test.ts +58 -1
- package/src/__tests__/composables/useListReorder.test.ts +145 -0
- package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
- package/src/__tests__/stores/settings.test.ts +71 -1
- package/src/__tests__/styles/cssTokens.test.ts +131 -0
- package/src/__tests__/utils/permissions.test.ts +68 -0
- package/src/components/ActionMenu.vue +45 -88
- package/src/components/AlertBox.vue +0 -4
- package/src/components/AppAvatarMenu.vue +2 -14
- package/src/components/AppLayout.vue +0 -4
- package/src/components/AppPluginSwitcher.story.vue +87 -0
- package/src/components/AppPluginSwitcher.vue +2 -5
- package/src/components/AppSidebar.vue +0 -4
- package/src/components/AppToastContainer.vue +6 -8
- package/src/components/AppTopBar.vue +0 -4
- package/src/components/ArtifactSaveDialog.vue +0 -4
- package/src/components/ArtifactSelectorModal.vue +0 -4
- package/src/components/AuditTrail.story.vue +18 -0
- package/src/components/AuditTrail.vue +27 -7
- package/src/components/AutoGroupModal.vue +0 -4
- package/src/components/Avatar.vue +0 -4
- package/src/components/BaseButton.vue +0 -4
- package/src/components/BaseCheckbox.vue +0 -4
- package/src/components/BaseInput.vue +0 -4
- package/src/components/BaseModal.vue +0 -4
- package/src/components/BasePill.vue +0 -4
- package/src/components/BaseRadioGroup.vue +0 -4
- package/src/components/BaseSelect.vue +0 -4
- package/src/components/BaseSlider.vue +0 -4
- package/src/components/BaseTabs.vue +0 -4
- package/src/components/BaseTextarea.vue +0 -4
- package/src/components/BaseToggle.vue +0 -4
- package/src/components/BatchProgressList.story.vue +18 -0
- package/src/components/BatchProgressList.vue +32 -4
- package/src/components/Breadcrumb.vue +0 -4
- package/src/components/Calendar.vue +0 -4
- package/src/components/ChartContainer.story.vue +1 -1
- package/src/components/ChartContainer.vue +0 -4
- package/src/components/ChemicalFormula.vue +0 -4
- package/src/components/CollapsibleCard.vue +0 -4
- package/src/components/ColorSlider.vue +0 -4
- package/src/components/ConcentrationInput.vue +0 -4
- package/src/components/ConfirmDialog.vue +0 -4
- package/src/components/DataFrame.story.vue +14 -0
- package/src/components/DataFrame.vue +97 -23
- package/src/components/DatePicker.vue +73 -27
- package/src/components/DateTimePicker.vue +45 -17
- package/src/components/Divider.vue +0 -4
- package/src/components/DoseCalculator.vue +0 -4
- package/src/components/DropdownButton.vue +2 -14
- package/src/components/EmptyState.vue +0 -4
- package/src/components/ExperimentCodeBadge.vue +0 -4
- package/src/components/ExperimentDataViewer.story.vue +3 -0
- package/src/components/ExperimentDataViewer.vue +33 -13
- package/src/components/ExperimentPopover.vue +2 -5
- package/src/components/ExperimentSelectorModal.vue +0 -4
- package/src/components/ExperimentTimeline.story.vue +18 -0
- package/src/components/ExperimentTimeline.vue +81 -75
- package/src/components/FileUploader.vue +0 -4
- package/src/components/FitPanel.vue +0 -4
- package/src/components/FormActions.story.vue +96 -0
- package/src/components/FormActions.vue +0 -4
- package/src/components/FormBuilder.vue +0 -4
- package/src/components/FormField.vue +0 -4
- package/src/components/FormulaInput.vue +0 -4
- package/src/components/GeneratedPathInput.story.vue +68 -0
- package/src/components/GeneratedPlotlyResult.story.vue +96 -0
- package/src/components/GeneratedPlotlyResult.vue +3 -3
- package/src/components/GeneratedPluginView.story.vue +145 -0
- package/src/components/GeneratedPluginView.vue +10 -10
- package/src/components/GeneratedResultRenderer.story.vue +117 -0
- package/src/components/GeneratedResultRenderer.vue +4 -4
- package/src/components/GroupAssigner.vue +67 -5
- package/src/components/IconButton.vue +0 -4
- package/src/components/InstrumentAlertLog.vue +0 -4
- package/src/components/InstrumentStateBadge.vue +0 -4
- package/src/components/InstrumentStatusCard.vue +0 -4
- package/src/components/JobsStatusTray.vue +0 -4
- package/src/components/LcmsSequenceTable.vue +81 -50
- package/src/components/LoadingSpinner.vue +0 -4
- package/src/components/MobileSupportGate.vue +0 -4
- package/src/components/MoleculeInput.vue +0 -4
- package/src/components/MultiSelect.vue +0 -4
- package/src/components/NumberInput.vue +0 -4
- package/src/components/PlateMapEditor.vue +3 -14
- package/src/components/PluginIcon.vue +0 -4
- package/src/components/ProgressBar.vue +0 -4
- package/src/components/ProtocolStepEditor.vue +2 -4
- package/src/components/RackEditor.vue +51 -70
- package/src/components/ReagentEditor.vue +0 -4
- package/src/components/ReagentList.story.vue +18 -0
- package/src/components/ReagentList.vue +127 -82
- package/src/components/ResourceCard.vue +0 -4
- package/src/components/RuntimeUpdateNotice.story.vue +49 -0
- package/src/components/RuntimeUpdateNotice.vue +4 -4
- package/src/components/SampleHierarchyTree.story.vue +18 -0
- package/src/components/SampleHierarchyTree.vue +32 -9
- package/src/components/SampleLegend.vue +2 -8
- package/src/components/SampleSelector.drag.ts +51 -0
- package/src/components/SampleSelector.story.vue +6 -0
- package/src/components/SampleSelector.tree.ts +2 -0
- package/src/components/SampleSelector.vue +49 -10
- package/src/components/SampleSelectorGroupNode.vue +19 -0
- package/src/components/ScheduleCalendar.story.vue +6 -0
- package/src/components/ScheduleCalendar.vue +82 -7
- package/src/components/ScientificNumber.vue +0 -4
- package/src/components/SectionCard.vue +0 -4
- package/src/components/SegmentedControl.vue +48 -5
- package/src/components/SequenceInput.vue +0 -4
- package/src/components/SequenceProgressBar.story.vue +110 -0
- package/src/components/SequenceProgressBar.vue +0 -4
- package/src/components/SettingsModal.vue +0 -4
- package/src/components/Skeleton.vue +0 -4
- package/src/components/SmartGroupFieldRecipe.vue +8 -6
- package/src/components/SmartGroupManual.vue +0 -4
- package/src/components/StatusIndicator.vue +0 -4
- package/src/components/StepWizard.vue +0 -4
- package/src/components/TagsInput.vue +4 -6
- package/src/components/ThemeToggle.vue +0 -4
- package/src/components/TimePicker.vue +36 -7
- package/src/components/TimeRangeInput.vue +0 -4
- package/src/components/Tooltip.story.vue +3 -3
- package/src/components/Tooltip.vue +0 -4
- package/src/components/UnitInput.vue +0 -4
- package/src/components/WellPlate.geometry.ts +60 -1
- package/src/components/WellPlate.interaction.ts +44 -0
- package/src/components/WellPlate.story.vue +8 -0
- package/src/components/WellPlate.vue +53 -7
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
- package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
- package/src/components/internal/DataStateInternal.vue +74 -0
- package/src/components/internal/FormSectionRenderer.vue +0 -4
- package/src/components/internal/WellEditPopupInternal.vue +0 -4
- package/src/composables/controlSchemaTypes.ts +34 -0
- package/src/composables/index.ts +10 -0
- package/src/composables/useDropdownState.ts +35 -5
- package/src/composables/useEventStream.ts +14 -2
- package/src/composables/useFileImport.ts +2 -7
- package/src/composables/useGeneratedAnalysis.ts +8 -9
- package/src/composables/useListReorder.story.vue +143 -0
- package/src/composables/useListReorder.ts +242 -0
- package/src/composables/useMenuKeyboard.ts +2 -2
- package/src/composables/useOptimisticMutation.ts +1 -15
- package/src/composables/usePluginConfig.ts +3 -1
- package/src/composables/useRackEditor.ts +2 -11
- package/src/composables/useRequestSyncState.ts +98 -28
- package/src/composables/useResourceCrud.ts +1 -15
- package/src/composables/useScheduleDrag.ts +37 -0
- package/src/composables/useWellPainting.ts +5 -29
- package/src/composables/useWellPlateEditor.ts +1 -11
- package/src/permissions.ts +48 -9
- package/src/stores/experiment.ts +2 -7
- package/src/stores/index.ts +9 -1
- package/src/stores/settings.ts +93 -1
- package/src/styles/components/audit-trail.css +5 -5
- package/src/styles/components/batch-progress-list.css +7 -0
- package/src/styles/components/color-slider.css +2 -2
- package/src/styles/components/data-state.css +30 -0
- package/src/styles/components/dataframe.css +29 -35
- package/src/styles/components/experiment-data-viewer.css +4 -4
- package/src/styles/components/experiment-timeline.css +3 -30
- package/src/styles/components/form-builder.css +5 -5
- package/src/styles/components/lcms-sequence-table.css +16 -1
- package/src/styles/components/multi-select.css +1 -1
- package/src/styles/components/reagent-list.css +38 -31
- package/src/styles/components/sample-hierarchy-tree.css +5 -19
- package/src/styles/components/sample-selector.css +9 -2
- package/src/styles/components/schedule-calendar.css +8 -0
- package/src/styles/components/scientific-number.css +1 -1
- package/src/styles/components/well-plate.css +8 -0
- package/src/styles/index.css +8 -0
- package/src/styles/variables.css +29 -0
- package/src/templates/builderPresetControls.ts +1 -1
- package/src/templates/componentPlateHelpers.ts +3 -1
- package/src/templates/doseResponseAdapters.ts +2 -13
- package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
- package/src/templates/plateMapBuilder.ts +2 -2
- package/src/templates/qpcrPlateBuilder.ts +1 -1
- package/src/templates/templateValidators.ts +1 -1
- package/src/templates/timeCourseBuilder.ts +2 -2
- package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
- package/src/utils/color.ts +51 -0
- package/src/utils/errors.ts +29 -0
- package/dist/EmptyState-NBqf2_8z.js.map +0 -1
- package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
- package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
- package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
- package/dist/components-B80gV0P6.js.map +0 -1
- package/dist/composables/autoGroupConstants.d.ts +0 -1
- package/dist/composables/autoGroupGrouping.d.ts +0 -3
- package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
- package/dist/composables-C8mmD5ta.js.map +0 -1
- package/dist/experiment-Y9Iz7KYi.js.map +0 -1
- package/dist/templates/builderDefaults.d.ts +0 -1
- package/dist/templates/plateGeometry.d.ts +0 -4
- package/dist/templates-CK72aKO1.js.map +0 -1
- package/dist/useApi-CDBq6z23.js.map +0 -1
- package/dist/useControlSchema-_OVhavKq.js.map +0 -1
- package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
- package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
- package/src/composables/autoGroupConstants.ts +0 -4
- package/src/composables/autoGroupGrouping.ts +0 -148
- package/src/composables/useAutoGroupInputSources.ts +0 -147
- package/src/templates/builderDefaults.ts +0 -11
- package/src/templates/plateGeometry.ts +0 -62
- /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import type { Rack, SlotPosition, WellPlateFormat, Well } from '../types'
|
|
3
|
+
import { PLATE_CONFIGS, rowLabel as plateRowLabel } from '../components/WellPlate.geometry'
|
|
3
4
|
|
|
4
5
|
export interface UseRackEditorOptions {
|
|
5
6
|
defaultFormat?: WellPlateFormat
|
|
@@ -31,15 +32,6 @@ export interface UseRackEditorReturn {
|
|
|
31
32
|
|
|
32
33
|
const SLOT_CYCLE: SlotPosition[] = ['R', 'G', 'B', 'Y']
|
|
33
34
|
|
|
34
|
-
const PLATE_CONFIGS: Record<WellPlateFormat, { rows: number; cols: number }> = {
|
|
35
|
-
6: { rows: 2, cols: 3 },
|
|
36
|
-
12: { rows: 3, cols: 4 },
|
|
37
|
-
24: { rows: 4, cols: 6 },
|
|
38
|
-
48: { rows: 6, cols: 8 },
|
|
39
|
-
54: { rows: 6, cols: 9 },
|
|
40
|
-
96: { rows: 8, cols: 12 },
|
|
41
|
-
384: { rows: 16, cols: 24 },
|
|
42
|
-
}
|
|
43
35
|
|
|
44
36
|
let globalIdCounter = 0
|
|
45
37
|
|
|
@@ -156,8 +148,7 @@ export function useRackEditor(
|
|
|
156
148
|
let counter = 1
|
|
157
149
|
for (let row = 0; row < config.rows; row++) {
|
|
158
150
|
for (let col = 0; col < config.cols; col++) {
|
|
159
|
-
const
|
|
160
|
-
const wellId = `${rowLabel}${col + 1}`
|
|
151
|
+
const wellId = `${plateRowLabel(row)}${col + 1}`
|
|
161
152
|
// Only fill empty wells
|
|
162
153
|
if (!rack.wells[wellId]?.sampleType) {
|
|
163
154
|
const paddedNum = String(counter).padStart(3, '0')
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { ref, type Ref } from 'vue'
|
|
2
|
+
import { readErrorMessage as readSharedErrorMessage } from '../utils/errors'
|
|
2
3
|
|
|
3
4
|
export type RequestSyncSuccessKind = 'load' | 'save' | 'run'
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The unit of work `run()` drives.
|
|
8
|
+
*
|
|
9
|
+
* It receives an `AbortSignal` that fires when the run is superseded by a
|
|
10
|
+
* newer `run()` or by `cancel()`. Zero-argument operations stay valid and
|
|
11
|
+
* simply ignore it.
|
|
12
|
+
*/
|
|
13
|
+
export type RequestSyncOperation<T> = (signal: AbortSignal) => Promise<T>
|
|
14
|
+
|
|
5
15
|
export interface RequestSyncRunOptions {
|
|
6
16
|
success?: RequestSyncSuccessKind
|
|
7
17
|
errorMessage?: string
|
|
18
|
+
/**
|
|
19
|
+
* Abort the previously started run when this one starts. Default `true`;
|
|
20
|
+
* pass `false` to let overlapping runs finish their own work (the stale
|
|
21
|
+
* guard on shared state still applies either way).
|
|
22
|
+
*/
|
|
23
|
+
abortPrevious?: boolean
|
|
24
|
+
/** Caller-owned signal; aborting it aborts this run's signal too. */
|
|
25
|
+
signal?: AbortSignal
|
|
8
26
|
}
|
|
9
27
|
|
|
10
28
|
export interface UseRequestSyncStateReturn {
|
|
@@ -30,8 +48,32 @@ export interface UseRequestSyncStateReturn {
|
|
|
30
48
|
markSaved: (date?: Date) => void
|
|
31
49
|
/** Mark the operation as successfully run. */
|
|
32
50
|
markRun: (date?: Date) => void
|
|
33
|
-
/**
|
|
34
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Run a request with shared loading/error handling and optional sync marking.
|
|
53
|
+
*
|
|
54
|
+
* ## Stale-response semantics
|
|
55
|
+
*
|
|
56
|
+
* Every call takes a monotonically increasing token. Starting a run
|
|
57
|
+
* supersedes any run started before it, and a superseded run writes **no**
|
|
58
|
+
* shared state when it finally settles: it does not clear `loading`, does not
|
|
59
|
+
* set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.
|
|
60
|
+
* Only the newest run owns those.
|
|
61
|
+
*
|
|
62
|
+
* The caller's own promise is untouched by the guard — a superseded run still
|
|
63
|
+
* resolves with its value or rejects with its own error, exactly as the
|
|
64
|
+
* operation did. The guard never invents a rejection, so a caller awaiting
|
|
65
|
+
* `run()` never sees a "superseded" error it did not cause.
|
|
66
|
+
*
|
|
67
|
+
* The operation additionally receives an `AbortSignal` that fires when its
|
|
68
|
+
* run is superseded or `cancel()` is called, so an operation that forwards
|
|
69
|
+
* the signal (`fetch(url, { signal })`) stops work instead of racing.
|
|
70
|
+
*/
|
|
71
|
+
run: <T>(operation: RequestSyncOperation<T>, options?: RequestSyncRunOptions) => Promise<T>
|
|
72
|
+
/**
|
|
73
|
+
* Abort the in-flight run and drop `loading`. The aborted run's completion
|
|
74
|
+
* is treated as stale and writes no state.
|
|
75
|
+
*/
|
|
76
|
+
cancel: () => void
|
|
35
77
|
}
|
|
36
78
|
|
|
37
79
|
/** Shared loading/error/timestamp state for generated plugin request helpers. */
|
|
@@ -49,22 +91,7 @@ export function useRequestSyncState(
|
|
|
49
91
|
}
|
|
50
92
|
|
|
51
93
|
function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {
|
|
52
|
-
|
|
53
|
-
return value.message
|
|
54
|
-
}
|
|
55
|
-
if (typeof value === 'string' && value.trim()) {
|
|
56
|
-
return value
|
|
57
|
-
}
|
|
58
|
-
if (
|
|
59
|
-
typeof value === 'object'
|
|
60
|
-
&& value !== null
|
|
61
|
-
&& 'message' in value
|
|
62
|
-
&& typeof (value as { message?: unknown }).message === 'string'
|
|
63
|
-
&& (value as { message: string }).message
|
|
64
|
-
) {
|
|
65
|
-
return (value as { message: string }).message
|
|
66
|
-
}
|
|
67
|
-
return fallback
|
|
94
|
+
return readSharedErrorMessage(value, fallback)
|
|
68
95
|
}
|
|
69
96
|
|
|
70
97
|
function setError(value: unknown, fallback?: string): string {
|
|
@@ -85,27 +112,69 @@ export function useRequestSyncState(
|
|
|
85
112
|
lastRunAt.value = date
|
|
86
113
|
}
|
|
87
114
|
|
|
115
|
+
// Monotonic token guarding every write to the shared loading/error/timestamp
|
|
116
|
+
// state. Overlapping runs settle in any order, and a superseded one must not
|
|
117
|
+
// clear a newer run's loading flag or overwrite its error.
|
|
118
|
+
let runToken = 0
|
|
119
|
+
let activeController: AbortController | null = null
|
|
120
|
+
|
|
121
|
+
function cancel(): void {
|
|
122
|
+
runToken += 1
|
|
123
|
+
activeController?.abort()
|
|
124
|
+
activeController = null
|
|
125
|
+
loading.value = false
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
async function run<T>(
|
|
89
|
-
operation:
|
|
129
|
+
operation: RequestSyncOperation<T>,
|
|
90
130
|
options: RequestSyncRunOptions = {},
|
|
91
131
|
): Promise<T> {
|
|
132
|
+
if (options.abortPrevious !== false) {
|
|
133
|
+
activeController?.abort()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const token = (runToken += 1)
|
|
137
|
+
const controller = new AbortController()
|
|
138
|
+
activeController = controller
|
|
139
|
+
|
|
140
|
+
const callerSignal = options.signal
|
|
141
|
+
if (callerSignal) {
|
|
142
|
+
if (callerSignal.aborted) controller.abort(callerSignal.reason)
|
|
143
|
+
else callerSignal.addEventListener(
|
|
144
|
+
'abort',
|
|
145
|
+
() => controller.abort(callerSignal.reason),
|
|
146
|
+
{ once: true },
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const isCurrent = (): boolean => token === runToken
|
|
151
|
+
|
|
92
152
|
loading.value = true
|
|
93
153
|
clearError()
|
|
94
154
|
try {
|
|
95
|
-
const response = await operation()
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
const response = await operation(controller.signal)
|
|
156
|
+
if (isCurrent()) {
|
|
157
|
+
if (options.success === 'load') {
|
|
158
|
+
markLoaded()
|
|
159
|
+
} else if (options.success === 'save') {
|
|
160
|
+
markSaved()
|
|
161
|
+
} else if (options.success === 'run') {
|
|
162
|
+
markRun()
|
|
163
|
+
}
|
|
102
164
|
}
|
|
103
165
|
return response
|
|
104
166
|
} catch (err) {
|
|
105
|
-
|
|
167
|
+
if (isCurrent()) {
|
|
168
|
+
setError(err, options.errorMessage)
|
|
169
|
+
}
|
|
106
170
|
throw err
|
|
107
171
|
} finally {
|
|
108
|
-
|
|
172
|
+
if (isCurrent()) {
|
|
173
|
+
loading.value = false
|
|
174
|
+
}
|
|
175
|
+
if (activeController === controller) {
|
|
176
|
+
activeController = null
|
|
177
|
+
}
|
|
109
178
|
}
|
|
110
179
|
}
|
|
111
180
|
|
|
@@ -122,5 +191,6 @@ export function useRequestSyncState(
|
|
|
122
191
|
markSaved,
|
|
123
192
|
markRun,
|
|
124
193
|
run,
|
|
194
|
+
cancel,
|
|
125
195
|
}
|
|
126
196
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
|
2
|
+
import { readErrorMessage } from '../utils/errors'
|
|
2
3
|
|
|
3
4
|
export type ResourceKey = string | number
|
|
4
5
|
|
|
@@ -232,18 +233,3 @@ function resolveKey<TItem, TKey extends ResourceKey>(
|
|
|
232
233
|
}
|
|
233
234
|
return getKey(keyOrItem as TItem)
|
|
234
235
|
}
|
|
235
|
-
|
|
236
|
-
function readErrorMessage(value: unknown): string {
|
|
237
|
-
if (value instanceof Error && value.message) return value.message
|
|
238
|
-
if (typeof value === 'string' && value.trim()) return value
|
|
239
|
-
if (
|
|
240
|
-
typeof value === 'object'
|
|
241
|
-
&& value !== null
|
|
242
|
-
&& 'message' in value
|
|
243
|
-
&& typeof (value as { message?: unknown }).message === 'string'
|
|
244
|
-
&& (value as { message: string }).message.trim()
|
|
245
|
-
) {
|
|
246
|
-
return (value as { message: string }).message
|
|
247
|
-
}
|
|
248
|
-
return 'Request failed.'
|
|
249
|
-
}
|
|
@@ -200,6 +200,42 @@ export function useScheduleDrag(options: UseScheduleDragOptions) {
|
|
|
200
200
|
cleanup()
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Keyboard alternative to the pointer drag: shifts an event by one slot
|
|
205
|
+
* (`mode: 'move'`) or grows/shrinks its end by one slot (`mode: 'resize'`),
|
|
206
|
+
* staying inside the visible day. Returns whether the nudge was applied.
|
|
207
|
+
*/
|
|
208
|
+
function nudgeEvent(
|
|
209
|
+
event: ScheduleEvent,
|
|
210
|
+
direction: -1 | 1,
|
|
211
|
+
mode: 'move' | 'resize' = 'move',
|
|
212
|
+
): boolean {
|
|
213
|
+
if (options.readonly.value) return false
|
|
214
|
+
if (mode === 'move' && event.draggable === false) return false
|
|
215
|
+
if (mode === 'resize' && event.resizable === false) return false
|
|
216
|
+
|
|
217
|
+
const slotMin = options.slotDuration.value
|
|
218
|
+
const dayStart = options.dayStartHour.value * 60
|
|
219
|
+
const dayEnd = options.dayEndHour.value * 60
|
|
220
|
+
const start = dateToMinutes(new Date(event.start))
|
|
221
|
+
const end = dateToMinutes(new Date(event.end))
|
|
222
|
+
|
|
223
|
+
const nextStart = mode === 'move' ? start + direction * slotMin : start
|
|
224
|
+
const nextEnd = mode === 'move'
|
|
225
|
+
? end + direction * slotMin
|
|
226
|
+
: Math.max(start + slotMin, end + direction * slotMin)
|
|
227
|
+
|
|
228
|
+
if (nextStart < dayStart || nextEnd > dayEnd || nextEnd <= nextStart) return false
|
|
229
|
+
|
|
230
|
+
const base = new Date(event.start)
|
|
231
|
+
const newStart = minutesToDate(base, nextStart)
|
|
232
|
+
const newEnd = minutesToDate(base, nextEnd)
|
|
233
|
+
|
|
234
|
+
if (mode === 'move') options.onMoveComplete?.(event, newStart, newEnd)
|
|
235
|
+
else options.onResizeComplete?.(event, newStart, newEnd)
|
|
236
|
+
return true
|
|
237
|
+
}
|
|
238
|
+
|
|
203
239
|
function addListeners() {
|
|
204
240
|
document.addEventListener('pointermove', onPointerMove)
|
|
205
241
|
document.addEventListener('pointerup', onPointerUp)
|
|
@@ -221,6 +257,7 @@ export function useScheduleDrag(options: UseScheduleDragOptions) {
|
|
|
221
257
|
startCreate,
|
|
222
258
|
startMove,
|
|
223
259
|
startResize,
|
|
260
|
+
nudgeEvent,
|
|
224
261
|
}
|
|
225
262
|
}
|
|
226
263
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { computed, ref, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
2
|
import type { WellPlateFormat } from '../types'
|
|
3
|
+
import {
|
|
4
|
+
PLATE_CONFIGS as PLATE_DIMENSIONS,
|
|
5
|
+
rowIndex as rowLabelToIndex,
|
|
6
|
+
rowLabel as indexToRowLabel,
|
|
7
|
+
} from '../components/WellPlate.geometry'
|
|
3
8
|
|
|
4
9
|
export type WellPlateSource<T> = T | Ref<T> | ComputedRef<T> | (() => T)
|
|
5
10
|
|
|
@@ -34,17 +39,6 @@ export interface UseWellPaintingReturn<TMode extends string = string, TPayload =
|
|
|
34
39
|
cancel: () => void
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
const ROW_LABELS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
38
|
-
|
|
39
|
-
const PLATE_DIMENSIONS: Record<WellPlateFormat, { rows: number; cols: number }> = {
|
|
40
|
-
6: { rows: 2, cols: 3 },
|
|
41
|
-
12: { rows: 3, cols: 4 },
|
|
42
|
-
24: { rows: 4, cols: 6 },
|
|
43
|
-
48: { rows: 6, cols: 8 },
|
|
44
|
-
54: { rows: 6, cols: 9 },
|
|
45
|
-
96: { rows: 8, cols: 12 },
|
|
46
|
-
384: { rows: 16, cols: 24 },
|
|
47
|
-
}
|
|
48
42
|
|
|
49
43
|
/**
|
|
50
44
|
* Pointer painting/rectangle selection state for WellPlate-based editors.
|
|
@@ -170,22 +164,4 @@ export function wellIdsInRectangle(
|
|
|
170
164
|
return ids
|
|
171
165
|
}
|
|
172
166
|
|
|
173
|
-
function rowLabelToIndex(label: string): number {
|
|
174
|
-
let result = 0
|
|
175
|
-
for (const char of label) {
|
|
176
|
-
result = result * 26 + (char.charCodeAt(0) - 64)
|
|
177
|
-
}
|
|
178
|
-
return result - 1
|
|
179
|
-
}
|
|
180
167
|
|
|
181
|
-
function indexToRowLabel(index: number): string {
|
|
182
|
-
if (index < ROW_LABELS.length) return ROW_LABELS[index]
|
|
183
|
-
let value = index + 1
|
|
184
|
-
let label = ''
|
|
185
|
-
while (value > 0) {
|
|
186
|
-
value--
|
|
187
|
-
label = ROW_LABELS[value % 26] + label
|
|
188
|
-
value = Math.floor(value / 26)
|
|
189
|
-
}
|
|
190
|
-
return label
|
|
191
|
-
}
|
|
@@ -5,18 +5,8 @@ import type {
|
|
|
5
5
|
SampleType,
|
|
6
6
|
WellPlateFormat,
|
|
7
7
|
} from '../types'
|
|
8
|
+
import { SAMPLE_COLOR_SCALE as DEFAULT_PALETTE } from '../utils/color'
|
|
8
9
|
|
|
9
|
-
const DEFAULT_PALETTE = [
|
|
10
|
-
'#3B82F6', // blue
|
|
11
|
-
'#10B981', // green
|
|
12
|
-
'#EF4444', // red
|
|
13
|
-
'#F59E0B', // amber
|
|
14
|
-
'#8B5CF6', // purple
|
|
15
|
-
'#F97316', // orange
|
|
16
|
-
'#06B6D4', // cyan
|
|
17
|
-
'#14B8A6', // teal
|
|
18
|
-
'#6B7280', // gray
|
|
19
|
-
]
|
|
20
10
|
|
|
21
11
|
const MAX_HISTORY = 50
|
|
22
12
|
|
package/src/permissions.ts
CHANGED
|
@@ -2,7 +2,6 @@ export const ADMIN_ROLE = 'admin'
|
|
|
2
2
|
|
|
3
3
|
export const ADMIN_PANEL_PERMISSIONS = [
|
|
4
4
|
'users.view',
|
|
5
|
-
'users.invite',
|
|
6
5
|
'users.manage',
|
|
7
6
|
'platform.configure',
|
|
8
7
|
'platform.view_logs',
|
|
@@ -41,11 +40,36 @@ export interface AccessPolicy {
|
|
|
41
40
|
plugin?: string
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
/**
|
|
44
|
+
* An object that carries an access rule.
|
|
45
|
+
*
|
|
46
|
+
* The nested `access` policy is the supported shape. The four flat fields are
|
|
47
|
+
* the pre-1.2 spelling of the same four `AccessPolicy` fields; they still work
|
|
48
|
+
* but are deprecated, and `normalizeAccessPolicy()` lets the nested policy win
|
|
49
|
+
* wherever both are given.
|
|
50
|
+
*/
|
|
44
51
|
export interface AccessControlled {
|
|
52
|
+
/** The access policy. Preferred over the flat fields below. */
|
|
45
53
|
access?: AccessPolicy
|
|
54
|
+
/**
|
|
55
|
+
* @deprecated Since MINT 1.2; use `access.visibleFor` instead. The flat
|
|
56
|
+
* duplicate is removed in 1.3.
|
|
57
|
+
*/
|
|
46
58
|
visibleFor?: AccessAudienceInput
|
|
59
|
+
/**
|
|
60
|
+
* @deprecated Since MINT 1.2; use `access.requiresAdmin` instead. The flat
|
|
61
|
+
* duplicate is removed in 1.3.
|
|
62
|
+
*/
|
|
47
63
|
requiresAdmin?: boolean
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Since MINT 1.2; use `access.permissions` instead. The flat
|
|
66
|
+
* duplicate is removed in 1.3.
|
|
67
|
+
*/
|
|
48
68
|
permissions?: readonly string[]
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Since MINT 1.2; use `access.anyPermissions` instead. The flat
|
|
71
|
+
* duplicate is removed in 1.3.
|
|
72
|
+
*/
|
|
49
73
|
anyPermissions?: readonly string[]
|
|
50
74
|
}
|
|
51
75
|
|
|
@@ -105,17 +129,32 @@ export function canAccessPlugin(
|
|
|
105
129
|
return Array.isArray(access) && access.includes(pluginName)
|
|
106
130
|
}
|
|
107
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Flatten an access rule — either shape, or a mix of both — into one policy.
|
|
134
|
+
*
|
|
135
|
+
* ## Precedence
|
|
136
|
+
*
|
|
137
|
+
* 1. A defined field on the nested `access` policy wins. It is the supported
|
|
138
|
+
* spelling, so an object that carries both shapes is read as "the nested
|
|
139
|
+
* policy, with the deprecated flat fields as leftovers".
|
|
140
|
+
* 2. Otherwise the flat field on the rule itself is used.
|
|
141
|
+
* 3. A field set to `undefined` on either side counts as unset and never
|
|
142
|
+
* masks the other side.
|
|
143
|
+
*
|
|
144
|
+
* Only `AccessPolicy` fields the flat shape never had (`audience`,
|
|
145
|
+
* `requiresAuth`, `plugin`) can come from the nested policy alone; the four
|
|
146
|
+
* duplicated fields are the ones this ordering exists for.
|
|
147
|
+
*/
|
|
108
148
|
export function normalizeAccessPolicy(rule: AccessControlled | AccessPolicy | undefined): AccessPolicy {
|
|
109
149
|
if (!rule) return {}
|
|
110
|
-
const { access: nested, ...
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
...('requiresAdmin' in rule && rule.requiresAdmin !== undefined ? { requiresAdmin: rule.requiresAdmin } : {}),
|
|
116
|
-
...('permissions' in rule && rule.permissions !== undefined ? { permissions: rule.permissions } : {}),
|
|
117
|
-
...('anyPermissions' in rule && rule.anyPermissions !== undefined ? { anyPermissions: rule.anyPermissions } : {}),
|
|
150
|
+
const { access: nested, ...flat } = rule as AccessPolicy & { access?: AccessPolicy }
|
|
151
|
+
const policy = { ...flat } as Record<string, unknown>
|
|
152
|
+
|
|
153
|
+
for (const [field, value] of Object.entries(nested ?? {})) {
|
|
154
|
+
if (value !== undefined) policy[field] = value
|
|
118
155
|
}
|
|
156
|
+
|
|
157
|
+
return policy as AccessPolicy
|
|
119
158
|
}
|
|
120
159
|
|
|
121
160
|
export function canAccessByPolicy(
|
package/src/stores/experiment.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import { useApi } from '../composables/useApi'
|
|
4
|
+
import { readErrorMessage } from '../utils/errors'
|
|
4
5
|
import type { ExperimentSummary, PlatformContext } from '../types'
|
|
5
6
|
|
|
6
7
|
function getPlatformApiUrl(): string | undefined {
|
|
@@ -9,12 +10,6 @@ function getPlatformApiUrl(): string | undefined {
|
|
|
9
10
|
?.platformApiUrl
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
function readErrorMessage(value: unknown): string {
|
|
13
|
-
if (value instanceof Error && value.message) return value.message
|
|
14
|
-
if (typeof value === 'string' && value.trim()) return value
|
|
15
|
-
return 'Failed to load experiment'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
/**
|
|
19
14
|
* The single source of truth for the experiment a plugin is currently working on.
|
|
20
15
|
*
|
|
@@ -89,7 +84,7 @@ export const useExperimentStore = defineStore('mint-experiment', () => {
|
|
|
89
84
|
} catch (err) {
|
|
90
85
|
if (token !== selectionToken) return null
|
|
91
86
|
current.value = null
|
|
92
|
-
error.value = readErrorMessage(err)
|
|
87
|
+
error.value = readErrorMessage(err, 'Failed to load experiment')
|
|
93
88
|
return null
|
|
94
89
|
} finally {
|
|
95
90
|
// Only the newest request owns the loading flag; otherwise a superseded
|
package/src/stores/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export { useAuthStore } from './auth'
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
useSettingsStore,
|
|
4
|
+
tryUseSettingsStore,
|
|
5
|
+
colorPalettes,
|
|
6
|
+
paletteCssVariables,
|
|
7
|
+
tableDensityToSize,
|
|
8
|
+
PALETTE_CSS_VARIABLES,
|
|
9
|
+
type SettingsState,
|
|
10
|
+
} from './settings'
|
|
3
11
|
export { useExperimentStore } from './experiment'
|
package/src/stores/settings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
1
|
+
import { defineStore, getActivePinia } from 'pinia'
|
|
2
2
|
import { ref, watch } from 'vue'
|
|
3
3
|
import type { ThemeMode, ColorPalette, TableDensity } from '../types'
|
|
4
4
|
|
|
@@ -62,6 +62,46 @@ export const colorPalettes: Record<ColorPalette, { name: string; hues: [number,
|
|
|
62
62
|
pastel: { name: 'Pastel', hues: [200, 340] },
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Custom properties a non-default palette overrides on `<html>`.
|
|
67
|
+
*
|
|
68
|
+
* The saturation/lightness pairs are lifted from the canonical indigo/orange
|
|
69
|
+
* tokens in `styles/variables.css` so only the hue moves; the `default`
|
|
70
|
+
* palette writes nothing at all and the stylesheet values stand.
|
|
71
|
+
*/
|
|
72
|
+
export const PALETTE_CSS_VARIABLES = [
|
|
73
|
+
'--color-primary',
|
|
74
|
+
'--color-primary-hover',
|
|
75
|
+
'--color-primary-soft',
|
|
76
|
+
'--color-cta',
|
|
77
|
+
'--color-cta-hover',
|
|
78
|
+
] as const
|
|
79
|
+
|
|
80
|
+
/** Build the `<html>` custom-property overrides for a palette. */
|
|
81
|
+
export function paletteCssVariables(palette: ColorPalette): Record<string, string> {
|
|
82
|
+
if (palette === 'default') return {}
|
|
83
|
+
const [primaryHue, accentHue] = colorPalettes[palette].hues
|
|
84
|
+
return {
|
|
85
|
+
'--color-primary': `hsl(${primaryHue} 84% 67%)`,
|
|
86
|
+
'--color-primary-hover': `hsl(${primaryHue} 75% 59%)`,
|
|
87
|
+
'--color-primary-soft': `hsl(${primaryHue} 84% 67% / 0.12)`,
|
|
88
|
+
'--color-cta': `hsl(${accentHue} 95% 53%)`,
|
|
89
|
+
'--color-cta-hover': `hsl(${accentHue} 90% 48%)`,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `DataFrame`-style size that each table density maps onto. */
|
|
94
|
+
const DENSITY_TABLE_SIZE: Record<TableDensity, 'sm' | 'md' | 'lg'> = {
|
|
95
|
+
compact: 'sm',
|
|
96
|
+
normal: 'md',
|
|
97
|
+
comfortable: 'lg',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Map a table density onto the `size` prop shared by the SDK table components. */
|
|
101
|
+
export function tableDensityToSize(density: TableDensity): 'sm' | 'md' | 'lg' {
|
|
102
|
+
return DENSITY_TABLE_SIZE[density]
|
|
103
|
+
}
|
|
104
|
+
|
|
65
105
|
function loadSettings(): SettingsState {
|
|
66
106
|
try {
|
|
67
107
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
@@ -152,6 +192,8 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
152
192
|
tableDensity.value = loaded.tableDensity
|
|
153
193
|
|
|
154
194
|
applyTheme()
|
|
195
|
+
applyPalette()
|
|
196
|
+
applyDensity()
|
|
155
197
|
}
|
|
156
198
|
|
|
157
199
|
function applyTheme() {
|
|
@@ -160,11 +202,36 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
160
202
|
document.documentElement.classList.toggle('dark', dark)
|
|
161
203
|
}
|
|
162
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Write the selected palette's hues onto `<html>` as custom-property
|
|
207
|
+
* overrides. The `default` palette clears them so `styles/variables.css`
|
|
208
|
+
* keeps ownership of the brand colors.
|
|
209
|
+
*/
|
|
210
|
+
function applyPalette() {
|
|
211
|
+
if (typeof document === 'undefined') return
|
|
212
|
+
const style = document.documentElement.style
|
|
213
|
+
const overrides = paletteCssVariables(colorPalette.value)
|
|
214
|
+
for (const name of PALETTE_CSS_VARIABLES) {
|
|
215
|
+
const value = overrides[name]
|
|
216
|
+
if (value) style.setProperty(name, value)
|
|
217
|
+
else style.removeProperty(name)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Mirror the table density onto `<html data-density>` for CSS consumers. */
|
|
222
|
+
function applyDensity() {
|
|
223
|
+
if (typeof document === 'undefined') return
|
|
224
|
+
document.documentElement.dataset.density = tableDensity.value
|
|
225
|
+
}
|
|
226
|
+
|
|
163
227
|
watch(theme, () => {
|
|
164
228
|
applyTheme()
|
|
165
229
|
persistSettings()
|
|
166
230
|
})
|
|
167
231
|
|
|
232
|
+
watch(colorPalette, applyPalette)
|
|
233
|
+
watch(tableDensity, applyDensity)
|
|
234
|
+
|
|
168
235
|
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
|
169
236
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
|
170
237
|
systemPrefersDark.value = event.matches
|
|
@@ -206,6 +273,11 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
206
273
|
return colorPalettes[colorPalette.value].hues
|
|
207
274
|
}
|
|
208
275
|
|
|
276
|
+
/** The `size` the SDK table components default to for the current density. */
|
|
277
|
+
function getTableSize(): 'sm' | 'md' | 'lg' {
|
|
278
|
+
return tableDensityToSize(tableDensity.value)
|
|
279
|
+
}
|
|
280
|
+
|
|
209
281
|
function isDark(): boolean {
|
|
210
282
|
return theme.value === 'system' ? systemPrefersDark.value : theme.value === 'dark'
|
|
211
283
|
}
|
|
@@ -222,11 +294,31 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
222
294
|
tableDensity,
|
|
223
295
|
initialize,
|
|
224
296
|
applyTheme,
|
|
297
|
+
applyPalette,
|
|
298
|
+
applyDensity,
|
|
225
299
|
persistSettings,
|
|
226
300
|
resetToDefaults,
|
|
227
301
|
getPaletteHues,
|
|
302
|
+
getTableSize,
|
|
228
303
|
isDark,
|
|
229
304
|
getApiBaseUrl,
|
|
230
305
|
getWsBaseUrl,
|
|
231
306
|
}
|
|
232
307
|
})
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Read the settings store without requiring Pinia to be installed.
|
|
311
|
+
*
|
|
312
|
+
* SDK components are mounted standalone (unit tests, Histoire stories, plugin
|
|
313
|
+
* previews) where no Pinia instance exists. They use this to pick up a user's
|
|
314
|
+
* preferences when the platform is running and fall back to their own defaults
|
|
315
|
+
* otherwise, instead of throwing.
|
|
316
|
+
*/
|
|
317
|
+
export function tryUseSettingsStore(): ReturnType<typeof useSettingsStore> | null {
|
|
318
|
+
if (!getActivePinia()) return null
|
|
319
|
+
try {
|
|
320
|
+
return useSettingsStore()
|
|
321
|
+
} catch {
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -126,11 +126,11 @@
|
|
|
126
126
|
margin-right: 0.125rem;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
/* Empty / loading / error placeholder host (content comes from EmptyState) */
|
|
130
|
+
.mint-audit-trail__state {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/* Size variants */
|
|
@@ -194,3 +194,10 @@
|
|
|
194
194
|
background-color: var(--bg-hover);
|
|
195
195
|
color: var(--text-primary);
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
/* Empty / loading / error placeholder host (content comes from EmptyState) */
|
|
199
|
+
.mint-batch-progress__state {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
border-radius: 50%;
|
|
62
62
|
background: white;
|
|
63
63
|
cursor: pointer;
|
|
64
|
-
border: 2px solid var(--
|
|
64
|
+
border: 2px solid var(--color-primary);
|
|
65
65
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
66
66
|
transition: transform 0.15s ease;
|
|
67
67
|
}
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
border-radius: 50%;
|
|
81
81
|
background: white;
|
|
82
82
|
cursor: pointer;
|
|
83
|
-
border: 2px solid var(--
|
|
83
|
+
border: 2px solid var(--color-primary);
|
|
84
84
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
85
85
|
transition: transform 0.15s ease;
|
|
86
86
|
}
|