@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,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Combined date and time picker with a dropdown calendar, time slots, and 12/24 h format support. */
|
|
3
|
-
import { computed, watch, nextTick } from 'vue'
|
|
3
|
+
import { computed, ref, watch, nextTick } from 'vue'
|
|
4
4
|
import { useCalendarGrid, type CalendarGridDay } from '../composables/useCalendarGrid'
|
|
5
5
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
6
|
+
import { useFocusTrap } from '../composables/useFocusTrap'
|
|
6
7
|
import { formatTime, formatTimeSlot, generateTimeSlots, toMinutes } from '../composables/useTimeUtils'
|
|
7
8
|
import CalendarGridPanelInternal from './internal/CalendarGridPanelInternal.vue'
|
|
8
9
|
|
|
@@ -35,7 +36,10 @@ const emit = defineEmits<{
|
|
|
35
36
|
'update:modelValue': [value: string | undefined]
|
|
36
37
|
}>()
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
+
const dropdownRef = ref<HTMLElement | null>(null)
|
|
40
|
+
const { isOpen, rootRef, triggerRef, open, close, toggle } = useDropdownState()
|
|
41
|
+
// Non-modal dialog: Tab stays inside the panel, focus returns to the input on close.
|
|
42
|
+
useFocusTrap({ containerRef: dropdownRef, active: isOpen })
|
|
39
43
|
const {
|
|
40
44
|
weekDays,
|
|
41
45
|
monthYear,
|
|
@@ -148,18 +152,42 @@ function toggleDropdown() {
|
|
|
148
152
|
toggle()
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
if (
|
|
155
|
+
function handleTriggerKeydown(event: KeyboardEvent) {
|
|
156
|
+
if (props.disabled || isOpen.value) return
|
|
157
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
open()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Moves focus into the calendar grid: selected day, else today, else the first day. */
|
|
164
|
+
function focusCalendar() {
|
|
165
|
+
const panel = dropdownRef.value
|
|
166
|
+
if (!panel) return
|
|
167
|
+
const target =
|
|
168
|
+
panel.querySelector<HTMLElement>('.mint-date-picker__day--selected') ??
|
|
169
|
+
panel.querySelector<HTMLElement>('.mint-date-picker__day--today') ??
|
|
170
|
+
panel.querySelector<HTMLElement>('.mint-date-picker__day:not([disabled])')
|
|
171
|
+
target?.focus()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
watch(isOpen, async (opened) => {
|
|
175
|
+
if (!opened) return
|
|
176
|
+
if (selectedDate.value) {
|
|
153
177
|
setCurrentMonthFromDate(selectedDate.value)
|
|
154
|
-
nextTick(() => {
|
|
155
|
-
// Scroll time grid to selected time
|
|
156
|
-
const grid = rootRef.value?.querySelector('.mint-datetime-picker__time-grid')
|
|
157
|
-
const active = rootRef.value?.querySelector('.mint-datetime-picker__time-chip--active')
|
|
158
|
-
if (grid && active) {
|
|
159
|
-
active.scrollIntoView({ block: 'center' })
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
178
|
}
|
|
179
|
+
await nextTick()
|
|
180
|
+
if (selectedDate.value) {
|
|
181
|
+
// Scroll time grid to selected time
|
|
182
|
+
const grid = dropdownRef.value?.querySelector('.mint-datetime-picker__time-grid')
|
|
183
|
+
const active = dropdownRef.value?.querySelector('.mint-datetime-picker__time-chip--active')
|
|
184
|
+
if (grid && active) {
|
|
185
|
+
active.scrollIntoView({ block: 'center' })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// A second tick so this focus lands after the focus trap's initial focus.
|
|
189
|
+
await nextTick()
|
|
190
|
+
focusCalendar()
|
|
163
191
|
})
|
|
164
192
|
</script>
|
|
165
193
|
|
|
@@ -172,8 +200,11 @@ watch(isOpen, (open) => {
|
|
|
172
200
|
</svg>
|
|
173
201
|
</div>
|
|
174
202
|
<input
|
|
203
|
+
ref="triggerRef"
|
|
175
204
|
type="text"
|
|
176
205
|
readonly
|
|
206
|
+
aria-haspopup="dialog"
|
|
207
|
+
:aria-expanded="isOpen"
|
|
177
208
|
:value="displayValue"
|
|
178
209
|
:placeholder="placeholder"
|
|
179
210
|
:disabled="disabled"
|
|
@@ -185,6 +216,7 @@ watch(isOpen, (open) => {
|
|
|
185
216
|
]"
|
|
186
217
|
aria-label="Select date and time"
|
|
187
218
|
@click="toggleDropdown"
|
|
219
|
+
@keydown="handleTriggerKeydown"
|
|
188
220
|
/>
|
|
189
221
|
<div class="mint-datetime-picker__icon-clock">
|
|
190
222
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -203,9 +235,9 @@ watch(isOpen, (open) => {
|
|
|
203
235
|
>
|
|
204
236
|
<div
|
|
205
237
|
v-if="isOpen"
|
|
238
|
+
ref="dropdownRef"
|
|
206
239
|
class="mint-datetime-picker__dropdown"
|
|
207
240
|
role="dialog"
|
|
208
|
-
aria-modal="true"
|
|
209
241
|
aria-label="Date and time picker"
|
|
210
242
|
>
|
|
211
243
|
<!-- Calendar section -->
|
|
@@ -265,7 +297,3 @@ watch(isOpen, (open) => {
|
|
|
265
297
|
</Transition>
|
|
266
298
|
</div>
|
|
267
299
|
</template>
|
|
268
|
-
|
|
269
|
-
<style>
|
|
270
|
-
@import '../styles/components/datetime-picker.css';
|
|
271
|
-
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Button that opens a dropdown menu for selecting one option from a list. */
|
|
3
|
-
import { computed,
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
4
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
5
5
|
import { useMenuKeyboard } from '../composables/useMenuKeyboard'
|
|
6
6
|
import type { SelectOption, SelectOptionInput, ButtonVariant, ButtonSize } from '../types'
|
|
@@ -54,12 +54,11 @@ const emit = defineEmits<{
|
|
|
54
54
|
select: [option: SelectOption<string | number>]
|
|
55
55
|
}>()
|
|
56
56
|
|
|
57
|
-
const { isOpen, rootRef, open, close
|
|
57
|
+
const { isOpen, rootRef, triggerRef, open, close, toggle: toggleDropdown } = useDropdownState()
|
|
58
58
|
const normalizedOptions = computed<SelectOption<string | number>[]>(() =>
|
|
59
59
|
props.options.map(normalizeOptionInput)
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
const triggerRef = ref<HTMLElement | null>(null)
|
|
63
62
|
const panelRef = ref<HTMLElement | null>(null)
|
|
64
63
|
|
|
65
64
|
function optionItems(): HTMLElement[] {
|
|
@@ -67,13 +66,6 @@ function optionItems(): HTMLElement[] {
|
|
|
67
66
|
return Array.from(panelRef.value.querySelectorAll<HTMLElement>('[role="option"]:not([aria-disabled="true"])'))
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function close(returnFocus = false) {
|
|
71
|
-
closeDropdown()
|
|
72
|
-
if (returnFocus) {
|
|
73
|
-
void nextTick(() => triggerRef.value?.focus())
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
69
|
const { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({
|
|
78
70
|
items: optionItems,
|
|
79
71
|
isOpen: () => isOpen.value,
|
|
@@ -187,7 +179,3 @@ function selectOption(option: SelectOption<string | number>) {
|
|
|
187
179
|
</Transition>
|
|
188
180
|
</div>
|
|
189
181
|
</template>
|
|
190
|
-
|
|
191
|
-
<style>
|
|
192
|
-
@import '../styles/components/dropdown-button.css';
|
|
193
|
-
</style>
|
|
@@ -96,6 +96,7 @@ const views = ['summary', 'tree', 'table'] as const
|
|
|
96
96
|
<template #default="{ state }">
|
|
97
97
|
<div style="padding: 1rem; max-width: 800px; margin: 0 auto;">
|
|
98
98
|
<ExperimentDataViewer
|
|
99
|
+
:error="state.error || null"
|
|
99
100
|
:tree-data="treeData"
|
|
100
101
|
:table-data="tableData"
|
|
101
102
|
:summary-data="state.showSummary ? summaryData : null"
|
|
@@ -107,6 +108,8 @@ const views = ['summary', 'tree', 'table'] as const
|
|
|
107
108
|
</template>
|
|
108
109
|
|
|
109
110
|
<template #controls="{ state }">
|
|
111
|
+
|
|
112
|
+
<HstText v-model="state.error" title="Error Message" />
|
|
110
113
|
<HstSelect
|
|
111
114
|
v-model="state.defaultView"
|
|
112
115
|
title="Default View"
|
|
@@ -8,6 +8,7 @@ import DataFrame from './DataFrame.vue'
|
|
|
8
8
|
import SegmentedControl from './SegmentedControl.vue'
|
|
9
9
|
import BaseButton from './BaseButton.vue'
|
|
10
10
|
import Skeleton from './Skeleton.vue'
|
|
11
|
+
import DataStateInternal from './internal/DataStateInternal.vue'
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
treeData?: TreeNode[]
|
|
@@ -20,6 +21,11 @@ interface Props {
|
|
|
20
21
|
pluginRoutePrefix?: string
|
|
21
22
|
experimentId?: number
|
|
22
23
|
loading?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* Error message. When set (or when the auto-fetch fails), the content area
|
|
26
|
+
* renders an error state instead of the data views.
|
|
27
|
+
*/
|
|
28
|
+
error?: string | null
|
|
23
29
|
downloadJsonUrl?: string
|
|
24
30
|
downloadCsvUrl?: string
|
|
25
31
|
autoFetch?: boolean
|
|
@@ -29,6 +35,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
29
35
|
title: 'Data',
|
|
30
36
|
defaultView: 'summary',
|
|
31
37
|
loading: false,
|
|
38
|
+
error: null,
|
|
32
39
|
autoFetch: true,
|
|
33
40
|
})
|
|
34
41
|
|
|
@@ -61,6 +68,9 @@ const mergedSummaryData = computed(() =>
|
|
|
61
68
|
const isLoading = computed(() =>
|
|
62
69
|
props.loading || expData.isLoading.value,
|
|
63
70
|
)
|
|
71
|
+
const activeError = computed<string | null>(() =>
|
|
72
|
+
props.error ?? expData.error.value,
|
|
73
|
+
)
|
|
64
74
|
|
|
65
75
|
const emit = defineEmits<{
|
|
66
76
|
'open-plugin': []
|
|
@@ -179,13 +189,20 @@ function handleDownloadCsv() {
|
|
|
179
189
|
</div>
|
|
180
190
|
|
|
181
191
|
<div class="mint-data-viewer__content">
|
|
182
|
-
<div v-if="
|
|
183
|
-
<
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
<div v-if="activeError" class="mint-data-viewer__state">
|
|
193
|
+
<slot name="error" :error="activeError">
|
|
194
|
+
<DataStateInternal state="error" :title="activeError" />
|
|
195
|
+
</slot>
|
|
196
|
+
</div>
|
|
197
|
+
<div v-else-if="isLoading" class="mint-data-viewer__loading">
|
|
198
|
+
<slot name="loading">
|
|
199
|
+
<div class="mint-data-viewer__skeleton">
|
|
200
|
+
<Skeleton width="40%" height="16px" />
|
|
201
|
+
<Skeleton width="100%" height="12px" />
|
|
202
|
+
<Skeleton width="100%" height="12px" />
|
|
203
|
+
<Skeleton width="75%" height="12px" />
|
|
204
|
+
</div>
|
|
205
|
+
</slot>
|
|
189
206
|
</div>
|
|
190
207
|
<template v-else>
|
|
191
208
|
<!-- Summary View -->
|
|
@@ -280,14 +297,17 @@ function handleDownloadCsv() {
|
|
|
280
297
|
:searchable="true"
|
|
281
298
|
:sortable="true"
|
|
282
299
|
/>
|
|
283
|
-
<div v-else-if="viewMode === 'table'" class="mint-data-
|
|
284
|
-
|
|
300
|
+
<div v-else-if="viewMode === 'table'" class="mint-data-viewer__state">
|
|
301
|
+
<slot name="empty">
|
|
302
|
+
<DataStateInternal
|
|
303
|
+
state="empty"
|
|
304
|
+
title="No tabular data available"
|
|
305
|
+
description="Switch to the tree view to inspect this experiment."
|
|
306
|
+
size="sm"
|
|
307
|
+
/>
|
|
308
|
+
</slot>
|
|
285
309
|
</div>
|
|
286
310
|
</template>
|
|
287
311
|
</div>
|
|
288
312
|
</div>
|
|
289
313
|
</template>
|
|
290
|
-
|
|
291
|
-
<style>
|
|
292
|
-
@import '../styles/components/experiment-data-viewer.css';
|
|
293
|
-
</style>
|
|
@@ -57,7 +57,7 @@ const emit = defineEmits<{
|
|
|
57
57
|
detach: []
|
|
58
58
|
}>()
|
|
59
59
|
|
|
60
|
-
const { isOpen, rootRef: popoverRef, close, toggle } = useDropdownState({
|
|
60
|
+
const { isOpen, rootRef: popoverRef, triggerRef, close, toggle } = useDropdownState({
|
|
61
61
|
closeOnEscape: false,
|
|
62
62
|
})
|
|
63
63
|
const showSuccess = ref(false)
|
|
@@ -118,6 +118,7 @@ onUnmounted(() => {
|
|
|
118
118
|
<!-- Left: experiment trigger (opens popover). Prefer the human-readable
|
|
119
119
|
name; the formal code, when present, stays in the detail panel. -->
|
|
120
120
|
<button
|
|
121
|
+
ref="triggerRef"
|
|
121
122
|
type="button"
|
|
122
123
|
:class="[
|
|
123
124
|
'mint-experiment-popover__trigger',
|
|
@@ -306,7 +307,3 @@ onUnmounted(() => {
|
|
|
306
307
|
/>
|
|
307
308
|
</div>
|
|
308
309
|
</template>
|
|
309
|
-
|
|
310
|
-
<style>
|
|
311
|
-
@import '../styles/components/experiment-popover.css';
|
|
312
|
-
</style>
|
|
@@ -57,6 +57,8 @@ const mockSteps: ProtocolStep[] = [
|
|
|
57
57
|
|
|
58
58
|
function initState() {
|
|
59
59
|
return {
|
|
60
|
+
loading: false,
|
|
61
|
+
error: '',
|
|
60
62
|
modelValue: mockSteps,
|
|
61
63
|
orientation: 'vertical' as const,
|
|
62
64
|
showDuration: true,
|
|
@@ -76,6 +78,8 @@ function initState() {
|
|
|
76
78
|
<template #default="{ state }">
|
|
77
79
|
<div style="padding: 2rem; max-width: 600px;">
|
|
78
80
|
<ExperimentTimeline
|
|
81
|
+
:loading="state.loading"
|
|
82
|
+
:error="state.error || null"
|
|
79
83
|
v-model="state.modelValue"
|
|
80
84
|
:orientation="state.orientation"
|
|
81
85
|
:show-duration="state.showDuration"
|
|
@@ -89,6 +93,8 @@ function initState() {
|
|
|
89
93
|
</div>
|
|
90
94
|
</template>
|
|
91
95
|
<template #controls="{ state }">
|
|
96
|
+
<HstCheckbox v-model="state.loading" title="Loading" />
|
|
97
|
+
<HstText v-model="state.error" title="Error Message" />
|
|
92
98
|
<HstSelect
|
|
93
99
|
v-model="state.orientation"
|
|
94
100
|
title="Orientation"
|
|
@@ -157,5 +163,17 @@ function initState() {
|
|
|
157
163
|
<ExperimentTimeline :model-value="[]" editable />
|
|
158
164
|
</div>
|
|
159
165
|
</Variant>
|
|
166
|
+
|
|
167
|
+
<Variant title="Loading">
|
|
168
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
169
|
+
<ExperimentTimeline :model-value="[]" loading />
|
|
170
|
+
</div>
|
|
171
|
+
</Variant>
|
|
172
|
+
|
|
173
|
+
<Variant title="Error">
|
|
174
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
175
|
+
<ExperimentTimeline :model-value="[]" error="Could not load the protocol." />
|
|
176
|
+
</div>
|
|
177
|
+
</Variant>
|
|
160
178
|
</Story>
|
|
161
179
|
</template>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Horizontal or vertical timeline of ordered protocol steps with status indicators, drag-to-reorder, and step-type iconography. */
|
|
3
|
-
import { ref, computed } from 'vue'
|
|
3
|
+
import { ref, computed, type ComponentPublicInstance } from 'vue'
|
|
4
4
|
import type { ProtocolStep, ProtocolStepStatus } from '../types'
|
|
5
|
+
import { useListReorder } from '../composables/useListReorder'
|
|
5
6
|
import {
|
|
6
7
|
PROTOCOL_STEP_TYPE_COLORS as stepTypeColors,
|
|
7
8
|
PROTOCOL_STEP_TYPE_ICONS as stepTypeIcons,
|
|
8
9
|
formatProtocolDuration as formatDuration,
|
|
9
10
|
} from './ProtocolStep.presentation'
|
|
11
|
+
import DataStateInternal from './internal/DataStateInternal.vue'
|
|
10
12
|
|
|
11
13
|
interface Props {
|
|
12
14
|
modelValue?: ProtocolStep[]
|
|
@@ -19,6 +21,13 @@ interface Props {
|
|
|
19
21
|
size?: 'sm' | 'md' | 'lg'
|
|
20
22
|
colorByStatus?: boolean
|
|
21
23
|
colorByType?: boolean
|
|
24
|
+
/** Replace the timeline with a loading placeholder. */
|
|
25
|
+
loading?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Error message. When set, the timeline is replaced by an error state.
|
|
28
|
+
* `null`/`undefined` renders normally.
|
|
29
|
+
*/
|
|
30
|
+
error?: string | null
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -32,6 +41,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
32
41
|
size: 'md',
|
|
33
42
|
colorByStatus: true,
|
|
34
43
|
colorByType: false,
|
|
44
|
+
loading: false,
|
|
45
|
+
error: null,
|
|
35
46
|
})
|
|
36
47
|
|
|
37
48
|
const emit = defineEmits<{
|
|
@@ -46,8 +57,7 @@ const emit = defineEmits<{
|
|
|
46
57
|
}>()
|
|
47
58
|
|
|
48
59
|
const localExpandedId = ref<string | undefined>(props.expandedStepId)
|
|
49
|
-
const
|
|
50
|
-
const dragOverStepId = ref<string | null>(null)
|
|
60
|
+
const stepRefs = new Map<string, HTMLElement>()
|
|
51
61
|
|
|
52
62
|
const expandedId = computed({
|
|
53
63
|
get: () => props.expandedStepId ?? localExpandedId.value,
|
|
@@ -123,54 +133,38 @@ function handleAddStep(afterStepId?: string) {
|
|
|
123
133
|
emit('step-add', afterStepId)
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
136
|
+
// Steps are a plain ordered list, so pointer drag and the Alt+Arrow keyboard
|
|
137
|
+
// alternative both come from the shared reorder composable.
|
|
138
|
+
const {
|
|
139
|
+
draggingId: draggedStepId,
|
|
140
|
+
dragOverId: dragOverStepId,
|
|
141
|
+
announcement: reorderAnnouncement,
|
|
142
|
+
announcerStyle,
|
|
143
|
+
onDragStart: onStepDragStart,
|
|
144
|
+
onDragOver: onStepDragOver,
|
|
145
|
+
onDragLeave: onStepDragLeave,
|
|
146
|
+
onDrop: onStepDrop,
|
|
147
|
+
onDragEnd: onStepDragEnd,
|
|
148
|
+
onKeydown: onStepReorderKeydown,
|
|
149
|
+
} = useListReorder<ProtocolStep>({
|
|
150
|
+
items: () => sortedSteps.value,
|
|
151
|
+
getId: (step) => step.id,
|
|
152
|
+
getLabel: (step) => step.name,
|
|
153
|
+
disabled: () => !props.editable,
|
|
154
|
+
getElement: (id) => stepRefs.get(id),
|
|
155
|
+
itemNoun: 'Step',
|
|
156
|
+
onReorder: ({ toIndex, items }) => {
|
|
157
|
+
emit('step-reorder', items[toIndex].id, toIndex)
|
|
158
|
+
emit('update:modelValue', items.map((step, index) => ({ ...step, order: index })))
|
|
159
|
+
},
|
|
160
|
+
})
|
|
148
161
|
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
function setStepRef(id: string, el: Element | ComponentPublicInstance | null) {
|
|
163
|
+
if (el instanceof HTMLElement) {
|
|
164
|
+
stepRefs.set(id, el)
|
|
165
|
+
} else {
|
|
166
|
+
stepRefs.delete(id)
|
|
154
167
|
}
|
|
155
|
-
|
|
156
|
-
const steps = [...props.modelValue]
|
|
157
|
-
const draggedIndex = steps.findIndex(s => s.id === draggedStepId.value)
|
|
158
|
-
const targetIndex = steps.findIndex(s => s.id === targetStep.id)
|
|
159
|
-
|
|
160
|
-
steps.splice(draggedIndex, 1)
|
|
161
|
-
steps.splice(targetIndex, 0, draggedStep)
|
|
162
|
-
|
|
163
|
-
const reordered = steps.map((s, i) => ({ ...s, order: i }))
|
|
164
|
-
emit('step-reorder', draggedStepId.value, targetIndex)
|
|
165
|
-
emit('update:modelValue', reordered)
|
|
166
|
-
|
|
167
|
-
draggedStepId.value = null
|
|
168
|
-
dragOverStepId.value = null
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function handleDragEnd() {
|
|
172
|
-
draggedStepId.value = null
|
|
173
|
-
dragOverStepId.value = null
|
|
174
168
|
}
|
|
175
169
|
|
|
176
170
|
</script>
|
|
@@ -182,16 +176,22 @@ function handleDragEnd() {
|
|
|
182
176
|
:aria-label="`Protocol timeline with ${sortedSteps.length} steps`"
|
|
183
177
|
>
|
|
184
178
|
<div
|
|
185
|
-
v-for="(step, index) in sortedSteps"
|
|
179
|
+
v-for="(step, index) in (loading || error ? [] : sortedSteps)"
|
|
186
180
|
:key="step.id"
|
|
181
|
+
:ref="(el) => setStepRef(step.id, el)"
|
|
187
182
|
class="mint-timeline__step"
|
|
188
183
|
role="listitem"
|
|
189
184
|
:draggable="editable"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@
|
|
185
|
+
:tabindex="editable ? 0 : undefined"
|
|
186
|
+
:aria-label="editable
|
|
187
|
+
? `${step.name}, step ${index + 1} of ${sortedSteps.length}. Press Alt with the arrow keys to reorder.`
|
|
188
|
+
: undefined"
|
|
189
|
+
@dragstart="onStepDragStart($event, step, index)"
|
|
190
|
+
@dragover="onStepDragOver($event, step, index)"
|
|
191
|
+
@dragleave="onStepDragLeave"
|
|
192
|
+
@drop="onStepDrop($event, step, index)"
|
|
193
|
+
@dragend="onStepDragEnd"
|
|
194
|
+
@keydown="onStepReorderKeydown($event, step, index)"
|
|
195
195
|
>
|
|
196
196
|
<div
|
|
197
197
|
v-if="index < sortedSteps.length - 1"
|
|
@@ -321,25 +321,32 @@ function handleDragEnd() {
|
|
|
321
321
|
<div v-if="orientation === 'horizontal' && index < sortedSteps.length - 1" class="mint-timeline__spacer--horizontal" />
|
|
322
322
|
</div>
|
|
323
323
|
|
|
324
|
-
<div
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
324
|
+
<div v-if="error" class="mint-timeline__state">
|
|
325
|
+
<slot name="error" :error="error">
|
|
326
|
+
<DataStateInternal state="error" size="sm" :title="error" />
|
|
327
|
+
</slot>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div v-else-if="loading" class="mint-timeline__state">
|
|
331
|
+
<slot name="loading">
|
|
332
|
+
<DataStateInternal state="loading" size="sm" loading-label="Loading protocol" />
|
|
333
|
+
</slot>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div v-else-if="sortedSteps.length === 0" class="mint-timeline__state">
|
|
337
|
+
<slot name="empty">
|
|
338
|
+
<DataStateInternal
|
|
339
|
+
state="empty"
|
|
340
|
+
size="sm"
|
|
341
|
+
title="No protocol steps defined"
|
|
342
|
+
:action-label="editable ? 'Add First Step' : undefined"
|
|
343
|
+
@action="handleAddStep()"
|
|
344
|
+
/>
|
|
345
|
+
</slot>
|
|
339
346
|
</div>
|
|
340
347
|
|
|
341
348
|
<button
|
|
342
|
-
v-if="editable && sortedSteps.length > 0"
|
|
349
|
+
v-if="editable && !loading && !error && sortedSteps.length > 0"
|
|
343
350
|
type="button"
|
|
344
351
|
:class="['mint-timeline__add-end', `mint-timeline__add-end--${size}`]"
|
|
345
352
|
@click="handleAddStep()"
|
|
@@ -349,9 +356,8 @@ function handleDragEnd() {
|
|
|
349
356
|
</svg>
|
|
350
357
|
<span :class="`mint-timeline__add-end-text--${size}`">Add Step</span>
|
|
351
358
|
</button>
|
|
359
|
+
|
|
360
|
+
<!-- Reorder announcements for screen readers -->
|
|
361
|
+
<div role="status" aria-live="polite" :style="announcerStyle">{{ reorderAnnouncement }}</div>
|
|
352
362
|
</div>
|
|
353
363
|
</template>
|
|
354
|
-
|
|
355
|
-
<style>
|
|
356
|
-
@import '../styles/components/experiment-timeline.css';
|
|
357
|
-
</style>
|