@morscherlab/mld-sdk 0.7.5 → 0.7.6

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/composables/useAutoGroup.test.d.ts +1 -0
  3. package/dist/components/AutoGroupModal.vue.js +522 -0
  4. package/dist/components/AutoGroupModal.vue.js.map +1 -0
  5. package/dist/components/AutoGroupModal.vue3.js +6 -0
  6. package/dist/components/AutoGroupModal.vue3.js.map +1 -0
  7. package/dist/components/FormActions.vue.d.ts +1 -1
  8. package/dist/components/GroupingModal.vue.js.map +1 -1
  9. package/dist/components/SampleSelector.vue.d.ts +5 -9
  10. package/dist/components/SampleSelector.vue.js +127 -255
  11. package/dist/components/SampleSelector.vue.js.map +1 -1
  12. package/dist/components/index.d.ts +1 -0
  13. package/dist/components/index.js +53 -50
  14. package/dist/components/index.js.map +1 -1
  15. package/dist/composables/index.d.ts +1 -0
  16. package/dist/composables/index.js +3 -0
  17. package/dist/composables/index.js.map +1 -1
  18. package/dist/composables/useAutoGroup.d.ts +80 -0
  19. package/dist/composables/useAutoGroup.js +397 -0
  20. package/dist/composables/useAutoGroup.js.map +1 -0
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +48 -42
  23. package/dist/index.js.map +1 -1
  24. package/dist/styles.css +2652 -1788
  25. package/dist/types/auto-group.d.ts +31 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/composables/useAutoGroup.test.ts +375 -0
  29. package/src/components/AutoGroupModal.story.vue +155 -0
  30. package/src/components/AutoGroupModal.vue +441 -0
  31. package/src/components/GroupingModal.vue +1 -0
  32. package/src/components/SampleSelector.story.vue +8 -31
  33. package/src/components/SampleSelector.vue +22 -137
  34. package/src/components/index.ts +1 -0
  35. package/src/composables/index.ts +1 -0
  36. package/src/composables/useAutoGroup.ts +495 -0
  37. package/src/index.ts +12 -0
  38. package/src/styles/components/auto-group-modal.css +501 -0
  39. package/src/styles/index.css +1 -0
  40. package/src/types/auto-group.ts +37 -0
  41. package/src/types/index.ts +11 -0
