@morscherlab/mld-sdk 0.7.6 → 0.7.7

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/styles.css CHANGED
@@ -1205,7 +1205,7 @@ html.dark .focus\:ring-offset-2:focus {
1205
1205
  transform: translateX(0.75rem);
1206
1206
  }
1207
1207
  .mld-sidebar__sections > .mld-collapsible-card .mld-collapsible-card__content {
1208
- padding: 0 0.75rem 0.75rem;
1208
+ padding: 0.75rem;
1209
1209
  display: flex;
1210
1210
  flex-direction: column;
1211
1211
  gap: 0.75rem;
@@ -3285,6 +3285,9 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
3285
3285
  .mld-modal__container {
3286
3286
  position: relative;
3287
3287
  width: 100%;
3288
+ max-height: calc(100vh - 2rem);
3289
+ display: flex;
3290
+ flex-direction: column;
3288
3291
  background-color: var(--bg-card);
3289
3292
  border-radius: var(--mld-radius-lg);
3290
3293
  box-shadow: var(--mld-shadow-lg);
@@ -3342,6 +3345,9 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
3342
3345
  /* Modal Body */
3343
3346
  .mld-modal__body {
3344
3347
  padding: 1rem 1.5rem;
3348
+ flex: 1;
3349
+ min-height: 0;
3350
+ overflow-y: auto;
3345
3351
  }
3346
3352
  /* Modal Footer */
3347
3353
  .mld-modal__footer {
@@ -3399,6 +3405,11 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
3399
3405
  .mld-number-input--disabled {
3400
3406
  opacity: 0.5;
3401
3407
  }
3408
+ .mld-number-input__buttons {
3409
+ display: flex;
3410
+ flex-shrink: 0;
3411
+ border-left: 1px solid var(--border-color);
3412
+ }
3402
3413
  .mld-number-input__button {
3403
3414
  display: flex;
3404
3415
  align-items: center;
@@ -3421,9 +3432,6 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
3421
3432
  .mld-number-input__button--decrement {
3422
3433
  border-right: 1px solid var(--border-color);
3423
3434
  }
3424
- .mld-number-input__button--increment {
3425
- border-left: 1px solid var(--border-color);
3426
- }
3427
3435
  .mld-number-input__button--sm {
3428
3436
  width: 1.75rem;
3429
3437
  }
@@ -3440,7 +3448,7 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
3440
3448
  .mld-number-input__input {
3441
3449
  flex: 1;
3442
3450
  min-width: 0;
3443
- text-align: center;
3451
+ text-align: left;
3444
3452
  background-color: var(--bg-secondary);
3445
3453
  color: var(--text-primary);
3446
3454
  border: none;
@@ -5959,7 +5967,10 @@ html.dark .mld-toggle__track:focus-visible {
5959
5967
  }
5960
5968
  /* AutoGroupModal - Smart grouping wizard */
5961
5969
  .mld-auto-group {
5962
- min-height: 400px;
5970
+ flex: 1;
5971
+ display: flex;
5972
+ flex-direction: column;
5973
+ min-height: 0;
5963
5974
  }
5964
5975
  /* --- Mode toggle --- */
5965
5976
  .mld-auto-group__mode-toggle {
@@ -6301,9 +6312,7 @@ html.dark .mld-toggle__track:focus-visible {
6301
6312
  .mld-auto-group__preview-groups {
6302
6313
  display: flex;
6303
6314
  flex-direction: column;
6304
- gap: 0.375rem;
6305
- max-height: 320px;
6306
- overflow-y: auto;
6315
+ gap: 0.5rem;
6307
6316
  }
6308
6317
  .mld-auto-group__preview-group {
6309
6318
  border: 1px solid var(--border-color);
@@ -10887,9 +10896,12 @@ html.dark .mld-settings-modal__option-btn--active {
10887
10896
  flex-direction: column;
10888
10897
  gap: 1.5rem;
10889
10898
  outline: none;
10899
+ flex: 1;
10900
+ min-height: 0;
10890
10901
  }
10891
10902
  /* Progress indicator */
10892
10903
  .mld-wizard__progress {
10904
+ flex-shrink: 0;
10893
10905
  }
10894
10906
  .mld-wizard__steps-indicator {
10895
10907
  display: flex;
@@ -10970,6 +10982,7 @@ html.dark .mld-settings-modal__option-btn--active {
10970
10982
  .mld-wizard__body {
10971
10983
  flex: 1;
10972
10984
  min-height: 0;
10985
+ overflow-y: auto;
10973
10986
  }
10974
10987
  /* Navigation */
10975
10988
  .mld-wizard__navigation {
@@ -10978,6 +10991,7 @@ html.dark .mld-settings-modal__option-btn--active {
10978
10991
  gap: 0.5rem;
10979
10992
  padding-top: 1rem;
10980
10993
  border-top: 1px solid var(--border-color);
10994
+ flex-shrink: 0;
10981
10995
  }
10982
10996
  .mld-wizard__nav-btn {
10983
10997
  padding: 0.5rem 1rem;
@@ -13469,6 +13483,9 @@ html.dark .mld-radio-option__native:focus-visible + .mld-radio-option__circle {
13469
13483
  .mld-modal__container {
13470
13484
  position: relative;
13471
13485
  width: 100%;
13486
+ max-height: calc(100vh - 2rem);
13487
+ display: flex;
13488
+ flex-direction: column;
13472
13489
  background-color: var(--bg-card);
13473
13490
  border-radius: var(--mld-radius-lg);
13474
13491
  box-shadow: var(--mld-shadow-lg);
@@ -13528,6 +13545,9 @@ html.dark .mld-radio-option__native:focus-visible + .mld-radio-option__circle {
13528
13545
  /* Modal Body */
13529
13546
  .mld-modal__body {
13530
13547
  padding: 1rem 1.5rem;
13548
+ flex: 1;
13549
+ min-height: 0;
13550
+ overflow-y: auto;
13531
13551
  }
13532
13552
 
13533
13553
  /* Modal Footer */
@@ -15249,6 +15269,11 @@ to { transform: rotate(360deg);
15249
15269
  .mld-number-input--disabled {
15250
15270
  opacity: 0.5;
15251
15271
  }
15272
+ .mld-number-input__buttons {
15273
+ display: flex;
15274
+ flex-shrink: 0;
15275
+ border-left: 1px solid var(--border-color);
15276
+ }
15252
15277
  .mld-number-input__button {
15253
15278
  display: flex;
15254
15279
  align-items: center;
@@ -15271,9 +15296,6 @@ to { transform: rotate(360deg);
15271
15296
  .mld-number-input__button--decrement {
15272
15297
  border-right: 1px solid var(--border-color);
15273
15298
  }
15274
- .mld-number-input__button--increment {
15275
- border-left: 1px solid var(--border-color);
15276
- }
15277
15299
  .mld-number-input__button--sm {
15278
15300
  width: 1.75rem;
15279
15301
  }
@@ -15290,7 +15312,7 @@ to { transform: rotate(360deg);
15290
15312
  .mld-number-input__input {
15291
15313
  flex: 1;
15292
15314
  min-width: 0;
15293
- text-align: center;
15315
+ text-align: left;
15294
15316
  background-color: var(--bg-secondary);
15295
15317
  color: var(--text-primary);
15296
15318
  border: none;
@@ -16829,7 +16851,7 @@ html.dark .mld-settings-modal__option-btn--active {
16829
16851
  transform: translateX(0.75rem);
16830
16852
  }
16831
16853
  .mld-sidebar__sections > .mld-collapsible-card .mld-collapsible-card__content {
16832
- padding: 0 0.75rem 0.75rem;
16854
+ padding: 0.75rem;
16833
16855
  display: flex;
16834
16856
  flex-direction: column;
16835
16857
  gap: 0.75rem;
@@ -19454,10 +19476,13 @@ to {
19454
19476
  flex-direction: column;
19455
19477
  gap: 1.5rem;
19456
19478
  outline: none;
19479
+ flex: 1;
19480
+ min-height: 0;
19457
19481
  }
19458
19482
 
19459
19483
  /* Progress indicator */
19460
19484
  .mld-wizard__progress {
19485
+ flex-shrink: 0;
19461
19486
  }
19462
19487
  .mld-wizard__steps-indicator {
19463
19488
  display: flex;
@@ -19540,6 +19565,7 @@ to {
19540
19565
  .mld-wizard__body {
19541
19566
  flex: 1;
19542
19567
  min-height: 0;
19568
+ overflow-y: auto;
19543
19569
  }
19544
19570
 
19545
19571
  /* Navigation */
@@ -19549,6 +19575,7 @@ to {
19549
19575
  gap: 0.5rem;
19550
19576
  padding-top: 1rem;
19551
19577
  border-top: 1px solid var(--border-color);
19578
+ flex-shrink: 0;
19552
19579
  }
19553
19580
  .mld-wizard__nav-btn {
19554
19581
  padding: 0.5rem 1rem;
@@ -19612,7 +19639,10 @@ to {
19612
19639
  }
19613
19640
  /* AutoGroupModal - Smart grouping wizard */
19614
19641
  .mld-auto-group {
19615
- min-height: 400px;
19642
+ flex: 1;
19643
+ display: flex;
19644
+ flex-direction: column;
19645
+ min-height: 0;
19616
19646
  }
19617
19647
 
19618
19648
  /* --- Mode toggle --- */
@@ -19960,9 +19990,7 @@ to {
19960
19990
  .mld-auto-group__preview-groups {
19961
19991
  display: flex;
19962
19992
  flex-direction: column;
19963
- gap: 0.375rem;
19964
- max-height: 320px;
19965
- overflow-y: auto;
19993
+ gap: 0.5rem;
19966
19994
  }
19967
19995
  .mld-auto-group__preview-group {
19968
19996
  border: 1px solid var(--border-color);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mld-sdk",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "MLD Platform SDK - Vue 3 components, composables, and types for plugin development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'
2
2
  import {
3
3
  analyzeDelimiter,
4
4
  detectOutliers,
5
+ classifyOutlierAction,
5
6
  extractColumns,
6
7
  parseCSV,
7
8
  parseCSVLine,
@@ -373,3 +374,135 @@ describe('computeGroups', () => {
373
374
  expect(treatGroup?.samples).toHaveLength(3)
374
375
  })
375
376
  })
377
+
378
+ describe('classifyOutlierAction', () => {
379
+ it('should return qc for samples containing QC keywords', () => {
380
+ expect(classifyOutlierAction('LT_13102025_EQC_Jurkat_1_2', '_')).toBe('qc')
381
+ expect(classifyOutlierAction('LT_13102025_IQC_Pool_1', '_')).toBe('qc')
382
+ expect(classifyOutlierAction('Blank_001', '_')).toBe('qc')
383
+ expect(classifyOutlierAction('STD_Low_1', '_')).toBe('qc')
384
+ expect(classifyOutlierAction('test_EQC_Jurkat_02', '_')).toBe('qc')
385
+ })
386
+
387
+ it('should match case-insensitively', () => {
388
+ expect(classifyOutlierAction('LT_eqc_Pool', '_')).toBe('qc')
389
+ expect(classifyOutlierAction('LT_EQC_Pool', '_')).toBe('qc')
390
+ expect(classifyOutlierAction('LT_Eqc_Pool', '_')).toBe('qc')
391
+ expect(classifyOutlierAction('BLANK_001', '_')).toBe('qc')
392
+ expect(classifyOutlierAction('Test_Sample', '_')).toBe('qc')
393
+ })
394
+
395
+ it('should match against individual segments only', () => {
396
+ // "eqc" embedded inside a segment should NOT match
397
+ expect(classifyOutlierAction('LT_SEQC123_Pool', '_')).toBe('include')
398
+ // "std" as standalone segment should match
399
+ expect(classifyOutlierAction('LT_std_Pool', '_')).toBe('qc')
400
+ })
401
+
402
+ it('should return include for regular experimental samples', () => {
403
+ expect(classifyOutlierAction('LT_13102025_212_WT_Glu', '_')).toBe('include')
404
+ expect(classifyOutlierAction('Control_Rep1', '_')).toBe('include')
405
+ expect(classifyOutlierAction('Treatment_High_Rep3', '_')).toBe('include')
406
+ })
407
+
408
+ it('should work with different delimiters', () => {
409
+ expect(classifyOutlierAction('LT-EQC-Pool', '-')).toBe('qc')
410
+ expect(classifyOutlierAction('blank.001', '.')).toBe('qc')
411
+ expect(classifyOutlierAction('Control-Rep1', '-')).toBe('include')
412
+ })
413
+ })
414
+
415
+ describe('integration: mixed experimental + QC + test samples', () => {
416
+ // Simulates the real dataset pattern:
417
+ // 154 experimental (11 fields), ~15 QC (6-7 fields), 3 test (4 fields)
418
+ const experimental = [
419
+ 'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33',
420
+ 'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
421
+ 'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35',
422
+ 'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
423
+ 'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37',
424
+ 'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
425
+ 'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39',
426
+ ]
427
+ const qcSamples = [
428
+ 'LT_13102025_EQC_Jurkat_1_2',
429
+ 'LT_13102025_IQC_Pool_3',
430
+ ]
431
+ const testSamples = [
432
+ 'test_EQC_Jurkat_02',
433
+ ]
434
+ const allLines = [...experimental, ...qcSamples, ...testSamples]
435
+
436
+ it('should detect dominantFieldCount=11 and flag QC/test as outliers', () => {
437
+ const analysis = analyzeDelimiter(allLines)
438
+ expect(analysis.delimiter).toBe('_')
439
+ expect(analysis.dominantFieldCount).toBe(11)
440
+
441
+ const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
442
+ // QC samples (6 fields) and test samples (4 fields) are outliers
443
+ expect(outliers).toHaveLength(qcSamples.length + testSamples.length)
444
+
445
+ // Experimental samples should NOT be flagged
446
+ const outlierIndices = new Set(outliers.map(o => o.index))
447
+ for (let i = 0; i < experimental.length; i++) {
448
+ expect(outlierIndices.has(i)).toBe(false)
449
+ }
450
+ })
451
+
452
+ it('should auto-classify QC/test outliers with smart defaults', () => {
453
+ const analysis = analyzeDelimiter(allLines)
454
+ const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
455
+
456
+ for (const outlier of outliers) {
457
+ outlier.action = classifyOutlierAction(outlier.sample, '_')
458
+ }
459
+
460
+ // All QC and test samples should be classified as 'qc'
461
+ expect(outliers.every(o => o.action === 'qc')).toBe(true)
462
+ })
463
+
464
+ it('should produce 11 columns from conforming samples', () => {
465
+ const analysis = analyzeDelimiter(allLines)
466
+ const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
467
+ const conforming = allLines.filter(
468
+ (_, i) => !outliers.some(o => o.index === i)
469
+ )
470
+
471
+ const conformingFieldCounts = conforming.map(s => s.split('_').length)
472
+ const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
473
+
474
+ const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
475
+ expect(columns).toHaveLength(11)
476
+ })
477
+
478
+ it('should auto-disable constant columns (cardinality 1)', () => {
479
+ const analysis = analyzeDelimiter(allLines)
480
+ const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
481
+ const conforming = allLines.filter(
482
+ (_, i) => !outliers.some(o => o.index === i)
483
+ )
484
+
485
+ const conformingFieldCounts = conforming.map(s => s.split('_').length)
486
+ const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
487
+
488
+ const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
489
+
490
+ // Auto-disable: only enable columns with cardinality > 1
491
+ const enabled = new Set(
492
+ columns.filter(f => f.cardinality > 1).map(f => f.index)
493
+ )
494
+
495
+ // Constant columns (LT, 13102025, Glu, 091123) should be disabled
496
+ const constantCols = columns.filter(c => c.cardinality === 1)
497
+ for (const col of constantCols) {
498
+ expect(enabled.has(col.index)).toBe(false)
499
+ }
500
+
501
+ // Variable columns should be enabled
502
+ const variableCols = columns.filter(c => c.cardinality > 1)
503
+ for (const col of variableCols) {
504
+ expect(enabled.has(col.index)).toBe(true)
505
+ }
506
+ expect(variableCols.length).toBeGreaterThan(0)
507
+ })
508
+ })
@@ -19,6 +19,29 @@ const withOutliersSamples = [
19
19
  'Ctrl_Brain_D1_Rep1', 'Ctrl_Brain_D1_Rep2',
20
20
  ]
21
21
 
22
+ // Real-world mixed dataset: 11-field experimental + 6-field QC + 4-field test
23
+ // This exercises the dominantFieldCount outlier fix
24
+ const mixedFieldCountSamples = [
25
+ // Experimental (11 fields each)
26
+ 'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33',
27
+ 'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
28
+ 'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35',
29
+ 'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
30
+ 'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37',
31
+ 'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
32
+ 'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39',
33
+ 'LT_13102025_219_KI_Glu_6_40_L_091123_T2_40',
34
+ 'LT_13102025_220_WT_Glu_3_20_S_091123_T2_41',
35
+ 'LT_13102025_221_WT_Glu_6_40_L_091123_T2_42',
36
+ // QC samples (6 fields)
37
+ 'LT_13102025_EQC_Jurkat_1_2',
38
+ 'LT_13102025_EQC_Jurkat_2_8',
39
+ 'LT_13102025_IQC_Pool_1_5',
40
+ // Test samples (4 fields)
41
+ 'test_EQC_Jurkat_02',
42
+ 'test_Blank_Run_01',
43
+ ]
44
+
22
45
  // Hyphen-delimited samples
23
46
  const hyphenSamples = [
24
47
  'WT-Vehicle-1', 'WT-Vehicle-2', 'WT-Vehicle-3',
@@ -35,10 +58,10 @@ function handleApply(result: AutoGroupResult) {
35
58
  console.log('AutoGroupModal result:', result)
36
59
  }
37
60
 
38
- function initState() {
61
+ function initState(samples: string[] = proteomicsSamples) {
39
62
  return {
40
- isOpen: false,
41
- samples: proteomicsSamples,
63
+ isOpen: true,
64
+ samples,
42
65
  lastResult: null as AutoGroupResult | null,
43
66
  }
44
67
  }
@@ -46,7 +69,7 @@ function initState() {
46
69
 
47
70
  <template>
48
71
  <Story title="Lab/AutoGroupModal">
49
- <Variant title="Playground" :init-state="initState">
72
+ <Variant title="Playground" :init-state="() => ({ isOpen: false, samples: proteomicsSamples, lastResult: null as AutoGroupResult | null })">
50
73
  <template #default="{ state }">
51
74
  <div style="padding: 2rem;">
52
75
  <button
@@ -80,53 +103,98 @@ function initState() {
80
103
  </template>
81
104
  <template #controls="{ state }">
82
105
  <HstSelect
83
- :model-value="state.samples === proteomicsSamples ? 'proteomics' : state.samples === withOutliersSamples ? 'outliers' : 'hyphen'"
106
+ :model-value="state.samples === proteomicsSamples ? 'proteomics' : state.samples === withOutliersSamples ? 'outliers' : state.samples === mixedFieldCountSamples ? 'mixed' : 'hyphen'"
84
107
  title="Sample Set"
85
108
  :options="[
86
109
  { value: 'proteomics', label: 'Proteomics (clean)' },
87
110
  { value: 'outliers', label: 'With QC/Blanks' },
111
+ { value: 'mixed', label: 'Mixed Field Counts (11 + 6 + 4)' },
88
112
  { value: 'hyphen', label: 'Hyphen-delimited' },
89
113
  ]"
90
114
  @update:model-value="(v: string) => {
91
115
  if (v === 'proteomics') state.samples = proteomicsSamples
92
116
  else if (v === 'outliers') state.samples = withOutliersSamples
117
+ else if (v === 'mixed') state.samples = mixedFieldCountSamples
93
118
  else state.samples = hyphenSamples
94
119
  }"
95
120
  />
96
121
  </template>
97
122
  </Variant>
98
123
 
99
- <Variant title="Pre-filled (Proteomics)">
100
- <div style="padding: 2rem;">
101
- <AutoGroupModal
102
- :model-value="true"
103
- :samples="proteomicsSamples"
104
- @update:model-value="() => {}"
105
- @apply="handleApply"
106
- />
107
- </div>
124
+ <Variant title="Pre-filled (Proteomics)" :init-state="() => initState(proteomicsSamples)">
125
+ <template #default="{ state }">
126
+ <div style="padding: 2rem;">
127
+ <button
128
+ type="button"
129
+ style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
130
+ @click="state.isOpen = true"
131
+ >
132
+ Re-open Modal
133
+ </button>
134
+ <AutoGroupModal
135
+ v-model="state.isOpen"
136
+ :samples="state.samples"
137
+ @apply="handleApply"
138
+ />
139
+ </div>
140
+ </template>
108
141
  </Variant>
109
142
 
110
- <Variant title="With Outliers (QC/Blanks)">
111
- <div style="padding: 2rem;">
112
- <AutoGroupModal
113
- :model-value="true"
114
- :samples="withOutliersSamples"
115
- @update:model-value="() => {}"
116
- @apply="handleApply"
117
- />
118
- </div>
143
+ <Variant title="With Outliers (QC/Blanks)" :init-state="() => initState(withOutliersSamples)">
144
+ <template #default="{ state }">
145
+ <div style="padding: 2rem;">
146
+ <button
147
+ type="button"
148
+ style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
149
+ @click="state.isOpen = true"
150
+ >
151
+ Re-open Modal
152
+ </button>
153
+ <AutoGroupModal
154
+ v-model="state.isOpen"
155
+ :samples="state.samples"
156
+ @apply="handleApply"
157
+ />
158
+ </div>
159
+ </template>
119
160
  </Variant>
120
161
 
121
- <Variant title="Hyphen-Delimited">
122
- <div style="padding: 2rem;">
123
- <AutoGroupModal
124
- :model-value="true"
125
- :samples="hyphenSamples"
126
- @update:model-value="() => {}"
127
- @apply="handleApply"
128
- />
129
- </div>
162
+ <Variant title="Mixed Field Counts (Smart Outlier Detection)" :init-state="() => initState(mixedFieldCountSamples)">
163
+ <template #default="{ state }">
164
+ <div style="padding: 2rem;">
165
+ <button
166
+ type="button"
167
+ style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
168
+ @click="state.isOpen = true"
169
+ >
170
+ Re-open Modal
171
+ </button>
172
+ <AutoGroupModal
173
+ v-model="state.isOpen"
174
+ :samples="state.samples"
175
+ @apply="handleApply"
176
+ />
177
+ </div>
178
+ </template>
179
+ </Variant>
180
+
181
+ <Variant title="Hyphen-Delimited" :init-state="() => initState(hyphenSamples)">
182
+ <template #default="{ state }">
183
+ <div style="padding: 2rem;">
184
+ <button
185
+ type="button"
186
+ style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
187
+ @click="state.isOpen = true"
188
+ >
189
+ Re-open Modal
190
+ </button>
191
+ <AutoGroupModal
192
+ v-model="state.isOpen"
193
+ :samples="state.samples"
194
+ @apply="handleApply"
195
+ />
196
+ </div>
197
+ </template>
130
198
  </Variant>
131
199
 
132
200
  <Variant title="Empty (Manual Paste)">
@@ -38,7 +38,7 @@ watch(() => props.modelValue, (open) => {
38
38
  autoGroup.rawText.value = props.samples.join('\n')
39
39
  }
40
40
  }
41
- })
41
+ }, { immediate: true })
42
42
 
43
43
  // Dynamic steps: skip outlier step when no outliers
44
44
  const allSteps: WizardStep[] = [
@@ -239,7 +239,7 @@ const isFirstStep = computed(() => currentStep.value === 0)
239
239
  <div class="mld-auto-group__outlier-banner">
240
240
  {{ autoGroup.outliers.value.length }} of {{ autoGroup.samples.value.length }}
241
241
  samples have irregular structure
242
- (fewer than {{ autoGroup.minFieldCount.value }} fields, delimiter
242
+ (fewer than {{ autoGroup.dominantFieldCount.value }} fields, delimiter
243
243
  <code>{{ autoGroup.delimiter.value }}</code>)
244
244
  </div>
245
245
 
@@ -74,22 +74,6 @@ function increment() {
74
74
  disabled ? 'mld-number-input--disabled' : '',
75
75
  ]"
76
76
  >
77
- <button
78
- type="button"
79
- aria-label="Decrease value"
80
- :disabled="disabled || !canDecrement"
81
- :class="[
82
- 'mld-number-input__button',
83
- 'mld-number-input__button--decrement',
84
- `mld-number-input__button--${size}`,
85
- ]"
86
- @click="decrement"
87
- >
88
- <svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
89
- <path d="M5 12h14" />
90
- </svg>
91
- </button>
92
-
93
77
  <input
94
78
  type="number"
95
79
  :value="modelValue"
@@ -106,21 +90,39 @@ function increment() {
106
90
  @input="handleInput"
107
91
  />
108
92
 
109
- <button
110
- type="button"
111
- aria-label="Increase value"
112
- :disabled="disabled || !canIncrement"
113
- :class="[
114
- 'mld-number-input__button',
115
- 'mld-number-input__button--increment',
116
- `mld-number-input__button--${size}`,
117
- ]"
118
- @click="increment"
119
- >
120
- <svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
121
- <path d="M5 12h14" /><path d="M12 5v14" />
122
- </svg>
123
- </button>
93
+ <div class="mld-number-input__buttons">
94
+ <button
95
+ type="button"
96
+ aria-label="Decrease value"
97
+ :disabled="disabled || !canDecrement"
98
+ :class="[
99
+ 'mld-number-input__button',
100
+ 'mld-number-input__button--decrement',
101
+ `mld-number-input__button--${size}`,
102
+ ]"
103
+ @click="decrement"
104
+ >
105
+ <svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
106
+ <path d="M5 12h14" />
107
+ </svg>
108
+ </button>
109
+
110
+ <button
111
+ type="button"
112
+ aria-label="Increase value"
113
+ :disabled="disabled || !canIncrement"
114
+ :class="[
115
+ 'mld-number-input__button',
116
+ 'mld-number-input__button--increment',
117
+ `mld-number-input__button--${size}`,
118
+ ]"
119
+ @click="increment"
120
+ >
121
+ <svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
122
+ <path d="M5 12h14" /><path d="M12 5v14" />
123
+ </svg>
124
+ </button>
125
+ </div>
124
126
  </div>
125
127
  </template>
126
128