@morscherlab/mint-sdk 1.4.0-beta.19 → 1.4.0-beta.20
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/dist/index.js +2 -2
- package/dist/{install-C5psB1H0.js → install-qjELyNwc.js} +2 -2
- package/dist/{install-C5psB1H0.js.map → install-qjELyNwc.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-BHVT1qN2.js → lab-UyTbokQC.js} +58 -51
- package/dist/{lab-BHVT1qN2.js.map → lab-UyTbokQC.js.map} +1 -1
- package/dist/lab.js +1 -1
- package/dist/styles.css +128 -21
- package/package.json +1 -1
- package/src/__tests__/components/SampleSelector.test.ts +20 -0
- package/src/__tests__/styles/cssTokens.test.ts +4 -1
- package/src/components/LayoutResizeHandle.story.vue +65 -4
- package/src/components/SampleSelector.story.vue +21 -6
- package/src/components/SampleSelector.vue +10 -3
- package/src/components/SectionCard.story.vue +33 -0
- package/src/styles/components/layout-resize-handle.css +59 -16
- package/src/styles/components/sample-selector.css +43 -0
- package/src/styles/components/section-card.css +26 -4
package/dist/lab.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { P as time_exports } from "./records-CR_pYzIP.js";
|
|
2
2
|
import { C as FormulaInput_default, L as definePlateEditor, S as useSequenceUtils, a as StepWizard_default, b as MoleculeInput_default, v as ConcentrationInput_default, w as formula_exports, x as SequenceInput_default, y as useConcentrationUnits, z as defineWellPlate } from "./useListReorder-CQ9OCScc.js";
|
|
3
|
-
import { C as defineWellPlateControlProps, S as defineDoseDesignControlModel, T as defineWellPlateDoseControlProps, _ as SampleSelector_default, a as useReagentSeries, c as InstrumentStatusPanels_default, d as ChemicalFormula_default, f as AdductText_default, g as SampleHierarchyTree_default, h as useProtocolTemplates, i as generateDilutionSeries, l as InstrumentStateBadge_default, m as ProtocolStepEditor_default, n as DEFAULT_PRESETS, o as ResourceCard_default, p as ScientificNumber_default, r as DEFAULT_UNITS, s as TimeRangeInput_default, u as SequenceProgressBar_default, v as useSampleGroups, w as defineWellPlateDoseComponentBindings, x as defineDoseCalculatorControlProps } from "./lab-
|
|
3
|
+
import { C as defineWellPlateControlProps, S as defineDoseDesignControlModel, T as defineWellPlateDoseControlProps, _ as SampleSelector_default, a as useReagentSeries, c as InstrumentStatusPanels_default, d as ChemicalFormula_default, f as AdductText_default, g as SampleHierarchyTree_default, h as useProtocolTemplates, i as generateDilutionSeries, l as InstrumentStateBadge_default, m as ProtocolStepEditor_default, n as DEFAULT_PRESETS, o as ResourceCard_default, p as ScientificNumber_default, r as DEFAULT_UNITS, s as TimeRangeInput_default, u as SequenceProgressBar_default, v as useSampleGroups, w as defineWellPlateDoseComponentBindings, x as defineDoseCalculatorControlProps } from "./lab-UyTbokQC.js";
|
|
4
4
|
import { C as hexToHsl, E as sampleTypeMarker, S as deriveShade, T as WellPlate_default, _ as mintPlotlyTemplate, b as PlateEditor_default, g as ChartContainer_default, h as PlotlyChart_default, n as ScheduleCalendar_default, r as useScheduleDrag, t as useRackEditor, v as DoseCalculator_default, w as hslToHex, y as dose_exports } from "./useRackEditor-wL3kPrRT.js";
|
|
5
5
|
export { AdductText_default as AdductText, ChartContainer_default as ChartContainer, ChemicalFormula_default as ChemicalFormula, ConcentrationInput_default as ConcentrationInput, DEFAULT_PRESETS, DEFAULT_UNITS, DoseCalculator_default as DoseCalculator, FormulaInput_default as FormulaInput, InstrumentStateBadge_default as InstrumentStateBadge, InstrumentStatusPanels_default as InstrumentStatusPanels, MoleculeInput_default as MoleculeInput, PlateEditor_default as PlateEditor, PlotlyChart_default as PlotlyChart, ProtocolStepEditor_default as ProtocolStepEditor, ResourceCard_default as ResourceCard, SampleHierarchyTree_default as SampleHierarchyTree, SampleSelector_default as SampleSelector, ScheduleCalendar_default as ScheduleCalendar, ScientificNumber_default as ScientificNumber, SequenceInput_default as SequenceInput, SequenceProgressBar_default as SequenceProgressBar, StepWizard_default as StepWizard, TimeRangeInput_default as TimeRangeInput, WellPlate_default as WellPlate, defineDoseCalculatorControlProps, defineDoseDesignControlModel, definePlateEditor, defineWellPlate, defineWellPlateControlProps, defineWellPlateDoseComponentBindings, defineWellPlateDoseControlProps, deriveShade, dose_exports as dose, formula_exports as formula, generateDilutionSeries, hexToHsl, hslToHex, mintPlotlyTemplate, sampleTypeMarker, time_exports as time, useConcentrationUnits, useProtocolTemplates, useRackEditor, useReagentSeries, useSampleGroups, useScheduleDrag, useSequenceUtils };
|
package/dist/styles.css
CHANGED
|
@@ -4619,29 +4619,75 @@ button, input, select, textarea {
|
|
|
4619
4619
|
.mint-modal:has(.mint-jobs-tray__confirm) {
|
|
4620
4620
|
z-index: 1001;
|
|
4621
4621
|
}
|
|
4622
|
+
/* The handle is the gap between two panels: its layout width (horizontal: height)
|
|
4623
|
+
is the gap, --mint-layout-resize-gap or else --mint-gutter (8px compact, 12px
|
|
4624
|
+
normal). It stretches along the panels, and its bar and a 12px hit area are
|
|
4625
|
+
centred in it, the hit area drawn above the neighbouring panels. */
|
|
4622
4626
|
.mint-layout-resize-handle {
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
+
--_size: var(--mint-layout-resize-gap, var(--mint-gutter, 0.75rem));
|
|
4628
|
+
--_hit: 0.75rem;
|
|
4629
|
+
position: relative;
|
|
4630
|
+
z-index: 10;
|
|
4631
|
+
flex: 0 0 auto;
|
|
4632
|
+
align-self: stretch;
|
|
4627
4633
|
background: transparent;
|
|
4628
4634
|
cursor: col-resize;
|
|
4635
|
+
outline: none;
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
.mint-layout-resize-handle--vertical {
|
|
4639
|
+
justify-self: center;
|
|
4640
|
+
width: var(--_size);
|
|
4641
|
+
min-height: 2.75rem;
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
.mint-layout-resize-handle--horizontal {
|
|
4645
|
+
justify-self: stretch;
|
|
4646
|
+
min-width: 2.75rem;
|
|
4647
|
+
height: var(--_size);
|
|
4648
|
+
cursor: row-resize;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
/* Hit area and focus ring. Pointer events on it go to the handle. */
|
|
4652
|
+
.mint-layout-resize-handle::before {
|
|
4653
|
+
position: absolute;
|
|
4654
|
+
border-radius: var(--radius-sm);
|
|
4655
|
+
content: '';
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
.mint-layout-resize-handle--vertical::before {
|
|
4659
|
+
top: 0;
|
|
4660
|
+
bottom: 0;
|
|
4661
|
+
left: 50%;
|
|
4662
|
+
width: var(--_hit);
|
|
4663
|
+
transform: translateX(-50%);
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4666
|
+
.mint-layout-resize-handle--horizontal::before {
|
|
4667
|
+
top: 50%;
|
|
4668
|
+
right: 0;
|
|
4669
|
+
left: 0;
|
|
4670
|
+
height: var(--_hit);
|
|
4671
|
+
transform: translateY(-50%);
|
|
4629
4672
|
}
|
|
4630
4673
|
|
|
4631
|
-
|
|
4632
|
-
.mint-layout-resize-handle--horizontal { min-width: 2.75rem; height: 1rem; cursor: row-resize; }
|
|
4674
|
+
/* Edge handles lie outside the edge and cover exactly the gap. */
|
|
4633
4675
|
.mint-layout-resize-handle--edge-left,
|
|
4634
|
-
.mint-layout-resize-handle--edge-right { position: absolute; top: 0; bottom: 0;
|
|
4676
|
+
.mint-layout-resize-handle--edge-right { position: absolute; top: 0; bottom: 0; }
|
|
4635
4677
|
.mint-layout-resize-handle--edge-left { left: 0; transform: translateX(-100%); }
|
|
4636
4678
|
.mint-layout-resize-handle--edge-right { right: 0; transform: translateX(100%); }
|
|
4637
4679
|
|
|
4638
4680
|
.mint-layout-resize-handle__bar {
|
|
4681
|
+
position: absolute;
|
|
4682
|
+
top: 50%;
|
|
4683
|
+
left: 50%;
|
|
4639
4684
|
display: block;
|
|
4640
4685
|
width: 0.25rem;
|
|
4641
4686
|
height: 2.75rem;
|
|
4642
4687
|
border-radius: 999px;
|
|
4643
4688
|
background: var(--border-color);
|
|
4644
4689
|
opacity: 0.72;
|
|
4690
|
+
transform: translate(-50%, -50%);
|
|
4645
4691
|
transition: background-color 150ms ease, opacity 150ms ease, transform 150ms ease;
|
|
4646
4692
|
}
|
|
4647
4693
|
|
|
@@ -4657,29 +4703,25 @@ button, input, select, textarea {
|
|
|
4657
4703
|
}
|
|
4658
4704
|
|
|
4659
4705
|
.mint-layout-resize-handle--vertical:hover .mint-layout-resize-handle__bar,
|
|
4660
|
-
.mint-layout-resize-handle--vertical:focus-visible .mint-layout-resize-handle__bar { transform: scaleX(1.35); }
|
|
4706
|
+
.mint-layout-resize-handle--vertical:focus-visible .mint-layout-resize-handle__bar { transform: translate(-50%, -50%) scaleX(1.35); }
|
|
4661
4707
|
.mint-layout-resize-handle--horizontal:hover .mint-layout-resize-handle__bar,
|
|
4662
|
-
.mint-layout-resize-handle--horizontal:focus-visible .mint-layout-resize-handle__bar { transform: scaleY(1.35); }
|
|
4708
|
+
.mint-layout-resize-handle--horizontal:focus-visible .mint-layout-resize-handle__bar { transform: translate(-50%, -50%) scaleY(1.35); }
|
|
4663
4709
|
|
|
4664
|
-
.mint-layout-resize-handle:focus-visible {
|
|
4710
|
+
.mint-layout-resize-handle:focus-visible::before {
|
|
4665
4711
|
outline: 2px solid var(--color-primary);
|
|
4666
|
-
outline-offset: 2px;
|
|
4712
|
+
outline-offset: -2px;
|
|
4667
4713
|
}
|
|
4668
4714
|
|
|
4669
|
-
.mint-layout-resize-handle--edge-left:focus-visible,
|
|
4670
|
-
.mint-layout-resize-handle--edge-right:focus-visible { outline-offset: -2px; }
|
|
4671
|
-
|
|
4672
4715
|
.mint-layout-resize-handle[aria-disabled='true'] { cursor: not-allowed; opacity: 0.5; }
|
|
4673
4716
|
|
|
4674
4717
|
@media (prefers-reduced-motion: reduce) {
|
|
4675
4718
|
.mint-layout-resize-handle__bar { transition: none; }
|
|
4676
4719
|
}
|
|
4677
4720
|
|
|
4721
|
+
/* Touch: the handle reserves its full 2.75rem hit area in the layout. */
|
|
4678
4722
|
@media (pointer: coarse) {
|
|
4679
|
-
.mint-layout-resize-handle
|
|
4680
|
-
.mint-layout-resize-handle--horizontal { height: 2.75rem; }
|
|
4723
|
+
.mint-layout-resize-handle { --_size: 2.75rem; --_hit: 2.75rem; }
|
|
4681
4724
|
}
|
|
4682
|
-
|
|
4683
4725
|
/* BaseInput Component Styles */
|
|
4684
4726
|
|
|
4685
4727
|
.mint-input {
|
|
@@ -6789,6 +6831,11 @@ html.dark {
|
|
|
6789
6831
|
*/
|
|
6790
6832
|
|
|
6791
6833
|
.mint-section-card {
|
|
6834
|
+
/* The left grid that the header and the rows share: header icon (or row
|
|
6835
|
+
lead) width, then this gap. Set --mint-section-card-gap on the card to
|
|
6836
|
+
change both. */
|
|
6837
|
+
--mint-section-card-lead-width: 1rem;
|
|
6838
|
+
--mint-section-card-gap: 0.625rem;
|
|
6792
6839
|
display: flex;
|
|
6793
6840
|
flex-direction: column;
|
|
6794
6841
|
border-radius: var(--radius-lg);
|
|
@@ -6814,7 +6861,7 @@ html.dark {
|
|
|
6814
6861
|
.mint-section-card__header {
|
|
6815
6862
|
display: flex;
|
|
6816
6863
|
align-items: center;
|
|
6817
|
-
gap:
|
|
6864
|
+
gap: var(--mint-section-card-gap);
|
|
6818
6865
|
box-sizing: border-box;
|
|
6819
6866
|
min-height: 2.5rem;
|
|
6820
6867
|
/* 4px block padding: a 28px header control plus padding plus the 1px rule
|
|
@@ -6834,8 +6881,8 @@ html.dark {
|
|
|
6834
6881
|
align-items: center;
|
|
6835
6882
|
justify-content: center;
|
|
6836
6883
|
flex-shrink: 0;
|
|
6837
|
-
width:
|
|
6838
|
-
height:
|
|
6884
|
+
width: var(--mint-section-card-lead-width);
|
|
6885
|
+
height: var(--mint-section-card-lead-width);
|
|
6839
6886
|
color: var(--text-muted);
|
|
6840
6887
|
}
|
|
6841
6888
|
|
|
@@ -6895,12 +6942,27 @@ html.dark {
|
|
|
6895
6942
|
.mint-section-card__row {
|
|
6896
6943
|
display: flex;
|
|
6897
6944
|
align-items: center;
|
|
6898
|
-
gap:
|
|
6945
|
+
gap: var(--mint-section-card-gap);
|
|
6899
6946
|
padding: 0.375rem 0.75rem;
|
|
6900
6947
|
background: var(--bg-secondary);
|
|
6901
6948
|
transition: background-color var(--mint-transition);
|
|
6902
6949
|
}
|
|
6903
6950
|
|
|
6951
|
+
/* Leading glyph slot of a row: as wide as the header icon, so the glyph
|
|
6952
|
+
* centres under it and the row text starts at the header title. Leave it
|
|
6953
|
+
* empty to keep the text on the grid. It is one title line high (1lh at the
|
|
6954
|
+
* title size), so a two-line row keeps its glyph on the first line. */
|
|
6955
|
+
.mint-section-card__row-lead {
|
|
6956
|
+
display: inline-flex;
|
|
6957
|
+
align-items: center;
|
|
6958
|
+
justify-content: center;
|
|
6959
|
+
flex-shrink: 0;
|
|
6960
|
+
align-self: flex-start;
|
|
6961
|
+
width: var(--mint-section-card-lead-width);
|
|
6962
|
+
height: 1lh;
|
|
6963
|
+
font-size: 0.8125rem;
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6904
6966
|
a.mint-section-card__row,
|
|
6905
6967
|
button.mint-section-card__row {
|
|
6906
6968
|
cursor: pointer;
|
|
@@ -6922,6 +6984,8 @@ button.mint-section-card__row:hover {
|
|
|
6922
6984
|
[data-density="comfortable"] .mint-section-card__row {
|
|
6923
6985
|
padding-top: 0.5rem;
|
|
6924
6986
|
padding-bottom: 0.5rem;
|
|
6987
|
+
/* Same left inset as the header, so row content lines up under the header. */
|
|
6988
|
+
padding-left: 0.875rem;
|
|
6925
6989
|
}
|
|
6926
6990
|
|
|
6927
6991
|
/* SecretInput — plugin secret settings field with a set / not set / environment status.
|
|
@@ -8430,26 +8494,69 @@ html.dark .mint-toast__item {
|
|
|
8430
8494
|
box-sizing: border-box;
|
|
8431
8495
|
border-bottom: 1px solid var(--border-light);
|
|
8432
8496
|
font-size: 0.8125rem;
|
|
8497
|
+
/* The header compacts when it is narrow (sidebars); see the query below. */
|
|
8498
|
+
container-type: inline-size;
|
|
8433
8499
|
}
|
|
8434
8500
|
|
|
8435
8501
|
.mint-sample-selector__title {
|
|
8502
|
+
flex-shrink: 0;
|
|
8503
|
+
white-space: nowrap;
|
|
8436
8504
|
font-weight: 600;
|
|
8437
8505
|
color: var(--text-primary);
|
|
8438
8506
|
}
|
|
8439
8507
|
|
|
8440
8508
|
.mint-sample-selector__selected {
|
|
8509
|
+
/* Without the query (or with a long count) the count gives way, never the row. */
|
|
8510
|
+
min-width: 0;
|
|
8511
|
+
overflow: hidden;
|
|
8512
|
+
text-overflow: ellipsis;
|
|
8513
|
+
white-space: nowrap;
|
|
8441
8514
|
font-size: 0.75rem;
|
|
8442
8515
|
font-weight: 500;
|
|
8443
8516
|
color: var(--text-muted);
|
|
8444
8517
|
}
|
|
8445
8518
|
|
|
8446
8519
|
.mint-sample-selector__header-actions {
|
|
8520
|
+
flex-shrink: 0;
|
|
8447
8521
|
display: flex;
|
|
8448
8522
|
align-items: center;
|
|
8449
8523
|
gap: 0.125rem;
|
|
8450
8524
|
margin-left: auto;
|
|
8451
8525
|
}
|
|
8452
8526
|
|
|
8527
|
+
.mint-sample-selector__selected-short {
|
|
8528
|
+
display: none;
|
|
8529
|
+
}
|
|
8530
|
+
|
|
8531
|
+
/* Narrow selector: the header stays one row and keeps its height. The grouping
|
|
8532
|
+
button shows only its icon (the label stays its accessible name and tooltip)
|
|
8533
|
+
and the count shortens to "N sel." (screen readers keep "N selected"). */
|
|
8534
|
+
/* Measured: the full row (title, "N selected", Smart group, Clear groups) needs
|
|
8535
|
+
about 17.55rem of header content width with Fira Sans. */
|
|
8536
|
+
@container (max-width: 17.625rem) {
|
|
8537
|
+
.mint-sample-selector__group-btn .mint-button__label,
|
|
8538
|
+
.mint-sample-selector__selected-full {
|
|
8539
|
+
position: absolute;
|
|
8540
|
+
width: 1px;
|
|
8541
|
+
height: 1px;
|
|
8542
|
+
margin: -1px;
|
|
8543
|
+
padding: 0;
|
|
8544
|
+
overflow: hidden;
|
|
8545
|
+
clip: rect(0, 0, 0, 0);
|
|
8546
|
+
white-space: nowrap;
|
|
8547
|
+
border: 0;
|
|
8548
|
+
}
|
|
8549
|
+
|
|
8550
|
+
.mint-sample-selector__selected-short {
|
|
8551
|
+
display: inline;
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
.mint-sample-selector__group-btn {
|
|
8555
|
+
min-width: var(--form-height-sm);
|
|
8556
|
+
padding-inline: 0.25rem;
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
|
|
8453
8560
|
.mint-sample-selector__grouped,
|
|
8454
8561
|
.mint-sample-selector__flat,
|
|
8455
8562
|
.mint-sample-selector__state {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.4.0-beta.
|
|
3
|
+
"version": "1.4.0-beta.20",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -125,6 +125,26 @@ describe('SampleSelector', () => {
|
|
|
125
125
|
expect(modal.props('initialMode')).toBe('auto')
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
+
it.each([
|
|
129
|
+
[true, 'Smart group'],
|
|
130
|
+
[false, 'Group'],
|
|
131
|
+
])('should keep the grouping label as accessible name and tooltip when smart group is %s', (enableSmartGroup, label) => {
|
|
132
|
+
const wrapper = mount(SampleSelector, {
|
|
133
|
+
props: { samples: ['S1', 'S2'], modelValue: ['S1'], groups: [], enableSmartGroup },
|
|
134
|
+
global: { stubs: { AutoGroupModalInternal: true } },
|
|
135
|
+
})
|
|
136
|
+
const button = wrapper.get('.mint-sample-selector__header-actions > .mint-sample-selector__action-btn')
|
|
137
|
+
const count = wrapper.get('.mint-sample-selector__selected')
|
|
138
|
+
|
|
139
|
+
expect({
|
|
140
|
+
name: button.text(),
|
|
141
|
+
title: button.attributes('title'),
|
|
142
|
+
full: count.get('.mint-sample-selector__selected-full').text(),
|
|
143
|
+
short: count.get('.mint-sample-selector__selected-short').text(),
|
|
144
|
+
shortHidden: count.get('.mint-sample-selector__selected-short').attributes('aria-hidden'),
|
|
145
|
+
}).toEqual({ name: label, title: label, full: '1 selected', short: '1 sel.', shortHidden: 'true' })
|
|
146
|
+
})
|
|
147
|
+
|
|
128
148
|
it('opens the grouping modal in manual mode', async () => {
|
|
129
149
|
const wrapper = mount(SampleSelector, {
|
|
130
150
|
props: {
|
|
@@ -14,7 +14,10 @@ const variablesCss = readFileSync(join(stylesDir, 'variables.css'), 'utf-8')
|
|
|
14
14
|
* component-local knobs declared in the same stylesheet that consumes them.
|
|
15
15
|
* Anything else must be a canonical token from variables.css.
|
|
16
16
|
*/
|
|
17
|
-
const RUNTIME_PROPERTY_ALLOWLIST = new Set<string>([
|
|
17
|
+
const RUNTIME_PROPERTY_ALLOWLIST = new Set<string>([
|
|
18
|
+
// Consumer knob: the gap width an edge LayoutResizeHandle centres on (falls back to --mint-gutter).
|
|
19
|
+
'--mint-layout-resize-gap',
|
|
20
|
+
])
|
|
18
21
|
|
|
19
22
|
const SOURCE_EXTENSIONS = ['.vue', '.css', '.ts']
|
|
20
23
|
const IGNORED_DIRS = new Set(['__tests__', 'generated-ui', 'node_modules'])
|
|
@@ -3,15 +3,20 @@ import { ref } from 'vue'
|
|
|
3
3
|
import LayoutResizeHandle from './LayoutResizeHandle.vue'
|
|
4
4
|
|
|
5
5
|
const width = ref(320)
|
|
6
|
+
const gapWidth = ref(320)
|
|
7
|
+
const edgeWidth = ref(280)
|
|
8
|
+
const height = ref(50)
|
|
9
|
+
|
|
10
|
+
const panel = 'background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 0.75rem;'
|
|
11
|
+
const caption = 'margin: 0 0 0.5rem; font: 0.75rem var(--font-mono); color: var(--text-secondary);'
|
|
6
12
|
</script>
|
|
7
13
|
|
|
8
14
|
<template>
|
|
9
15
|
<Story title="Layout/LayoutResizeHandle">
|
|
10
16
|
<Variant title="Keyboard splitter">
|
|
17
|
+
<!-- Directly between two panels, the handle is the gutter. -->
|
|
11
18
|
<div style="display: flex; height: 16rem; padding: 2rem;">
|
|
12
|
-
<div :style="{ width:
|
|
13
|
-
Panel width: {{ width }}px
|
|
14
|
-
</div>
|
|
19
|
+
<div :style="`${panel} width: ${width}px;`">Panel width: {{ width }}px</div>
|
|
15
20
|
<LayoutResizeHandle
|
|
16
21
|
v-model="width"
|
|
17
22
|
orientation="vertical"
|
|
@@ -19,7 +24,63 @@ const width = ref(320)
|
|
|
19
24
|
:min="240"
|
|
20
25
|
:max="480"
|
|
21
26
|
/>
|
|
22
|
-
<div style="flex: 1
|
|
27
|
+
<div :style="`${panel} flex: 1;`" />
|
|
28
|
+
</div>
|
|
29
|
+
</Variant>
|
|
30
|
+
|
|
31
|
+
<Variant title="Gaps and edge">
|
|
32
|
+
<div style="display: grid; gap: 1.5rem; padding: 2rem;">
|
|
33
|
+
<div data-gap="flex-8">
|
|
34
|
+
<p :style="caption">flex, 8px gutter (compact), handle between the panels</p>
|
|
35
|
+
<div style="display: flex; height: 8rem; --mint-gutter: 0.5rem;">
|
|
36
|
+
<div :style="`${panel} width: ${gapWidth}px;`">Left panel</div>
|
|
37
|
+
<LayoutResizeHandle v-model="gapWidth" orientation="vertical" label="Resize left panel (8px)" :min="200" :max="480" />
|
|
38
|
+
<div :style="`${panel} flex: 1;`">Right panel</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div data-gap="wrapper-12">
|
|
42
|
+
<p :style="caption">flex, 12px gutter, handle in a gutter-wide wrapper</p>
|
|
43
|
+
<div style="display: flex; height: 8rem; --mint-gutter: 0.75rem;">
|
|
44
|
+
<div :style="`${panel} width: ${gapWidth}px;`">Left panel</div>
|
|
45
|
+
<div style="display: flex; justify-content: center; flex-shrink: 0; width: var(--mint-gutter);">
|
|
46
|
+
<LayoutResizeHandle v-model="gapWidth" orientation="vertical" label="Resize left panel (12px)" :min="200" :max="480" />
|
|
47
|
+
</div>
|
|
48
|
+
<div :style="`${panel} flex: 1;`">Right panel</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div data-gap="grid-8">
|
|
52
|
+
<p :style="caption">grid, 8px track</p>
|
|
53
|
+
<div :style="`display: grid; grid-template-columns: ${gapWidth}px 0.5rem 1fr; height: 8rem;`">
|
|
54
|
+
<div :style="panel">Left panel</div>
|
|
55
|
+
<LayoutResizeHandle
|
|
56
|
+
v-model="gapWidth"
|
|
57
|
+
orientation="vertical"
|
|
58
|
+
label="Resize left panel (grid)"
|
|
59
|
+
:min="200"
|
|
60
|
+
:max="480"
|
|
61
|
+
style="--mint-layout-resize-gap: 0.5rem;"
|
|
62
|
+
/>
|
|
63
|
+
<div :style="panel">Right panel</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div data-gap="edge">
|
|
67
|
+
<p :style="caption">edge="right", 12px gutter</p>
|
|
68
|
+
<div style="display: flex; gap: var(--mint-gutter); height: 8rem; --mint-gutter: 0.75rem;">
|
|
69
|
+
<aside :style="`${panel} position: relative; width: ${edgeWidth}px;`">
|
|
70
|
+
Sidebar
|
|
71
|
+
<LayoutResizeHandle v-model="edgeWidth" orientation="vertical" edge="right" label="Resize sidebar" :min="200" :max="480" />
|
|
72
|
+
</aside>
|
|
73
|
+
<div :style="`${panel} flex: 1;`">Main</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div data-gap="horizontal">
|
|
77
|
+
<p :style="caption">horizontal, 8px gutter</p>
|
|
78
|
+
<div style="display: flex; flex-direction: column; height: 14rem; --mint-gutter: 0.5rem;">
|
|
79
|
+
<div :style="`${panel} flex: 0 0 ${height}%;`">Top panel</div>
|
|
80
|
+
<LayoutResizeHandle v-model="height" orientation="horizontal" label="Resize top panel" :min="20" :max="80" :step="1" />
|
|
81
|
+
<div :style="`${panel} flex: 1;`">Bottom panel</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
23
84
|
</div>
|
|
24
85
|
</Variant>
|
|
25
86
|
</Story>
|
|
@@ -21,6 +21,13 @@ const mockGroups: SampleGroup[] = [
|
|
|
21
21
|
{ name: 'Vehicle', color: '#F59E0B', samples: ['Vehicle_Rep1', 'Vehicle_Rep2', 'Vehicle_Rep3'] },
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
// 1024 selected runs in two groups: the long count must not push the actions out.
|
|
25
|
+
const manyRuns = Array.from({ length: 1024 }, (_, i) => `Run_${String(i + 1).padStart(4, '0')}`)
|
|
26
|
+
const manyGroups: SampleGroup[] = [
|
|
27
|
+
{ name: 'Batch A', color: '#3B82F6', samples: manyRuns.slice(0, 512) },
|
|
28
|
+
{ name: 'Batch B', color: '#10B981', samples: manyRuns.slice(512) },
|
|
29
|
+
]
|
|
30
|
+
|
|
24
31
|
const manualSamples = [
|
|
25
32
|
'Pt001_TumorA_d7_rep1',
|
|
26
33
|
'Pt001_TumorA_d7_rep1_reseq2024',
|
|
@@ -136,12 +143,20 @@ function handleSmartGroup(result: AutoGroupResult, state: { groups: SampleGroup[
|
|
|
136
143
|
</Variant>
|
|
137
144
|
|
|
138
145
|
<Variant title="Narrow Header">
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
<!-- At 17.625rem of header content width and below, the grouping button shows only its icon and the count shortens. -->
|
|
147
|
+
<div style="display: flex; align-items: flex-start; gap: 2rem; padding: 2rem;">
|
|
148
|
+
<div v-for="width in [220, 300, 340]" :key="width" :data-width="width" :style="{ width: `${width}px` }">
|
|
149
|
+
<p style="margin: 0 0 0.5rem; font: 0.75rem var(--font-mono); color: var(--text-secondary);">{{ width }}px</p>
|
|
150
|
+
<SampleSelector
|
|
151
|
+
:model-value="['Control_Rep1']"
|
|
152
|
+
:samples="mockSamples"
|
|
153
|
+
:groups="mockGroups"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
<div data-width="320-long" style="width: 320px; height: 22rem;">
|
|
157
|
+
<p style="margin: 0 0 0.5rem; font: 0.75rem var(--font-mono); color: var(--text-secondary);">320px, 1024 selected</p>
|
|
158
|
+
<SampleSelector :model-value="manyRuns" :samples="manyRuns" :groups="manyGroups" />
|
|
159
|
+
</div>
|
|
145
160
|
</div>
|
|
146
161
|
</Variant>
|
|
147
162
|
|
|
@@ -256,6 +256,8 @@ function addNewGroup() {
|
|
|
256
256
|
newGroupName.value = ''
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
const groupingLabel = computed(() => (props.enableSmartGroup ? 'Smart group' : 'Group'))
|
|
260
|
+
|
|
259
261
|
/** Selected samples among the resolved list, for the select-all header. */
|
|
260
262
|
const selectedCount = computed(() => resolvedSamples.value.filter(s => props.modelValue.includes(s)).length)
|
|
261
263
|
|
|
@@ -271,16 +273,21 @@ defineExpose({ handleSmartGroupApply })
|
|
|
271
273
|
<div class="mint-sample-selector">
|
|
272
274
|
<div class="mint-sample-selector__header">
|
|
273
275
|
<span class="mint-sample-selector__title">Samples</span>
|
|
274
|
-
|
|
276
|
+
<!-- A narrow selector shows "N sel."; screen readers keep "N selected". -->
|
|
277
|
+
<span class="mint-sample-selector__selected">
|
|
278
|
+
<span class="mint-sample-selector__selected-full">{{ selectedCount }} selected</span>
|
|
279
|
+
<span class="mint-sample-selector__selected-short" aria-hidden="true">{{ selectedCount }} sel.</span>
|
|
280
|
+
</span>
|
|
275
281
|
<div v-if="!isPlaceholderState && enableGrouping" class="mint-sample-selector__header-actions">
|
|
276
282
|
<BaseButton
|
|
277
283
|
variant="ghost"
|
|
278
284
|
size="sm"
|
|
279
285
|
:icon="enableSmartGroup ? WandSparkles : Layers"
|
|
280
|
-
|
|
286
|
+
:title="groupingLabel"
|
|
287
|
+
class="mint-sample-selector__action-btn mint-sample-selector__group-btn"
|
|
281
288
|
@click="openGroupingModal(groupingActionMode)"
|
|
282
289
|
>
|
|
283
|
-
{{
|
|
290
|
+
{{ groupingLabel }}
|
|
284
291
|
</BaseButton>
|
|
285
292
|
<BaseButton
|
|
286
293
|
v-if="internalGroups.length > 0"
|
|
@@ -107,6 +107,39 @@ function initPlayground() {
|
|
|
107
107
|
</div>
|
|
108
108
|
</Variant>
|
|
109
109
|
|
|
110
|
+
<Variant title="Rows with lead">
|
|
111
|
+
<div style="padding: 2rem; max-width: 600px; display: grid; gap: 1rem;">
|
|
112
|
+
<SectionCard title="Experiments" count="3 total" :icon="flaskIcon" accent="experiment" uppercase>
|
|
113
|
+
<div class="mint-section-card__rows">
|
|
114
|
+
<a v-for="e in experiments" :key="e.code" href="#" class="mint-section-card__row">
|
|
115
|
+
<span class="mint-section-card__row-lead">
|
|
116
|
+
<span style="width: 0.375rem; height: 0.375rem; border-radius: 50%; background: var(--mint-success)" />
|
|
117
|
+
</span>
|
|
118
|
+
<span style="flex: 1; min-width: 0">
|
|
119
|
+
<span style="display: block; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">{{ e.name }}</span>
|
|
120
|
+
<span style="display: block; font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-muted)">{{ e.code }}</span>
|
|
121
|
+
</span>
|
|
122
|
+
<span style="flex-shrink: 0; font-size: 0.71875rem; color: var(--text-muted)">{{ e.updated }}</span>
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
</SectionCard>
|
|
126
|
+
<SectionCard title="Projects" count="2 active" :icon="folderIcon" accent="project" uppercase>
|
|
127
|
+
<div class="mint-section-card__rows">
|
|
128
|
+
<a href="#" class="mint-section-card__row">
|
|
129
|
+
<span class="mint-section-card__row-lead" aria-hidden="true" />
|
|
130
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Hypoxia metabolic rewiring</span>
|
|
131
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">12 experiments</span>
|
|
132
|
+
</a>
|
|
133
|
+
<a href="#" class="mint-section-card__row">
|
|
134
|
+
<span class="mint-section-card__row-lead" aria-hidden="true" />
|
|
135
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Serine one-carbon flux</span>
|
|
136
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">7 experiments</span>
|
|
137
|
+
</a>
|
|
138
|
+
</div>
|
|
139
|
+
</SectionCard>
|
|
140
|
+
</div>
|
|
141
|
+
</Variant>
|
|
142
|
+
|
|
110
143
|
<Variant title="Detail card with footer">
|
|
111
144
|
<div style="padding: 2rem; max-width: 600px;">
|
|
112
145
|
<SectionCard title="Analysis artifacts" :count="4" divider="light">
|
|
@@ -1,26 +1,72 @@
|
|
|
1
|
+
/* The handle is the gap between two panels: its layout width (horizontal: height)
|
|
2
|
+
is the gap, --mint-layout-resize-gap or else --mint-gutter (8px compact, 12px
|
|
3
|
+
normal). It stretches along the panels, and its bar and a 12px hit area are
|
|
4
|
+
centred in it, the hit area drawn above the neighbouring panels. */
|
|
1
5
|
.mint-layout-resize-handle {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
--_size: var(--mint-layout-resize-gap, var(--mint-gutter, 0.75rem));
|
|
7
|
+
--_hit: 0.75rem;
|
|
8
|
+
position: relative;
|
|
9
|
+
z-index: 10;
|
|
10
|
+
flex: 0 0 auto;
|
|
11
|
+
align-self: stretch;
|
|
6
12
|
background: transparent;
|
|
7
13
|
cursor: col-resize;
|
|
14
|
+
outline: none;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
.mint-layout-resize-handle--vertical {
|
|
11
|
-
|
|
17
|
+
.mint-layout-resize-handle--vertical {
|
|
18
|
+
justify-self: center;
|
|
19
|
+
width: var(--_size);
|
|
20
|
+
min-height: 2.75rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.mint-layout-resize-handle--horizontal {
|
|
24
|
+
justify-self: stretch;
|
|
25
|
+
min-width: 2.75rem;
|
|
26
|
+
height: var(--_size);
|
|
27
|
+
cursor: row-resize;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Hit area and focus ring. Pointer events on it go to the handle. */
|
|
31
|
+
.mint-layout-resize-handle::before {
|
|
32
|
+
position: absolute;
|
|
33
|
+
border-radius: var(--radius-sm);
|
|
34
|
+
content: '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.mint-layout-resize-handle--vertical::before {
|
|
38
|
+
top: 0;
|
|
39
|
+
bottom: 0;
|
|
40
|
+
left: 50%;
|
|
41
|
+
width: var(--_hit);
|
|
42
|
+
transform: translateX(-50%);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mint-layout-resize-handle--horizontal::before {
|
|
46
|
+
top: 50%;
|
|
47
|
+
right: 0;
|
|
48
|
+
left: 0;
|
|
49
|
+
height: var(--_hit);
|
|
50
|
+
transform: translateY(-50%);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Edge handles lie outside the edge and cover exactly the gap. */
|
|
12
54
|
.mint-layout-resize-handle--edge-left,
|
|
13
|
-
.mint-layout-resize-handle--edge-right { position: absolute; top: 0; bottom: 0;
|
|
55
|
+
.mint-layout-resize-handle--edge-right { position: absolute; top: 0; bottom: 0; }
|
|
14
56
|
.mint-layout-resize-handle--edge-left { left: 0; transform: translateX(-100%); }
|
|
15
57
|
.mint-layout-resize-handle--edge-right { right: 0; transform: translateX(100%); }
|
|
16
58
|
|
|
17
59
|
.mint-layout-resize-handle__bar {
|
|
60
|
+
position: absolute;
|
|
61
|
+
top: 50%;
|
|
62
|
+
left: 50%;
|
|
18
63
|
display: block;
|
|
19
64
|
width: 0.25rem;
|
|
20
65
|
height: 2.75rem;
|
|
21
66
|
border-radius: 999px;
|
|
22
67
|
background: var(--border-color);
|
|
23
68
|
opacity: 0.72;
|
|
69
|
+
transform: translate(-50%, -50%);
|
|
24
70
|
transition: background-color 150ms ease, opacity 150ms ease, transform 150ms ease;
|
|
25
71
|
}
|
|
26
72
|
|
|
@@ -36,25 +82,22 @@
|
|
|
36
82
|
}
|
|
37
83
|
|
|
38
84
|
.mint-layout-resize-handle--vertical:hover .mint-layout-resize-handle__bar,
|
|
39
|
-
.mint-layout-resize-handle--vertical:focus-visible .mint-layout-resize-handle__bar { transform: scaleX(1.35); }
|
|
85
|
+
.mint-layout-resize-handle--vertical:focus-visible .mint-layout-resize-handle__bar { transform: translate(-50%, -50%) scaleX(1.35); }
|
|
40
86
|
.mint-layout-resize-handle--horizontal:hover .mint-layout-resize-handle__bar,
|
|
41
|
-
.mint-layout-resize-handle--horizontal:focus-visible .mint-layout-resize-handle__bar { transform: scaleY(1.35); }
|
|
87
|
+
.mint-layout-resize-handle--horizontal:focus-visible .mint-layout-resize-handle__bar { transform: translate(-50%, -50%) scaleY(1.35); }
|
|
42
88
|
|
|
43
|
-
.mint-layout-resize-handle:focus-visible {
|
|
89
|
+
.mint-layout-resize-handle:focus-visible::before {
|
|
44
90
|
outline: 2px solid var(--color-primary);
|
|
45
|
-
outline-offset: 2px;
|
|
91
|
+
outline-offset: -2px;
|
|
46
92
|
}
|
|
47
93
|
|
|
48
|
-
.mint-layout-resize-handle--edge-left:focus-visible,
|
|
49
|
-
.mint-layout-resize-handle--edge-right:focus-visible { outline-offset: -2px; }
|
|
50
|
-
|
|
51
94
|
.mint-layout-resize-handle[aria-disabled='true'] { cursor: not-allowed; opacity: 0.5; }
|
|
52
95
|
|
|
53
96
|
@media (prefers-reduced-motion: reduce) {
|
|
54
97
|
.mint-layout-resize-handle__bar { transition: none; }
|
|
55
98
|
}
|
|
56
99
|
|
|
100
|
+
/* Touch: the handle reserves its full 2.75rem hit area in the layout. */
|
|
57
101
|
@media (pointer: coarse) {
|
|
58
|
-
.mint-layout-resize-handle
|
|
59
|
-
.mint-layout-resize-handle--horizontal { height: 2.75rem; }
|
|
102
|
+
.mint-layout-resize-handle { --_size: 2.75rem; --_hit: 2.75rem; }
|
|
60
103
|
}
|