@@ -0,0 +1,397 @@
1
+ import { ref, computed } from "vue";
2
+ const DEFAULT_COLORS = [
3
+ "#3B82F6",
4
+ "#10B981",
5
+ "#F59E0B",
6
+ "#EF4444",
7
+ "#8B5CF6",
8
+ "#EC4899",
9
+ "#06B6D4",
10
+ "#84CC16",
11
+ "#F97316",
12
+ "#6366F1"
13
+ ];
14
+ const DELIMITER_CANDIDATES = ["_", "-", "."];
15
+ function analyzeDelimiter(lines) {
16
+ if (lines.length === 0) {
17
+ return { delimiter: "_", dominantFieldCount: 1, minFieldCount: 1, consistency: 0 };
18
+ }
19
+ let bestDelimiter = "_";
20
+ let bestConsistency = -1;
21
+ let bestFieldCount = 1;
22
+ for (const candidate of DELIMITER_CANDIDATES) {
23
+ const fieldCounts = lines.map((line) => line.split(candidate).length);
24
+ const countFrequency = /* @__PURE__ */ new Map();
25
+ for (const count of fieldCounts) {
26
+ countFrequency.set(count, (countFrequency.get(count) ?? 0) + 1);
27
+ }
28
+ let modeCount = 1;
29
+ let modeFrequency = 0;
30
+ for (const [count, freq] of countFrequency) {
31
+ if (freq > modeFrequency || freq === modeFrequency && count > modeCount) {
32
+ modeCount = count;
33
+ modeFrequency = freq;
34
+ }
35
+ }
36
+ const rawConsistency = modeFrequency / lines.length;
37
+ const consistency = modeCount > 1 ? rawConsistency : 0;
38
+ if (consistency > bestConsistency || consistency === bestConsistency && DELIMITER_CANDIDATES.indexOf(candidate) < DELIMITER_CANDIDATES.indexOf(bestDelimiter)) {
39
+ bestDelimiter = candidate;
40
+ bestConsistency = consistency;
41
+ bestFieldCount = modeCount;
42
+ }
43
+ }
44
+ const bestFieldCounts = lines.map((line) => line.split(bestDelimiter).length);
45
+ const multiFieldCounts = bestFieldCounts.filter((c) => c >= 2);
46
+ const minFieldCount = multiFieldCounts.length > 0 ? Math.min(...multiFieldCounts) : 1;
47
+ return {
48
+ delimiter: bestDelimiter,
49
+ dominantFieldCount: bestFieldCount,
50
+ minFieldCount,
51
+ consistency: bestConsistency
52
+ };
53
+ }
54
+ function detectOutliers(lines, delimiter, minFieldCount) {
55
+ const outliers = [];
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const fieldCount = lines[i].split(delimiter).length;
58
+ if (fieldCount < minFieldCount) {
59
+ outliers.push({
60
+ sample: lines[i],
61
+ index: i,
62
+ fieldCount,
63
+ action: "include"
64
+ });
65
+ }
66
+ }
67
+ return outliers;
68
+ }
69
+ function extractColumns(samples, delimiter, minFieldCount) {
70
+ if (samples.length === 0) return [];
71
+ const suffixCount = minFieldCount - 1;
72
+ const rows = samples.map((s) => {
73
+ const parts = s.split(delimiter);
74
+ const splitAt = parts.length - suffixCount;
75
+ return [
76
+ parts.slice(0, splitAt).join(delimiter),
77
+ ...parts.slice(splitAt)
78
+ ];
79
+ });
80
+ const columnCount = minFieldCount;
81
+ const columns = [];
82
+ for (let col = 0; col < columnCount; col++) {
83
+ const values = rows.map((row) => row[col]);
84
+ const unique = [...new Set(values)];
85
+ columns.push({
86
+ index: col,
87
+ name: col === 0 ? "Condition" : `Field ${col + 1}`,
88
+ uniqueValues: unique,
89
+ cardinality: unique.length,
90
+ type: col === 0 ? "prefix" : "suffix"
91
+ });
92
+ }
93
+ return columns;
94
+ }
95
+ function parseCSVLine(line) {
96
+ const result = [];
97
+ let current = "";
98
+ let inQuotes = false;
99
+ for (let i = 0; i < line.length; i++) {
100
+ const char = line[i];
101
+ if (char === '"') {
102
+ inQuotes = !inQuotes;
103
+ } else if (char === "," && !inQuotes) {
104
+ result.push(current.trim());
105
+ current = "";
106
+ } else {
107
+ current += char;
108
+ }
109
+ }
110
+ result.push(current.trim());
111
+ return result;
112
+ }
113
+ function parseCSV(text) {
114
+ const lines = text.trim().split("\n");
115
+ if (lines.length < 2) {
116
+ throw new Error("CSV must have at least a header and one data row");
117
+ }
118
+ const headers = parseCSVLine(lines[0]);
119
+ const rows = [];
120
+ for (let i = 1; i < lines.length; i++) {
121
+ const values = parseCSVLine(lines[i]);
122
+ if (values.length !== headers.length) continue;
123
+ const row = {};
124
+ headers.forEach((header, idx) => {
125
+ row[header] = values[idx];
126
+ });
127
+ rows.push(row);
128
+ }
129
+ const sampleKeywords = ["sample", "name", "id", "sample_name", "samplename"];
130
+ const sampleColumn = headers.find((h) => sampleKeywords.includes(h.toLowerCase())) ?? headers[0];
131
+ return { columns: headers, rows, sampleColumn };
132
+ }
133
+ function computeGroups(allSamples, columns, enabledFields, outlierActions, delimiter, minFieldCount) {
134
+ const excludedSamples = [];
135
+ const qcSamples = [];
136
+ const conformingSamples = [];
137
+ for (let i = 0; i < allSamples.length; i++) {
138
+ const action = outlierActions.get(i);
139
+ if (action === "exclude") {
140
+ excludedSamples.push(allSamples[i]);
141
+ } else if (action === "qc") {
142
+ qcSamples.push(allSamples[i]);
143
+ } else {
144
+ conformingSamples.push(allSamples[i]);
145
+ }
146
+ }
147
+ const groupMap = /* @__PURE__ */ new Map();
148
+ const metadata = [];
149
+ const enabledIndices = [...enabledFields].sort((a, b) => a - b);
150
+ const suffixCount = minFieldCount - 1;
151
+ for (const sample of conformingSamples) {
152
+ const parts = sample.split(delimiter);
153
+ const splitAt = Math.max(1, parts.length - suffixCount);
154
+ const row = [
155
+ parts.slice(0, splitAt).join(delimiter),
156
+ ...parts.slice(splitAt)
157
+ ];
158
+ const keyParts = [];
159
+ for (const idx of enabledIndices) {
160
+ if (idx < row.length && idx < columns.length) {
161
+ keyParts.push(row[idx]);
162
+ }
163
+ }
164
+ const groupKey = keyParts.join(" / ");
165
+ if (!groupMap.has(groupKey)) {
166
+ groupMap.set(groupKey, []);
167
+ }
168
+ groupMap.get(groupKey).push(sample);
169
+ const fields = {};
170
+ for (const col of columns) {
171
+ if (col.index < row.length) {
172
+ fields[col.name] = row[col.index];
173
+ }
174
+ }
175
+ metadata.push({ sampleName: sample, fields, group: groupKey });
176
+ }
177
+ const groups = [];
178
+ let colorIdx = 0;
179
+ for (const [name, samples] of groupMap) {
180
+ groups.push({
181
+ name,
182
+ color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
183
+ samples
184
+ });
185
+ colorIdx++;
186
+ }
187
+ if (qcSamples.length > 0) {
188
+ groups.push({
189
+ name: "QC",
190
+ color: "#6B7280",
191
+ samples: qcSamples
192
+ });
193
+ for (const sample of qcSamples) {
194
+ metadata.push({ sampleName: sample, fields: {}, group: "QC" });
195
+ }
196
+ }
197
+ return { groups, metadata, excludedSamples };
198
+ }
199
+ function useAutoGroup() {
200
+ const inputMode = ref("paste");
201
+ const rawText = ref("");
202
+ const csvData = ref(null);
203
+ const delimiter = ref("_");
204
+ const dominantFieldCount = ref(1);
205
+ const minFieldCount = ref(1);
206
+ const outliers = ref([]);
207
+ const fields = ref([]);
208
+ const fieldNames = ref({});
209
+ const enabledFields = ref(/* @__PURE__ */ new Set());
210
+ const samples = computed(() => {
211
+ if (inputMode.value === "csv" && csvData.value) {
212
+ return csvData.value.rows.map((r) => r[csvData.value.sampleColumn]);
213
+ }
214
+ return rawText.value.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
215
+ });
216
+ const hasOutliers = computed(() => outliers.value.length > 0);
217
+ const conformingSamples = computed(() => {
218
+ const outlierIndices = new Set(outliers.value.map((o) => o.index));
219
+ return samples.value.filter((_, i) => !outlierIndices.has(i));
220
+ });
221
+ const outlierActions = computed(() => {
222
+ const map = /* @__PURE__ */ new Map();
223
+ for (const o of outliers.value) {
224
+ map.set(o.index, o.action);
225
+ }
226
+ return map;
227
+ });
228
+ const effectiveColumns = computed(() => {
229
+ return fields.value.map((col) => ({
230
+ ...col,
231
+ name: fieldNames.value[col.index] ?? col.name
232
+ }));
233
+ });
234
+ const groups = computed(() => {
235
+ if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
236
+ return [];
237
+ }
238
+ const result2 = computeGroups(
239
+ samples.value,
240
+ effectiveColumns.value,
241
+ enabledFields.value,
242
+ outlierActions.value,
243
+ delimiter.value,
244
+ minFieldCount.value
245
+ );
246
+ return result2.groups;
247
+ });
248
+ const metadata = computed(() => {
249
+ if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
250
+ return [];
251
+ }
252
+ return computeGroups(
253
+ samples.value,
254
+ effectiveColumns.value,
255
+ enabledFields.value,
256
+ outlierActions.value,
257
+ delimiter.value,
258
+ minFieldCount.value
259
+ ).metadata;
260
+ });
261
+ const excludedSamples = computed(() => {
262
+ return computeGroups(
263
+ samples.value,
264
+ effectiveColumns.value,
265
+ enabledFields.value,
266
+ outlierActions.value,
267
+ delimiter.value,
268
+ minFieldCount.value
269
+ ).excludedSamples;
270
+ });
271
+ const result = computed(() => ({
272
+ groups: groups.value,
273
+ metadata: metadata.value,
274
+ excludedSamples: excludedSamples.value
275
+ }));
276
+ function parseInput() {
277
+ if (inputMode.value === "csv" && csvData.value) {
278
+ parseCsvInput();
279
+ } else {
280
+ parsePasteInput();
281
+ }
282
+ }
283
+ function parsePasteInput() {
284
+ const lines = samples.value;
285
+ if (lines.length === 0) return;
286
+ const analysis = analyzeDelimiter(lines);
287
+ delimiter.value = analysis.delimiter;
288
+ dominantFieldCount.value = analysis.dominantFieldCount;
289
+ minFieldCount.value = analysis.minFieldCount;
290
+ outliers.value = detectOutliers(lines, analysis.delimiter, analysis.minFieldCount);
291
+ const conforming = lines.filter(
292
+ (_, i) => !outliers.value.some((o) => o.index === i)
293
+ );
294
+ fields.value = extractColumns(conforming, analysis.delimiter, analysis.minFieldCount);
295
+ fieldNames.value = {};
296
+ enabledFields.value = new Set(fields.value.map((f) => f.index));
297
+ }
298
+ function parseCsvInput() {
299
+ if (!csvData.value) return;
300
+ const csv = csvData.value;
301
+ const nonSampleCols = csv.columns.filter((c) => c !== csv.sampleColumn);
302
+ fields.value = nonSampleCols.map((col, i) => {
303
+ const values = csv.rows.map((r) => r[col]);
304
+ const unique = [...new Set(values)];
305
+ return {
306
+ index: i,
307
+ name: col,
308
+ uniqueValues: unique,
309
+ cardinality: unique.length
310
+ };
311
+ });
312
+ outliers.value = [];
313
+ delimiter.value = ",";
314
+ dominantFieldCount.value = csv.columns.length;
315
+ fieldNames.value = {};
316
+ for (const f of fields.value) {
317
+ fieldNames.value[f.index] = f.name;
318
+ }
319
+ enabledFields.value = new Set(fields.value.map((f) => f.index));
320
+ }
321
+ function setOutlierAction(index, action) {
322
+ const outlier = outliers.value.find((o) => o.index === index);
323
+ if (outlier) {
324
+ outlier.action = action;
325
+ outliers.value = [...outliers.value];
326
+ }
327
+ }
328
+ function setAllOutlierActions(action) {
329
+ for (const outlier of outliers.value) {
330
+ outlier.action = action;
331
+ }
332
+ outliers.value = [...outliers.value];
333
+ }
334
+ function toggleField(index) {
335
+ const newSet = new Set(enabledFields.value);
336
+ if (newSet.has(index)) {
337
+ newSet.delete(index);
338
+ } else {
339
+ newSet.add(index);
340
+ }
341
+ enabledFields.value = newSet;
342
+ }
343
+ function renameField(index, name) {
344
+ fieldNames.value = { ...fieldNames.value, [index]: name };
345
+ }
346
+ function reset() {
347
+ rawText.value = "";
348
+ csvData.value = null;
349
+ delimiter.value = "_";
350
+ dominantFieldCount.value = 1;
351
+ minFieldCount.value = 1;
352
+ outliers.value = [];
353
+ fields.value = [];
354
+ fieldNames.value = {};
355
+ enabledFields.value = /* @__PURE__ */ new Set();
356
+ }
357
+ return {
358
+ // State
359
+ inputMode,
360
+ rawText,
361
+ csvData,
362
+ delimiter,
363
+ dominantFieldCount,
364
+ minFieldCount,
365
+ outliers,
366
+ fields,
367
+ fieldNames,
368
+ enabledFields,
369
+ // Computed
370
+ samples,
371
+ hasOutliers,
372
+ conformingSamples,
373
+ groups,
374
+ metadata,
375
+ excludedSamples,
376
+ result,
377
+ effectiveColumns,
378
+ // Actions
379
+ parseInput,
380
+ setOutlierAction,
381
+ setAllOutlierActions,
382
+ toggleField,
383
+ renameField,
384
+ reset
385
+ };
386
+ }
387
+ export {
388
+ DEFAULT_COLORS,
389
+ analyzeDelimiter,
390
+ computeGroups,
391
+ detectOutliers,
392
+ extractColumns,
393
+ parseCSV,
394
+ parseCSVLine,
395
+ useAutoGroup
396
+ };
397
+ //# sourceMappingURL=useAutoGroup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAutoGroup.js","sources":["../../src/composables/useAutoGroup.ts"],"sourcesContent":["import { ref, computed } from 'vue'\nimport type {\n InputMode,\n OutlierAction,\n OutlierInfo,\n ColumnInfo,\n MetadataRow,\n AutoGroupResult,\n ParsedCsvData,\n} from '../types/auto-group'\nimport type { SampleGroup } from '../types/components'\n\nexport const DEFAULT_COLORS = [\n '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',\n '#EC4899', '#06B6D4', '#84CC16', '#F97316', '#6366F1',\n]\n\nconst DELIMITER_CANDIDATES = ['_', '-', '.'] as const\n\n// --- Pure functions (exported for testing) ---\n\nexport function analyzeDelimiter(lines: string[]): {\n delimiter: string\n dominantFieldCount: number\n minFieldCount: number\n consistency: number\n} {\n if (lines.length === 0) {\n return { delimiter: '_', dominantFieldCount: 1, minFieldCount: 1, consistency: 0 }\n }\n\n let bestDelimiter = '_'\n let bestConsistency = -1\n let bestFieldCount = 1\n\n for (const candidate of DELIMITER_CANDIDATES) {\n const fieldCounts = lines.map(line => line.split(candidate).length)\n const countFrequency = new Map<number, number>()\n\n for (const count of fieldCounts) {\n countFrequency.set(count, (countFrequency.get(count) ?? 0) + 1)\n }\n\n // Find mode (most frequent field count)\n let modeCount = 1\n let modeFrequency = 0\n for (const [count, freq] of countFrequency) {\n if (freq > modeFrequency || (freq === modeFrequency && count > modeCount)) {\n modeCount = count\n modeFrequency = freq\n }\n }\n\n const rawConsistency = modeFrequency / lines.length\n // A delimiter that produces field count 1 didn't actually split anything\n const consistency = modeCount > 1 ? rawConsistency : 0\n\n if (\n consistency > bestConsistency ||\n (consistency === bestConsistency &&\n DELIMITER_CANDIDATES.indexOf(candidate) < DELIMITER_CANDIDATES.indexOf(bestDelimiter as typeof candidate))\n ) {\n bestDelimiter = candidate\n bestConsistency = consistency\n bestFieldCount = modeCount\n }\n }\n\n const bestFieldCounts = lines.map(line => line.split(bestDelimiter).length)\n const multiFieldCounts = bestFieldCounts.filter(c => c >= 2)\n const minFieldCount = multiFieldCounts.length > 0 ? Math.min(...multiFieldCounts) : 1\n\n return {\n delimiter: bestDelimiter,\n dominantFieldCount: bestFieldCount,\n minFieldCount,\n consistency: bestConsistency,\n }\n}\n\nexport function detectOutliers(\n lines: string[],\n delimiter: string,\n minFieldCount: number,\n): OutlierInfo[] {\n const outliers: OutlierInfo[] = []\n\n for (let i = 0; i < lines.length; i++) {\n const fieldCount = lines[i].split(delimiter).length\n if (fieldCount < minFieldCount) {\n outliers.push({\n sample: lines[i],\n index: i,\n fieldCount,\n action: 'include',\n })\n }\n }\n\n return outliers\n}\n\nexport function extractColumns(\n samples: string[],\n delimiter: string,\n minFieldCount: number,\n): ColumnInfo[] {\n if (samples.length === 0) return []\n\n const suffixCount = minFieldCount - 1\n const rows = samples.map(s => {\n const parts = s.split(delimiter)\n const splitAt = parts.length - suffixCount\n return [\n parts.slice(0, splitAt).join(delimiter),\n ...parts.slice(splitAt),\n ]\n })\n\n const columnCount = minFieldCount\n const columns: ColumnInfo[] = []\n for (let col = 0; col < columnCount; col++) {\n const values = rows.map(row => row[col])\n const unique = [...new Set(values)]\n columns.push({\n index: col,\n name: col === 0 ? 'Condition' : `Field ${col + 1}`,\n uniqueValues: unique,\n cardinality: unique.length,\n type: col === 0 ? 'prefix' : 'suffix',\n })\n }\n\n return columns\n}\n\nexport function parseCSVLine(line: string): string[] {\n const result: string[] = []\n let current = ''\n let inQuotes = false\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i]\n if (char === '\"') {\n inQuotes = !inQuotes\n } else if (char === ',' && !inQuotes) {\n result.push(current.trim())\n current = ''\n } else {\n current += char\n }\n }\n result.push(current.trim())\n\n return result\n}\n\nexport function parseCSV(text: string): ParsedCsvData {\n const lines = text.trim().split('\\n')\n if (lines.length < 2) {\n throw new Error('CSV must have at least a header and one data row')\n }\n\n const headers = parseCSVLine(lines[0])\n const rows: Record<string, string>[] = []\n\n for (let i = 1; i < lines.length; i++) {\n const values = parseCSVLine(lines[i])\n if (values.length !== headers.length) continue\n const row: Record<string, string> = {}\n headers.forEach((header, idx) => {\n row[header] = values[idx]\n })\n rows.push(row)\n }\n\n // Auto-detect sample column\n const sampleKeywords = ['sample', 'name', 'id', 'sample_name', 'samplename']\n const sampleColumn =\n headers.find(h => sampleKeywords.includes(h.toLowerCase())) ?? headers[0]\n\n return { columns: headers, rows, sampleColumn }\n}\n\nexport function computeGroups(\n allSamples: string[],\n columns: ColumnInfo[],\n enabledFields: Set<number>,\n outlierActions: Map<number, OutlierAction>,\n delimiter: string,\n minFieldCount: number,\n): { groups: SampleGroup[]; metadata: MetadataRow[]; excludedSamples: string[] } {\n const excludedSamples: string[] = []\n const qcSamples: string[] = []\n const conformingSamples: string[] = []\n\n for (let i = 0; i < allSamples.length; i++) {\n const action = outlierActions.get(i)\n if (action === 'exclude') {\n excludedSamples.push(allSamples[i])\n } else if (action === 'qc') {\n qcSamples.push(allSamples[i])\n } else {\n conformingSamples.push(allSamples[i])\n }\n }\n\n // Build group map\n const groupMap = new Map<string, string[]>()\n const metadata: MetadataRow[] = []\n const enabledIndices = [...enabledFields].sort((a, b) => a - b)\n\n const suffixCount = minFieldCount - 1\n\n for (const sample of conformingSamples) {\n const parts = sample.split(delimiter)\n const splitAt = Math.max(1, parts.length - suffixCount)\n const row = [\n parts.slice(0, splitAt).join(delimiter),\n ...parts.slice(splitAt),\n ]\n\n // Build group key from enabled columns\n const keyParts: string[] = []\n for (const idx of enabledIndices) {\n if (idx < row.length && idx < columns.length) {\n keyParts.push(row[idx])\n }\n }\n const groupKey = keyParts.join(' / ')\n\n if (!groupMap.has(groupKey)) {\n groupMap.set(groupKey, [])\n }\n groupMap.get(groupKey)!.push(sample)\n\n // Build metadata row with ALL columns\n const fields: Record<string, string> = {}\n for (const col of columns) {\n if (col.index < row.length) {\n fields[col.name] = row[col.index]\n }\n }\n metadata.push({ sampleName: sample, fields, group: groupKey })\n }\n\n // Convert to SampleGroup[]\n const groups: SampleGroup[] = []\n let colorIdx = 0\n for (const [name, samples] of groupMap) {\n groups.push({\n name,\n color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],\n samples,\n })\n colorIdx++\n }\n\n // QC group\n if (qcSamples.length > 0) {\n groups.push({\n name: 'QC',\n color: '#6B7280',\n samples: qcSamples,\n })\n for (const sample of qcSamples) {\n metadata.push({ sampleName: sample, fields: {}, group: 'QC' })\n }\n }\n\n return { groups, metadata, excludedSamples }\n}\n\n// --- Reactive composable ---\n\nexport function useAutoGroup() {\n const inputMode = ref<InputMode>('paste')\n const rawText = ref('')\n const csvData = ref<ParsedCsvData | null>(null)\n const delimiter = ref('_')\n const dominantFieldCount = ref(1)\n const minFieldCount = ref(1)\n const outliers = ref<OutlierInfo[]>([])\n const fields = ref<ColumnInfo[]>([])\n const fieldNames = ref<Record<number, string>>({})\n const enabledFields = ref(new Set<number>())\n\n const samples = computed(() => {\n if (inputMode.value === 'csv' && csvData.value) {\n return csvData.value.rows.map(r => r[csvData.value!.sampleColumn])\n }\n return rawText.value\n .split('\\n')\n .map(l => l.trim())\n .filter(l => l.length > 0)\n })\n\n const hasOutliers = computed(() => outliers.value.length > 0)\n\n const conformingSamples = computed(() => {\n const outlierIndices = new Set(outliers.value.map(o => o.index))\n return samples.value.filter((_, i) => !outlierIndices.has(i))\n })\n\n const outlierActions = computed(() => {\n const map = new Map<number, OutlierAction>()\n for (const o of outliers.value) {\n map.set(o.index, o.action)\n }\n return map\n })\n\n const effectiveColumns = computed(() => {\n return fields.value.map(col => ({\n ...col,\n name: fieldNames.value[col.index] ?? col.name,\n }))\n })\n\n const groups = computed(() => {\n if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {\n return []\n }\n const result = computeGroups(\n samples.value,\n effectiveColumns.value,\n enabledFields.value,\n outlierActions.value,\n delimiter.value,\n minFieldCount.value,\n )\n return result.groups\n })\n\n const metadata = computed(() => {\n if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {\n return []\n }\n return computeGroups(\n samples.value,\n effectiveColumns.value,\n enabledFields.value,\n outlierActions.value,\n delimiter.value,\n minFieldCount.value,\n ).metadata\n })\n\n const excludedSamples = computed(() => {\n return computeGroups(\n samples.value,\n effectiveColumns.value,\n enabledFields.value,\n outlierActions.value,\n delimiter.value,\n minFieldCount.value,\n ).excludedSamples\n })\n\n const result = computed<AutoGroupResult>(() => ({\n groups: groups.value,\n metadata: metadata.value,\n excludedSamples: excludedSamples.value,\n }))\n\n function parseInput() {\n if (inputMode.value === 'csv' && csvData.value) {\n parseCsvInput()\n } else {\n parsePasteInput()\n }\n }\n\n function parsePasteInput() {\n const lines = samples.value\n if (lines.length === 0) return\n\n const analysis = analyzeDelimiter(lines)\n delimiter.value = analysis.delimiter\n dominantFieldCount.value = analysis.dominantFieldCount\n minFieldCount.value = analysis.minFieldCount\n\n outliers.value = detectOutliers(lines, analysis.delimiter, analysis.minFieldCount)\n\n const conforming = lines.filter(\n (_, i) => !outliers.value.some(o => o.index === i)\n )\n fields.value = extractColumns(conforming, analysis.delimiter, analysis.minFieldCount)\n\n // Reset field names and enable all by default\n fieldNames.value = {}\n enabledFields.value = new Set(fields.value.map(f => f.index))\n }\n\n function parseCsvInput() {\n if (!csvData.value) return\n\n const csv = csvData.value\n const nonSampleCols = csv.columns.filter(c => c !== csv.sampleColumn)\n\n fields.value = nonSampleCols.map((col, i) => {\n const values = csv.rows.map(r => r[col])\n const unique = [...new Set(values)]\n return {\n index: i,\n name: col,\n uniqueValues: unique,\n cardinality: unique.length,\n }\n })\n\n // For CSV, no outliers\n outliers.value = []\n delimiter.value = ','\n dominantFieldCount.value = csv.columns.length\n\n fieldNames.value = {}\n for (const f of fields.value) {\n fieldNames.value[f.index] = f.name\n }\n enabledFields.value = new Set(fields.value.map(f => f.index))\n }\n\n function setOutlierAction(index: number, action: OutlierAction) {\n const outlier = outliers.value.find(o => o.index === index)\n if (outlier) {\n outlier.action = action\n // Trigger reactivity\n outliers.value = [...outliers.value]\n }\n }\n\n function setAllOutlierActions(action: OutlierAction) {\n for (const outlier of outliers.value) {\n outlier.action = action\n }\n outliers.value = [...outliers.value]\n }\n\n function toggleField(index: number) {\n const newSet = new Set(enabledFields.value)\n if (newSet.has(index)) {\n newSet.delete(index)\n } else {\n newSet.add(index)\n }\n enabledFields.value = newSet\n }\n\n function renameField(index: number, name: string) {\n fieldNames.value = { ...fieldNames.value, [index]: name }\n }\n\n function reset() {\n rawText.value = ''\n csvData.value = null\n delimiter.value = '_'\n dominantFieldCount.value = 1\n minFieldCount.value = 1\n outliers.value = []\n fields.value = []\n fieldNames.value = {}\n enabledFields.value = new Set()\n }\n\n return {\n // State\n inputMode,\n rawText,\n csvData,\n delimiter,\n dominantFieldCount,\n minFieldCount,\n outliers,\n fields,\n fieldNames,\n enabledFields,\n // Computed\n samples,\n hasOutliers,\n conformingSamples,\n groups,\n metadata,\n excludedSamples,\n result,\n effectiveColumns,\n // Actions\n parseInput,\n setOutlierAction,\n setAllOutlierActions,\n toggleField,\n renameField,\n reset,\n }\n}\n"],"names":["result"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { MLDSdk, default } from './install';
2
- export { BaseButton, BaseInput, BaseTextarea, BaseSelect, BaseCheckbox, BaseToggle, BaseRadioGroup, BaseSlider, ColorSlider, BaseTabs, BaseModal, FormField, DatePicker, TimePicker, TagsInput, NumberInput, FileUploader, AlertBox, ToastNotification, IconButton, ThemeToggle, SettingsButton, CollapsibleCard, AppTopBar, AppSidebar, AppLayout, AppContainer, Skeleton, WellPlate, RackEditor, SampleLegend, PlateMapEditor, ExperimentTimeline, SampleSelector, GroupingModal, GroupAssigner, MoleculeInput, ConcentrationInput, DoseCalculator, ReagentList, SampleHierarchyTree, ProtocolStepEditor, SegmentedControl, MultiSelect, BasePill, DropdownButton, Calendar, DataFrame, LoadingSpinner, Divider, StatusIndicator, ProgressBar, Avatar, EmptyState, Breadcrumb, Tooltip, ConfirmDialog, ChartContainer, SettingsModal, ScientificNumber, ChemicalFormula, FormulaInput, SequenceInput, UnitInput, StepWizard, AuditTrail, BatchProgressList, ExperimentDataViewer, ExperimentCodeBadge, DateTimePicker, TimeRangeInput, ScheduleCalendar, ResourceCard, } from './components';
3
- export { useApi, useAuth, usePasskey, useTheme, useToast, usePlatformContext, useWellPlateEditor, useConcentrationUnits, useDoseCalculator, useProtocolTemplates, useRackEditor, useChemicalFormula, ATOMIC_WEIGHTS, useSequenceUtils, type ApiClientOptions, type UseWellPlateEditorOptions, type UseWellPlateEditorReturn, type UseRackEditorOptions, type UseRackEditorReturn, type ConcentrationValue, type ConcentrationUnit, type VolumeValue, type VolumeUnit, type StepTemplate, type FormulaParseResult, type FormulaPart, type SequenceType, type SequenceStats, parseTime, formatTime, generateTimeSlots, rangesOverlap, durationMinutes, formatDuration, isTimeInRange, findAvailableSlots, snapToSlot, addMinutes, compareTime, useScheduleDrag, usePluginConfig, type UsePluginConfigReturn, } from './composables';
2
+ export { BaseButton, BaseInput, BaseTextarea, BaseSelect, BaseCheckbox, BaseToggle, BaseRadioGroup, BaseSlider, ColorSlider, BaseTabs, BaseModal, FormField, DatePicker, TimePicker, TagsInput, NumberInput, FileUploader, AlertBox, ToastNotification, IconButton, ThemeToggle, SettingsButton, CollapsibleCard, AppTopBar, AppSidebar, AppLayout, AppContainer, Skeleton, WellPlate, RackEditor, SampleLegend, PlateMapEditor, ExperimentTimeline, SampleSelector, GroupingModal, AutoGroupModal, GroupAssigner, MoleculeInput, ConcentrationInput, DoseCalculator, ReagentList, SampleHierarchyTree, ProtocolStepEditor, SegmentedControl, MultiSelect, BasePill, DropdownButton, Calendar, DataFrame, LoadingSpinner, Divider, StatusIndicator, ProgressBar, Avatar, EmptyState, Breadcrumb, Tooltip, ConfirmDialog, ChartContainer, SettingsModal, ScientificNumber, ChemicalFormula, FormulaInput, SequenceInput, UnitInput, StepWizard, AuditTrail, BatchProgressList, ExperimentDataViewer, ExperimentCodeBadge, DateTimePicker, TimeRangeInput, ScheduleCalendar, ResourceCard, } from './components';
3
+ export { useApi, useAuth, usePasskey, useTheme, useToast, usePlatformContext, useWellPlateEditor, useConcentrationUnits, useDoseCalculator, useProtocolTemplates, useRackEditor, useChemicalFormula, ATOMIC_WEIGHTS, useSequenceUtils, type ApiClientOptions, type UseWellPlateEditorOptions, type UseWellPlateEditorReturn, type UseRackEditorOptions, type UseRackEditorReturn, type ConcentrationValue, type ConcentrationUnit, type VolumeValue, type VolumeUnit, type StepTemplate, type FormulaParseResult, type FormulaPart, type SequenceType, type SequenceStats, parseTime, formatTime, generateTimeSlots, rangesOverlap, durationMinutes, formatDuration, isTimeInRange, findAvailableSlots, snapToSlot, addMinutes, compareTime, useScheduleDrag, usePluginConfig, type UsePluginConfigReturn, useAutoGroup, DEFAULT_COLORS, } from './composables';
4
4
  export { useAuthStore, useSettingsStore, colorPalettes, type SettingsState, } from './stores';
