@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/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-BHVT1qN2.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-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
- display: flex;
4624
- flex-shrink: 0;
4625
- align-items: center;
4626
- justify-content: center;
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
- .mint-layout-resize-handle--vertical { width: 1rem; min-height: 2.75rem; }
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; z-index: 10; height: auto; }
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--vertical { width: 2.75rem; }
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: 0.625rem;
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: 1rem;
6838
- height: 1rem;
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: 0.625rem;
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.19",
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: `${width}px`, background: 'var(--bg-secondary)', border: '1px solid var(--border-color)' }">
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; background: var(--bg-tertiary);" />
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
- <div style="padding: 2rem; width: 320px;">
140
- <SampleSelector
141
- :model-value="['Control_Rep1']"
142
- :samples="mockSamples"
143
- :groups="mockGroups"
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
- <span class="mint-sample-selector__selected">{{ selectedCount }} selected</span>
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
- class="mint-sample-selector__action-btn"
286
+ :title="groupingLabel"
287
+ class="mint-sample-selector__action-btn mint-sample-selector__group-btn"
281
288
  @click="openGroupingModal(groupingActionMode)"
282
289
  >
283
- {{ enableSmartGroup ? 'Smart group' : 'Group' }}
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
- display: flex;
3
- flex-shrink: 0;
4
- align-items: center;
5
- justify-content: center;
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 { width: 1rem; min-height: 2.75rem; }
11
- .mint-layout-resize-handle--horizontal { min-width: 2.75rem; height: 1rem; cursor: row-resize; }
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; z-index: 10; height: auto; }
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--vertical { width: 2.75rem; }
59
- .mint-layout-resize-handle--horizontal { height: 2.75rem; }
102
+ .mint-layout-resize-handle { --_size: 2.75rem; --_hit: 2.75rem; }
60
103
  }