5
- export type { ContainerDirection, ButtonVariant, ButtonSize, InputType, ModalSize, AlertType, Toast, TabItem, SelectOption, RadioOption, FormFieldProps, SidebarToolSection, CollapsibleState, TopBarVariant, TopBarPage, TopBarTab, TopBarTabOption, TopBarSettingsConfig, WellPlateFormat, WellState, WellPlateSelectionMode, Well, HeatmapColorScale, HeatmapConfig, SlotPosition, WellExtendedData, WellEditData, WellEditField, WellLegendItem, Rack, SampleType, PlateMap, PlateMapEditorState, ProtocolStepType, ProtocolStepStatus, ProtocolStep, SampleGroup, GroupItem, FileUploaderMode, SegmentedOption, SegmentedControlVariant, SegmentedControlSize, MultiSelectOption, MultiSelectSize, PillVariant, PillSize, CalendarSelectionMode, CalendarMarker, CalendarDayContext, SortDirection, SortState, DataFrameColumn, PaginationState, SpinnerSize, SpinnerVariant, DividerSpacing, StatusType, ProgressVariant, ProgressSize, AvatarSize, EmptyStateColor, EmptyStateSize, BreadcrumbItem, TooltipPosition, ConfirmVariant, SettingsTab, NumberNotation, TimePickerFormat, TimeRange, ScheduleView, ScheduleEventStatus, ScheduleEvent, ScheduleBlockedSlot, ScheduleSlotContext, ScheduleEventCreateContext, ScheduleEventUpdateContext, ResourceStatus, ResourceSpec, UnitOption, WizardStep, WizardStepState, AuditEntryType, AuditEntry, BatchItemStatus, BatchItem, BatchSummary, AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, RegisterRequest, UpdateProfileRequest, CredentialInfo, SummaryData, SummarySection, SummarySectionItem, TreeNode, TreeNodeType, PluginInfo, PluginNavItem, PluginSettings, PluginSettingField, PlatformContext, PlatformEventType, PlatformEvent, ThemeMode, ColorPalette, TableDensity, } from './types';
5
+ export type { ContainerDirection, ButtonVariant, ButtonSize, InputType, ModalSize, AlertType, Toast, TabItem, SelectOption, RadioOption, FormFieldProps, SidebarToolSection, CollapsibleState, TopBarVariant, TopBarPage, TopBarTab, TopBarTabOption, TopBarSettingsConfig, WellPlateFormat, WellState, WellPlateSelectionMode, Well, HeatmapColorScale, HeatmapConfig, SlotPosition, WellExtendedData, WellEditData, WellEditField, WellLegendItem, Rack, SampleType, PlateMap, PlateMapEditorState, ProtocolStepType, ProtocolStepStatus, ProtocolStep, SampleGroup, GroupItem, OutlierAction, InputMode, OutlierInfo, ColumnInfo, MetadataRow, AutoGroupResult, ParsedCsvData, FileUploaderMode, SegmentedOption, SegmentedControlVariant, SegmentedControlSize, MultiSelectOption, MultiSelectSize, PillVariant, PillSize, CalendarSelectionMode, CalendarMarker, CalendarDayContext, SortDirection, SortState, DataFrameColumn, PaginationState, SpinnerSize, SpinnerVariant, DividerSpacing, StatusType, ProgressVariant, ProgressSize, AvatarSize, EmptyStateColor, EmptyStateSize, BreadcrumbItem, TooltipPosition, ConfirmVariant, SettingsTab, NumberNotation, TimePickerFormat, TimeRange, ScheduleView, ScheduleEventStatus, ScheduleEvent, ScheduleBlockedSlot, ScheduleSlotContext, ScheduleEventCreateContext, ScheduleEventUpdateContext, ResourceStatus, ResourceSpec, UnitOption, WizardStep, WizardStepState, AuditEntryType, AuditEntry, BatchItemStatus, BatchItem, BatchSummary, AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, RegisterRequest, UpdateProfileRequest, CredentialInfo, SummaryData, SummarySection, SummarySectionItem, TreeNode, TreeNodeType, PluginInfo, PluginNavItem, PluginSettings, PluginSettingField, PlatformContext, PlatformEventType, PlatformEvent, ThemeMode, ColorPalette, TableDensity, } from './types';
package/dist/index.js CHANGED
@@ -103,47 +103,49 @@ import { default as default52 } from "./components/SampleSelector.vue.js";
103
103
  /* empty css */
104
104
  import { default as default53 } from "./components/GroupingModal.vue.js";
105
105
  /* empty css */
106
- import { default as default54 } from "./components/GroupAssigner.vue.js";
106
+ import { default as default54 } from "./components/AutoGroupModal.vue.js";
107
+ /* empty css */
108
+ import { default as default55 } from "./components/GroupAssigner.vue.js";
107
109
  /* empty css */
108
- import { default as default55 } from "./components/MoleculeInput.vue.js";
110
+ import { default as default56 } from "./components/MoleculeInput.vue.js";
109
111
  /* empty css */
110
- import { default as default56 } from "./components/ConcentrationInput.vue.js";
112
+ import { default as default57 } from "./components/ConcentrationInput.vue.js";
111
113
  /* empty css */
112
- import { default as default57 } from "./components/DoseCalculator.vue.js";
114
+ import { default as default58 } from "./components/DoseCalculator.vue.js";
113
115
  /* empty css */
114
- import { default as default58 } from "./components/ReagentList.vue.js";
116
+ import { default as default59 } from "./components/ReagentList.vue.js";
115
117
  /* empty css */
116
- import { default as default59 } from "./components/SampleHierarchyTree.vue.js";
118
+ import { default as default60 } from "./components/SampleHierarchyTree.vue.js";
117
119
  /* empty css */
118
- import { default as default60 } from "./components/ProtocolStepEditor.vue.js";
120
+ import { default as default61 } from "./components/ProtocolStepEditor.vue.js";
119
121
  /* empty css */
120
- import { default as default61 } from "./components/ScientificNumber.vue.js";
122
+ import { default as default62 } from "./components/ScientificNumber.vue.js";
121
123
  /* empty css */
122
- import { default as default62 } from "./components/ChemicalFormula.vue.js";
124
+ import { default as default63 } from "./components/ChemicalFormula.vue.js";
123
125
  /* empty css */
124
- import { default as default63 } from "./components/FormulaInput.vue.js";
126
+ import { default as default64 } from "./components/FormulaInput.vue.js";
125
127
  /* empty css */
126
- import { default as default64 } from "./components/SequenceInput.vue.js";
128
+ import { default as default65 } from "./components/SequenceInput.vue.js";
127
129
  /* empty css */
128
- import { default as default65 } from "./components/UnitInput.vue.js";
130
+ import { default as default66 } from "./components/UnitInput.vue.js";
129
131
  /* empty css */
130
- import { default as default66 } from "./components/StepWizard.vue.js";
132
+ import { default as default67 } from "./components/StepWizard.vue.js";
131
133
  /* empty css */
132
- import { default as default67 } from "./components/AuditTrail.vue.js";
134
+ import { default as default68 } from "./components/AuditTrail.vue.js";
133
135
  /* empty css */
134
- import { default as default68 } from "./components/BatchProgressList.vue.js";
136
+ import { default as default69 } from "./components/BatchProgressList.vue.js";
135
137
  /* empty css */
136
- import { default as default69 } from "./components/ExperimentDataViewer.vue.js";
138
+ import { default as default70 } from "./components/ExperimentDataViewer.vue.js";
137
139
  /* empty css */
138
- import { default as default70 } from "./components/ExperimentCodeBadge.vue.js";
140
+ import { default as default71 } from "./components/ExperimentCodeBadge.vue.js";
139
141
  /* empty css */
140
- import { default as default71 } from "./components/DateTimePicker.vue.js";
142
+ import { default as default72 } from "./components/DateTimePicker.vue.js";
141
143
  /* empty css */
142
- import { default as default72 } from "./components/TimeRangeInput.vue.js";
144
+ import { default as default73 } from "./components/TimeRangeInput.vue.js";
143
145
  /* empty css */
144
- import { default as default73 } from "./components/ScheduleCalendar.vue.js";
146
+ import { default as default74 } from "./components/ScheduleCalendar.vue.js";
145
147
  /* empty css */
146
- import { default as default74 } from "./components/ResourceCard.vue.js";
148
+ import { default as default75 } from "./components/ResourceCard.vue.js";
147
149
  /* empty css */
148
150
  import { useApi } from "./composables/useApi.js";
149
151
  import { useAuth } from "./composables/useAuth.js";
@@ -160,6 +162,7 @@ import { ATOMIC_WEIGHTS, useChemicalFormula } from "./composables/useChemicalFor
160
162
  import { useSequenceUtils } from "./composables/useSequenceUtils.js";
161
163
  import { addMinutes, compareTime, durationMinutes, findAvailableSlots, formatDuration, formatTime, generateTimeSlots, isTimeInRange, parseTime, rangesOverlap, snapToSlot } from "./composables/useTimeUtils.js";
162
164
  import { useScheduleDrag } from "./composables/useScheduleDrag.js";
165
+ import { DEFAULT_COLORS, useAutoGroup } from "./composables/useAutoGroup.js";
163
166
  import { usePluginConfig } from "./composables/usePluginConfig.js";
164
167
  import { useAuthStore } from "./stores/auth.js";
165
168
  import { colorPalettes, useSettingsStore } from "./stores/settings.js";
@@ -170,7 +173,8 @@ export {
170
173
  default33 as AppLayout,
171
174
  default32 as AppSidebar,
172
175
  default31 as AppTopBar,
173
- default67 as AuditTrail,
176
+ default68 as AuditTrail,
177
+ default54 as AutoGroupModal,
174
178
  default40 as Avatar,
175
179
  default2 as BaseButton,
176
180
  default6 as BaseCheckbox,
@@ -183,61 +187,62 @@ export {
183
187
  default11 as BaseTabs,
184
188
  default4 as BaseTextarea,
185
189
  default7 as BaseToggle,
186
- default68 as BatchProgressList,
190
+ default69 as BatchProgressList,
187
191
  default42 as Breadcrumb,
188
192
  default17 as Calendar,
189
193
  default45 as ChartContainer,
190
- default62 as ChemicalFormula,
194
+ default63 as ChemicalFormula,
191
195
  default30 as CollapsibleCard,
192
196
  default10 as ColorSlider,
193
- default56 as ConcentrationInput,
197
+ default57 as ConcentrationInput,
194
198
  default44 as ConfirmDialog,
199
+ DEFAULT_COLORS,
195
200
  default18 as DataFrame,
196
201
  default20 as DatePicker,
197
- default71 as DateTimePicker,
202
+ default72 as DateTimePicker,
198
203
  default37 as Divider,
199
- default57 as DoseCalculator,
204
+ default58 as DoseCalculator,
200
205
  default16 as DropdownButton,
201
206
  default41 as EmptyState,
202
- default70 as ExperimentCodeBadge,
203
- default69 as ExperimentDataViewer,
207
+ default71 as ExperimentCodeBadge,
208
+ default70 as ExperimentDataViewer,
204
209
  default51 as ExperimentTimeline,
205
210
  default24 as FileUploader,
206
211
  default19 as FormField,
207
- default63 as FormulaInput,
208
- default54 as GroupAssigner,
212
+ default64 as FormulaInput,
213
+ default55 as GroupAssigner,
209
214
  default53 as GroupingModal,
210
215
  default27 as IconButton,
211
216
  default36 as LoadingSpinner,
212
217
  MLDSdk,
213
- default55 as MoleculeInput,
218
+ default56 as MoleculeInput,
214
219
  default14 as MultiSelect,
215
220
  default23 as NumberInput,
216
221
  default50 as PlateMapEditor,
217
222
  default39 as ProgressBar,
218
- default60 as ProtocolStepEditor,
223
+ default61 as ProtocolStepEditor,
219
224
  default48 as RackEditor,
220
- default58 as ReagentList,
221
- default74 as ResourceCard,
222
- default59 as SampleHierarchyTree,
225
+ default59 as ReagentList,
226
+ default75 as ResourceCard,
227
+ default60 as SampleHierarchyTree,
223
228
  default49 as SampleLegend,
224
229
  default52 as SampleSelector,
225
- default73 as ScheduleCalendar,
226
- default61 as ScientificNumber,
230
+ default74 as ScheduleCalendar,
231
+ default62 as ScientificNumber,
227
232
  default13 as SegmentedControl,
228
- default64 as SequenceInput,
233
+ default65 as SequenceInput,
229
234
  default29 as SettingsButton,
230
235
  default46 as SettingsModal,
231
236
  default35 as Skeleton,
232
237
  default38 as StatusIndicator,
233
- default66 as StepWizard,
238
+ default67 as StepWizard,
234
239
  default22 as TagsInput,
235
240
  default28 as ThemeToggle,
236
241
  default21 as TimePicker,
237
- default72 as TimeRangeInput,
242
+ default73 as TimeRangeInput,
238
243
  default26 as ToastNotification,
239
244
  default43 as Tooltip,
240
- default65 as UnitInput,
245
+ default66 as UnitInput,
241
246
  default47 as WellPlate,
242
247
  addMinutes,
243
248
  colorPalettes,
@@ -255,6 +260,7 @@ export {
255
260
  useApi,
256
261
  useAuth,
257
262
  useAuthStore,
263
+ useAutoGroup,
258
264
  useChemicalFormula,
259
265
  useConcentrationUnits,
260
266
  useDoseCalculator,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}