@morscherlab/mld-sdk 0.9.7 → 0.10.0

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 (56) hide show
  1. package/dist/__tests__/composables/useAppExperiment.test.d.ts +1 -0
  2. package/dist/components/AppTopBar.vue.js +61 -28
  3. package/dist/components/AppTopBar.vue.js.map +1 -1
  4. package/dist/components/AuditTrail.vue.d.ts +1 -1
  5. package/dist/components/ExperimentSelectorModal.vue.d.ts +1 -1
  6. package/dist/components/FormulaInput.vue.js +24 -18
  7. package/dist/components/FormulaInput.vue.js.map +1 -1
  8. package/dist/components/MoleculeInput.vue.js +15 -6
  9. package/dist/components/MoleculeInput.vue.js.map +1 -1
  10. package/dist/components/PlateMapEditor.vue.js +1 -1
  11. package/dist/components/PlateMapEditor.vue.js.map +1 -1
  12. package/dist/components/ScientificNumber.vue.d.ts +1 -1
  13. package/dist/composables/index.d.ts +1 -0
  14. package/dist/composables/index.js +3 -0
  15. package/dist/composables/index.js.map +1 -1
  16. package/dist/composables/useAppExperiment.d.ts +34 -0
  17. package/dist/composables/useAppExperiment.js +91 -0
  18. package/dist/composables/useAppExperiment.js.map +1 -0
  19. package/dist/composables/useAuth.js +26 -25
  20. package/dist/composables/useAuth.js.map +1 -1
  21. package/dist/composables/useAutoGroup.js +7 -32
  22. package/dist/composables/useAutoGroup.js.map +1 -1
  23. package/dist/composables/useForm.js +1 -1
  24. package/dist/composables/useForm.js.map +1 -1
  25. package/dist/composables/usePlatformContext.js +8 -1
  26. package/dist/composables/usePlatformContext.js.map +1 -1
  27. package/dist/composables/useTheme.js +23 -25
  28. package/dist/composables/useTheme.js.map +1 -1
  29. package/dist/composables/useWellPlateEditor.d.ts +1 -0
  30. package/dist/composables/useWellPlateEditor.js +21 -10
  31. package/dist/composables/useWellPlateEditor.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/stores/auth.d.ts +1 -1
  36. package/dist/stores/settings.d.ts +1 -1
  37. package/dist/stores/settings.js +17 -30
  38. package/dist/stores/settings.js.map +1 -1
  39. package/dist/styles.css +6920 -6908
  40. package/package.json +1 -1
  41. package/src/__tests__/composables/useAppExperiment.test.ts +560 -0
  42. package/src/components/AppTopBar.vue +38 -2
  43. package/src/components/FormulaInput.vue +17 -16
  44. package/src/components/MoleculeInput.vue +29 -14
  45. package/src/components/PlateMapEditor.vue +1 -1
  46. package/src/composables/index.ts +7 -0
  47. package/src/composables/useAppExperiment.ts +143 -0
  48. package/src/composables/useAuth.ts +29 -31
  49. package/src/composables/useAutoGroup.ts +7 -33
  50. package/src/composables/useForm.ts +1 -1
  51. package/src/composables/usePlatformContext.ts +7 -1
  52. package/src/composables/useTheme.ts +33 -28
  53. package/src/composables/useWellPlateEditor.ts +22 -10
  54. package/src/index.ts +5 -0
  55. package/src/stores/settings.ts +22 -38
  56. package/src/styles/components/formula-input.css +13 -6
@@ -244,23 +244,9 @@ function useAutoGroup() {
244
244
  name: fieldNames.value[col.index] ?? col.name
245
245
  }));
246
246
  });
247
- const groups = computed(() => {
247
+ const _computedResult = computed(() => {
248
248
  if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
249
- return [];
250
- }
251
- const result2 = computeGroups(
252
- samples.value,
253
- effectiveColumns.value,
254
- enabledFields.value,
255
- outlierActions.value,
256
- delimiter.value,
257
- minFieldCount.value
258
- );
259
- return result2.groups;
260
- });
261
- const metadata = computed(() => {
262
- if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
263
- return [];
249
+ return { groups: [], metadata: [], excludedSamples: [] };
264
250
  }
265
251
  return computeGroups(
266
252
  samples.value,
@@ -269,23 +255,12 @@ function useAutoGroup() {
269
255
  outlierActions.value,
270
256
  delimiter.value,
271
257
  minFieldCount.value
272
- ).metadata;
273
- });
274
- const excludedSamples = computed(() => {
275
- return computeGroups(
276
- samples.value,
277
- effectiveColumns.value,
278
- enabledFields.value,
279
- outlierActions.value,
280
- delimiter.value,
281
- minFieldCount.value
282
- ).excludedSamples;
258
+ );
283
259
  });
284
- const result = computed(() => ({
285
- groups: groups.value,
286
- metadata: metadata.value,
287
- excludedSamples: excludedSamples.value
288
- }));
260
+ const groups = computed(() => _computedResult.value.groups);
261
+ const metadata = computed(() => _computedResult.value.metadata);
262
+ const excludedSamples = computed(() => _computedResult.value.excludedSamples);
263
+ const result = computed(() => _computedResult.value);
289
264
  function parseInput() {
290
265
  if (inputMode.value === "csv" && csvData.value) {
291
266
  parseCsvInput();
@@ -1 +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\nconst QC_KEYWORDS = new Set([\n 'eqc', 'iqc', 'qc', 'blank', 'std', 'standard', 'test',\n])\n\nexport function classifyOutlierAction(\n sample: string,\n delimiter: string,\n): OutlierAction {\n const segments = sample.split(delimiter)\n return segments.some(seg => QC_KEYWORDS.has(seg.toLowerCase()))\n ? 'qc'\n : 'include'\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\n // Use dominantFieldCount as outlier threshold so QC/test samples with\n // fewer fields than the majority are correctly flagged\n outliers.value = detectOutliers(lines, analysis.delimiter, analysis.dominantFieldCount)\n\n // Apply smart default actions: auto-classify QC/test samples\n for (const outlier of outliers.value) {\n outlier.action = classifyOutlierAction(outlier.sample, analysis.delimiter)\n }\n\n const conforming = lines.filter(\n (_, i) => !outliers.value.some(o => o.index === i)\n )\n\n // Recompute minFieldCount from conforming samples only\n const conformingFieldCounts = conforming.map(s => s.split(analysis.delimiter).length)\n minFieldCount.value = conformingFieldCounts.length > 0\n ? Math.min(...conformingFieldCounts)\n : analysis.dominantFieldCount\n\n fields.value = extractColumns(conforming, analysis.delimiter, minFieldCount.value)\n\n // Reset field names; auto-disable constant columns (cardinality 1)\n fieldNames.value = {}\n enabledFields.value = new Set(\n fields.value.filter(f => f.cardinality > 1).map(f => f.index)\n )\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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"}
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\nconst QC_KEYWORDS = new Set([\n 'eqc', 'iqc', 'qc', 'blank', 'std', 'standard', 'test',\n])\n\nexport function classifyOutlierAction(\n sample: string,\n delimiter: string,\n): OutlierAction {\n const segments = sample.split(delimiter)\n return segments.some(seg => QC_KEYWORDS.has(seg.toLowerCase()))\n ? 'qc'\n : 'include'\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 _computedResult = computed(() => {\n if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {\n return { groups: [] as SampleGroup[], metadata: [] as MetadataRow[], excludedSamples: [] as string[] }\n }\n return computeGroups(\n samples.value,\n effectiveColumns.value,\n enabledFields.value,\n outlierActions.value,\n delimiter.value,\n minFieldCount.value,\n )\n })\n\n const groups = computed(() => _computedResult.value.groups)\n const metadata = computed(() => _computedResult.value.metadata)\n const excludedSamples = computed(() => _computedResult.value.excludedSamples)\n\n const result = computed<AutoGroupResult>(() => _computedResult.value)\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\n // Use dominantFieldCount as outlier threshold so QC/test samples with\n // fewer fields than the majority are correctly flagged\n outliers.value = detectOutliers(lines, analysis.delimiter, analysis.dominantFieldCount)\n\n // Apply smart default actions: auto-classify QC/test samples\n for (const outlier of outliers.value) {\n outlier.action = classifyOutlierAction(outlier.sample, analysis.delimiter)\n }\n\n const conforming = lines.filter(\n (_, i) => !outliers.value.some(o => o.index === i)\n )\n\n // Recompute minFieldCount from conforming samples only\n const conformingFieldCounts = conforming.map(s => s.split(analysis.delimiter).length)\n minFieldCount.value = conformingFieldCounts.length > 0\n ? Math.min(...conformingFieldCounts)\n : analysis.dominantFieldCount\n\n fields.value = extractColumns(conforming, analysis.delimiter, minFieldCount.value)\n\n // Reset field names; auto-disable constant columns (cardinality 1)\n fieldNames.value = {}\n enabledFields.value = new Set(\n fields.value.filter(f => f.cardinality > 1).map(f => f.index)\n )\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":[],"mappings":"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"}
@@ -197,7 +197,7 @@ function useForm(initialValues, rules = {}) {
197
197
  function reset(values) {
198
198
  const resetValues = values ? { ..._initialValues, ...values } : _initialValues;
199
199
  for (const key of Object.keys(data)) {
200
- data[key] = resetValues[key];
200
+ data[key] = structuredClone(resetValues[key]);
201
201
  errors[key] = null;
202
202
  touched[key] = false;
203
203
  dirty[key] = false;
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.js","sources":["../../src/composables/useForm.ts"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref } from 'vue'\n\n/**\n * Validation rule function type.\n * Returns error message string if invalid, undefined/null if valid.\n */\nexport type ValidationRule<T = unknown> = (value: T, formData: Record<string, unknown>) => string | undefined | null\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldRules<T = unknown> {\n required?: boolean | string\n minLength?: number | { value: number; message: string }\n maxLength?: number | { value: number; message: string }\n min?: number | { value: number; message: string }\n max?: number | { value: number; message: string }\n pattern?: RegExp | { value: RegExp; message: string }\n email?: boolean | string\n custom?: ValidationRule<T> | ValidationRule<T>[]\n}\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldState {\n value: unknown\n error: string | null\n touched: boolean\n dirty: boolean\n}\n\n/**\n * Form state and methods.\n */\nexport interface UseFormReturn<T extends Record<string, unknown>> {\n // Form data (reactive)\n data: T\n\n // Field errors\n errors: Record<string, string | null>\n\n // Field touched state\n touched: Record<string, boolean>\n\n // Field dirty state (value changed from initial)\n dirty: Record<string, boolean>\n\n // Overall form state\n isValid: Ref<boolean>\n isDirty: Ref<boolean>\n isSubmitting: Ref<boolean>\n\n // Methods\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void\n setFieldError: (field: string, error: string | null) => void\n setFieldTouched: (field: string, touched?: boolean) => void\n validateField: (field: string) => boolean\n validate: () => boolean\n reset: (values?: Partial<T>) => void\n handleSubmit: (onSubmit: (data: T) => Promise<void> | void) => (e?: Event) => Promise<void>\n getFieldProps: <K extends keyof T>(field: K) => {\n modelValue: T[K]\n 'onUpdate:modelValue': (value: T[K]) => void\n onBlur: () => void\n error: string | null\n }\n}\n\n// Built-in validators\nconst validators = {\n required: (value: unknown, message = 'This field is required'): string | null => {\n if (value === null || value === undefined || value === '') {\n return message\n }\n if (Array.isArray(value) && value.length === 0) {\n return message\n }\n return null\n },\n\n minLength: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length < min) {\n return message || `Must be at least ${min} characters`\n }\n return null\n },\n\n maxLength: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length > max) {\n return message || `Must be at most ${max} characters`\n }\n return null\n },\n\n min: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value < min) {\n return message || `Must be at least ${min}`\n }\n return null\n },\n\n max: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value > max) {\n return message || `Must be at most ${max}`\n }\n return null\n },\n\n pattern: (value: unknown, pattern: RegExp, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (!pattern.test(value)) {\n return message || 'Invalid format'\n }\n return null\n },\n\n email: (value: unknown, message = 'Invalid email address'): string | null => {\n if (typeof value !== 'string' || !value) return null\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n if (!emailRegex.test(value)) {\n return message\n }\n return null\n },\n}\n\n/**\n * Form state management composable with validation.\n *\n * @param initialValues - Initial form values\n * @param rules - Validation rules for each field\n *\n * @example\n * ```typescript\n * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(\n * { email: '', password: '' },\n * {\n * email: { required: true, email: true },\n * password: { required: true, minLength: 8 },\n * }\n * )\n *\n * // In template\n * <BaseInput v-bind=\"getFieldProps('email')\" label=\"Email\" />\n * <BaseInput v-bind=\"getFieldProps('password')\" type=\"password\" label=\"Password\" />\n * <BaseButton @click=\"handleSubmit(onSubmit)\" :disabled=\"!isValid\">Submit</BaseButton>\n * ```\n */\nexport function useForm<T extends Record<string, unknown>>(\n initialValues: T,\n rules: Partial<Record<keyof T, FieldRules>> = {}\n): UseFormReturn<T> {\n // Deep copy initial values so nested objects are not shared\n const _initialValues = structuredClone(initialValues)\n\n // Reactive form data\n const data = reactive(structuredClone(initialValues)) as T\n\n // Field state - use simple Record types for better TS compatibility\n const errors = reactive<Record<string, string | null>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = null\n return acc\n }, {} as Record<string, string | null>)\n )\n\n const touched = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const dirty = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const isSubmitting = ref(false)\n\n // Watch data changes to track dirty state\n watch(\n () => ({ ...data }),\n (newData) => {\n for (const key of Object.keys(newData)) {\n dirty[key] = newData[key as keyof T] !== _initialValues[key as keyof T]\n }\n },\n { deep: true }\n )\n\n // Validate a single field\n function validateField(field: string): boolean {\n const value = data[field as keyof T]\n const fieldRules = rules[field as keyof T]\n\n if (!fieldRules) {\n errors[field] = null\n return true\n }\n\n // Check required\n if (fieldRules.required) {\n const message = typeof fieldRules.required === 'string' ? fieldRules.required : undefined\n const error = validators.required(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Skip other validations if empty and not required\n if (value === null || value === undefined || value === '') {\n errors[field] = null\n return true\n }\n\n // Check minLength\n if (fieldRules.minLength !== undefined) {\n const config = typeof fieldRules.minLength === 'number'\n ? { value: fieldRules.minLength, message: undefined }\n : fieldRules.minLength\n const error = validators.minLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check maxLength\n if (fieldRules.maxLength !== undefined) {\n const config = typeof fieldRules.maxLength === 'number'\n ? { value: fieldRules.maxLength, message: undefined }\n : fieldRules.maxLength\n const error = validators.maxLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check min\n if (fieldRules.min !== undefined) {\n const config = typeof fieldRules.min === 'number'\n ? { value: fieldRules.min, message: undefined }\n : fieldRules.min\n const error = validators.min(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check max\n if (fieldRules.max !== undefined) {\n const config = typeof fieldRules.max === 'number'\n ? { value: fieldRules.max, message: undefined }\n : fieldRules.max\n const error = validators.max(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check pattern\n if (fieldRules.pattern !== undefined) {\n const config = fieldRules.pattern instanceof RegExp\n ? { value: fieldRules.pattern, message: undefined }\n : fieldRules.pattern\n const error = validators.pattern(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check email\n if (fieldRules.email) {\n const message = typeof fieldRules.email === 'string' ? fieldRules.email : undefined\n const error = validators.email(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check custom validators\n if (fieldRules.custom) {\n const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom]\n for (const rule of customRules) {\n const error = rule(value, data as Record<string, unknown>)\n if (error) {\n errors[field] = error\n return false\n }\n }\n }\n\n errors[field] = null\n return true\n }\n\n // Validate all fields\n function validate(): boolean {\n let isAllValid = true\n for (const field of Object.keys(data)) {\n if (!validateField(field)) {\n isAllValid = false\n }\n }\n return isAllValid\n }\n\n // Computed overall validity\n const isValid = computed(() => {\n return Object.values(errors).every(error => error === null)\n })\n\n // Computed overall dirty state\n const isDirty = computed(() => {\n return Object.values(dirty).some(d => d)\n })\n\n // Set field value\n function setFieldValue<K extends keyof T>(field: K, value: T[K]): void {\n ;(data as Record<string, unknown>)[field as string] = value\n if (touched[field as string]) {\n validateField(field as string)\n }\n }\n\n // Set field error\n function setFieldError(field: string, error: string | null): void {\n errors[field] = error\n }\n\n // Set field touched\n function setFieldTouched(field: string, isTouched = true): void {\n touched[field] = isTouched\n if (isTouched) {\n validateField(field)\n }\n }\n\n // Reset form\n function reset(values?: Partial<T>): void {\n const resetValues = values ? { ..._initialValues, ...values } : _initialValues\n for (const key of Object.keys(data)) {\n ;(data as Record<string, unknown>)[key] = resetValues[key as keyof T]\n errors[key] = null\n touched[key] = false\n dirty[key] = false\n }\n }\n\n // Handle submit\n function handleSubmit(onSubmit: (data: T) => Promise<void> | void) {\n return async (e?: Event): Promise<void> => {\n e?.preventDefault()\n\n // Mark all fields as touched\n for (const field of Object.keys(data)) {\n touched[field] = true\n }\n\n if (!validate()) {\n return\n }\n\n isSubmitting.value = true\n try {\n await onSubmit(data)\n } finally {\n isSubmitting.value = false\n }\n }\n }\n\n // Get props for a field (for v-bind)\n function getFieldProps<K extends keyof T>(field: K) {\n const fieldStr = field as string\n return {\n modelValue: data[field],\n 'onUpdate:modelValue': (value: T[K]) => setFieldValue(field, value),\n onBlur: () => setFieldTouched(fieldStr),\n error: touched[fieldStr] ? errors[fieldStr] : null,\n }\n }\n\n return {\n data,\n errors,\n touched,\n dirty,\n isValid,\n isDirty,\n isSubmitting,\n setFieldValue,\n setFieldError,\n setFieldTouched,\n validateField,\n validate,\n reset,\n handleSubmit,\n getFieldProps,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useForm.js","sources":["../../src/composables/useForm.ts"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref } from 'vue'\n\n/**\n * Validation rule function type.\n * Returns error message string if invalid, undefined/null if valid.\n */\nexport type ValidationRule<T = unknown> = (value: T, formData: Record<string, unknown>) => string | undefined | null\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldRules<T = unknown> {\n required?: boolean | string\n minLength?: number | { value: number; message: string }\n maxLength?: number | { value: number; message: string }\n min?: number | { value: number; message: string }\n max?: number | { value: number; message: string }\n pattern?: RegExp | { value: RegExp; message: string }\n email?: boolean | string\n custom?: ValidationRule<T> | ValidationRule<T>[]\n}\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldState {\n value: unknown\n error: string | null\n touched: boolean\n dirty: boolean\n}\n\n/**\n * Form state and methods.\n */\nexport interface UseFormReturn<T extends Record<string, unknown>> {\n // Form data (reactive)\n data: T\n\n // Field errors\n errors: Record<string, string | null>\n\n // Field touched state\n touched: Record<string, boolean>\n\n // Field dirty state (value changed from initial)\n dirty: Record<string, boolean>\n\n // Overall form state\n isValid: Ref<boolean>\n isDirty: Ref<boolean>\n isSubmitting: Ref<boolean>\n\n // Methods\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void\n setFieldError: (field: string, error: string | null) => void\n setFieldTouched: (field: string, touched?: boolean) => void\n validateField: (field: string) => boolean\n validate: () => boolean\n reset: (values?: Partial<T>) => void\n handleSubmit: (onSubmit: (data: T) => Promise<void> | void) => (e?: Event) => Promise<void>\n getFieldProps: <K extends keyof T>(field: K) => {\n modelValue: T[K]\n 'onUpdate:modelValue': (value: T[K]) => void\n onBlur: () => void\n error: string | null\n }\n}\n\n// Built-in validators\nconst validators = {\n required: (value: unknown, message = 'This field is required'): string | null => {\n if (value === null || value === undefined || value === '') {\n return message\n }\n if (Array.isArray(value) && value.length === 0) {\n return message\n }\n return null\n },\n\n minLength: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length < min) {\n return message || `Must be at least ${min} characters`\n }\n return null\n },\n\n maxLength: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length > max) {\n return message || `Must be at most ${max} characters`\n }\n return null\n },\n\n min: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value < min) {\n return message || `Must be at least ${min}`\n }\n return null\n },\n\n max: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value > max) {\n return message || `Must be at most ${max}`\n }\n return null\n },\n\n pattern: (value: unknown, pattern: RegExp, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (!pattern.test(value)) {\n return message || 'Invalid format'\n }\n return null\n },\n\n email: (value: unknown, message = 'Invalid email address'): string | null => {\n if (typeof value !== 'string' || !value) return null\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n if (!emailRegex.test(value)) {\n return message\n }\n return null\n },\n}\n\n/**\n * Form state management composable with validation.\n *\n * @param initialValues - Initial form values\n * @param rules - Validation rules for each field\n *\n * @example\n * ```typescript\n * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(\n * { email: '', password: '' },\n * {\n * email: { required: true, email: true },\n * password: { required: true, minLength: 8 },\n * }\n * )\n *\n * // In template\n * <BaseInput v-bind=\"getFieldProps('email')\" label=\"Email\" />\n * <BaseInput v-bind=\"getFieldProps('password')\" type=\"password\" label=\"Password\" />\n * <BaseButton @click=\"handleSubmit(onSubmit)\" :disabled=\"!isValid\">Submit</BaseButton>\n * ```\n */\nexport function useForm<T extends Record<string, unknown>>(\n initialValues: T,\n rules: Partial<Record<keyof T, FieldRules>> = {}\n): UseFormReturn<T> {\n // Deep copy initial values so nested objects are not shared\n const _initialValues = structuredClone(initialValues)\n\n // Reactive form data\n const data = reactive(structuredClone(initialValues)) as T\n\n // Field state - use simple Record types for better TS compatibility\n const errors = reactive<Record<string, string | null>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = null\n return acc\n }, {} as Record<string, string | null>)\n )\n\n const touched = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const dirty = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const isSubmitting = ref(false)\n\n // Watch data changes to track dirty state\n watch(\n () => ({ ...data }),\n (newData) => {\n for (const key of Object.keys(newData)) {\n dirty[key] = newData[key as keyof T] !== _initialValues[key as keyof T]\n }\n },\n { deep: true }\n )\n\n // Validate a single field\n function validateField(field: string): boolean {\n const value = data[field as keyof T]\n const fieldRules = rules[field as keyof T]\n\n if (!fieldRules) {\n errors[field] = null\n return true\n }\n\n // Check required\n if (fieldRules.required) {\n const message = typeof fieldRules.required === 'string' ? fieldRules.required : undefined\n const error = validators.required(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Skip other validations if empty and not required\n if (value === null || value === undefined || value === '') {\n errors[field] = null\n return true\n }\n\n // Check minLength\n if (fieldRules.minLength !== undefined) {\n const config = typeof fieldRules.minLength === 'number'\n ? { value: fieldRules.minLength, message: undefined }\n : fieldRules.minLength\n const error = validators.minLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check maxLength\n if (fieldRules.maxLength !== undefined) {\n const config = typeof fieldRules.maxLength === 'number'\n ? { value: fieldRules.maxLength, message: undefined }\n : fieldRules.maxLength\n const error = validators.maxLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check min\n if (fieldRules.min !== undefined) {\n const config = typeof fieldRules.min === 'number'\n ? { value: fieldRules.min, message: undefined }\n : fieldRules.min\n const error = validators.min(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check max\n if (fieldRules.max !== undefined) {\n const config = typeof fieldRules.max === 'number'\n ? { value: fieldRules.max, message: undefined }\n : fieldRules.max\n const error = validators.max(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check pattern\n if (fieldRules.pattern !== undefined) {\n const config = fieldRules.pattern instanceof RegExp\n ? { value: fieldRules.pattern, message: undefined }\n : fieldRules.pattern\n const error = validators.pattern(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check email\n if (fieldRules.email) {\n const message = typeof fieldRules.email === 'string' ? fieldRules.email : undefined\n const error = validators.email(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check custom validators\n if (fieldRules.custom) {\n const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom]\n for (const rule of customRules) {\n const error = rule(value, data as Record<string, unknown>)\n if (error) {\n errors[field] = error\n return false\n }\n }\n }\n\n errors[field] = null\n return true\n }\n\n // Validate all fields\n function validate(): boolean {\n let isAllValid = true\n for (const field of Object.keys(data)) {\n if (!validateField(field)) {\n isAllValid = false\n }\n }\n return isAllValid\n }\n\n // Computed overall validity\n const isValid = computed(() => {\n return Object.values(errors).every(error => error === null)\n })\n\n // Computed overall dirty state\n const isDirty = computed(() => {\n return Object.values(dirty).some(d => d)\n })\n\n // Set field value\n function setFieldValue<K extends keyof T>(field: K, value: T[K]): void {\n ;(data as Record<string, unknown>)[field as string] = value\n if (touched[field as string]) {\n validateField(field as string)\n }\n }\n\n // Set field error\n function setFieldError(field: string, error: string | null): void {\n errors[field] = error\n }\n\n // Set field touched\n function setFieldTouched(field: string, isTouched = true): void {\n touched[field] = isTouched\n if (isTouched) {\n validateField(field)\n }\n }\n\n // Reset form\n function reset(values?: Partial<T>): void {\n const resetValues = values ? { ..._initialValues, ...values } : _initialValues\n for (const key of Object.keys(data)) {\n ;(data as Record<string, unknown>)[key] = structuredClone(resetValues[key as keyof T])\n errors[key] = null\n touched[key] = false\n dirty[key] = false\n }\n }\n\n // Handle submit\n function handleSubmit(onSubmit: (data: T) => Promise<void> | void) {\n return async (e?: Event): Promise<void> => {\n e?.preventDefault()\n\n // Mark all fields as touched\n for (const field of Object.keys(data)) {\n touched[field] = true\n }\n\n if (!validate()) {\n return\n }\n\n isSubmitting.value = true\n try {\n await onSubmit(data)\n } finally {\n isSubmitting.value = false\n }\n }\n }\n\n // Get props for a field (for v-bind)\n function getFieldProps<K extends keyof T>(field: K) {\n const fieldStr = field as string\n return {\n modelValue: data[field],\n 'onUpdate:modelValue': (value: T[K]) => setFieldValue(field, value),\n onBlur: () => setFieldTouched(fieldStr),\n error: touched[fieldStr] ? errors[fieldStr] : null,\n }\n }\n\n return {\n data,\n errors,\n touched,\n dirty,\n isValid,\n isDirty,\n isSubmitting,\n setFieldValue,\n setFieldError,\n setFieldTouched,\n validateField,\n validate,\n reset,\n handleSubmit,\n getFieldProps,\n }\n}\n"],"names":[],"mappings":";AAsEA,MAAM,aAAa;AAAA,EACjB,UAAU,CAAC,OAAgB,UAAU,6BAA4C;AAC/E,QAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,IAAI;AACzD,aAAO;AAAA,IACT;AACA,QAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAAG;AAC9C,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,WAAW,CAAC,OAAgB,KAAa,YAAoC;AAC3E,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,MAAM,SAAS,KAAK;AACtB,aAAO,WAAW,oBAAoB,GAAG;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,WAAW,CAAC,OAAgB,KAAa,YAAoC;AAC3E,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,MAAM,SAAS,KAAK;AACtB,aAAO,WAAW,mBAAmB,GAAG;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,CAAC,OAAgB,KAAa,YAAoC;AACrE,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,QAAQ,KAAK;AACf,aAAO,WAAW,oBAAoB,GAAG;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,CAAC,OAAgB,KAAa,YAAoC;AACrE,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,QAAQ,KAAK;AACf,aAAO,WAAW,mBAAmB,GAAG;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,SAAS,CAAC,OAAgB,SAAiB,YAAoC;AAC7E,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,CAAC,QAAQ,KAAK,KAAK,GAAG;AACxB,aAAO,WAAW;AAAA,IACpB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,CAAC,OAAgB,UAAU,4BAA2C;AAC3E,QAAI,OAAO,UAAU,YAAY,CAAC,MAAO,QAAO;AAChD,UAAM,aAAa;AACnB,QAAI,CAAC,WAAW,KAAK,KAAK,GAAG;AAC3B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AACF;AAwBO,SAAS,QACd,eACA,QAA8C,IAC5B;AAElB,QAAM,iBAAiB,gBAAgB,aAAa;AAGpD,QAAM,OAAO,SAAS,gBAAgB,aAAa,CAAC;AAGpD,QAAM,SAAS;AAAA,IACb,OAAO,KAAK,aAAa,EAAE,OAAO,CAAC,KAAK,QAAQ;AAC9C,UAAI,GAAG,IAAI;AACX,aAAO;AAAA,IACT,GAAG,CAAA,CAAmC;AAAA,EAAA;AAGxC,QAAM,UAAU;AAAA,IACd,OAAO,KAAK,aAAa,EAAE,OAAO,CAAC,KAAK,QAAQ;AAC9C,UAAI,GAAG,IAAI;AACX,aAAO;AAAA,IACT,GAAG,CAAA,CAA6B;AAAA,EAAA;AAGlC,QAAM,QAAQ;AAAA,IACZ,OAAO,KAAK,aAAa,EAAE,OAAO,CAAC,KAAK,QAAQ;AAC9C,UAAI,GAAG,IAAI;AACX,aAAO;AAAA,IACT,GAAG,CAAA,CAA6B;AAAA,EAAA;AAGlC,QAAM,eAAe,IAAI,KAAK;AAG9B;AAAA,IACE,OAAO,EAAE,GAAG;IACZ,CAAC,YAAY;AACX,iBAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,cAAM,GAAG,IAAI,QAAQ,GAAc,MAAM,eAAe,GAAc;AAAA,MACxE;AAAA,IACF;AAAA,IACA,EAAE,MAAM,KAAA;AAAA,EAAK;AAIf,WAAS,cAAc,OAAwB;AAC7C,UAAM,QAAQ,KAAK,KAAgB;AACnC,UAAM,aAAa,MAAM,KAAgB;AAEzC,QAAI,CAAC,YAAY;AACf,aAAO,KAAK,IAAI;AAChB,aAAO;AAAA,IACT;AAGA,QAAI,WAAW,UAAU;AACvB,YAAM,UAAU,OAAO,WAAW,aAAa,WAAW,WAAW,WAAW;AAChF,YAAM,QAAQ,WAAW,SAAS,OAAO,OAAO;AAChD,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,IAAI;AACzD,aAAO,KAAK,IAAI;AAChB,aAAO;AAAA,IACT;AAGA,QAAI,WAAW,cAAc,QAAW;AACtC,YAAM,SAAS,OAAO,WAAW,cAAc,WAC3C,EAAE,OAAO,WAAW,WAAW,SAAS,OAAA,IACxC,WAAW;AACf,YAAM,QAAQ,WAAW,UAAU,OAAO,OAAO,OAAO,OAAO,OAAO;AACtE,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,WAAW,cAAc,QAAW;AACtC,YAAM,SAAS,OAAO,WAAW,cAAc,WAC3C,EAAE,OAAO,WAAW,WAAW,SAAS,OAAA,IACxC,WAAW;AACf,YAAM,QAAQ,WAAW,UAAU,OAAO,OAAO,OAAO,OAAO,OAAO;AACtE,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,WAAW,QAAQ,QAAW;AAChC,YAAM,SAAS,OAAO,WAAW,QAAQ,WACrC,EAAE,OAAO,WAAW,KAAK,SAAS,OAAA,IAClC,WAAW;AACf,YAAM,QAAQ,WAAW,IAAI,OAAO,OAAO,OAAO,OAAO,OAAO;AAChE,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,WAAW,QAAQ,QAAW;AAChC,YAAM,SAAS,OAAO,WAAW,QAAQ,WACrC,EAAE,OAAO,WAAW,KAAK,SAAS,OAAA,IAClC,WAAW;AACf,YAAM,QAAQ,WAAW,IAAI,OAAO,OAAO,OAAO,OAAO,OAAO;AAChE,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,WAAW,YAAY,QAAW;AACpC,YAAM,SAAS,WAAW,mBAAmB,SACzC,EAAE,OAAO,WAAW,SAAS,SAAS,OAAA,IACtC,WAAW;AACf,YAAM,QAAQ,WAAW,QAAQ,OAAO,OAAO,OAAO,OAAO,OAAO;AACpE,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,WAAW,OAAO;AACpB,YAAM,UAAU,OAAO,WAAW,UAAU,WAAW,WAAW,QAAQ;AAC1E,YAAM,QAAQ,WAAW,MAAM,OAAO,OAAO;AAC7C,UAAI,OAAO;AACT,eAAO,KAAK,IAAI;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,WAAW,QAAQ;AACrB,YAAM,cAAc,MAAM,QAAQ,WAAW,MAAM,IAAI,WAAW,SAAS,CAAC,WAAW,MAAM;AAC7F,iBAAW,QAAQ,aAAa;AAC9B,cAAM,QAAQ,KAAK,OAAO,IAA+B;AACzD,YAAI,OAAO;AACT,iBAAO,KAAK,IAAI;AAChB,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAEA,WAAO,KAAK,IAAI;AAChB,WAAO;AAAA,EACT;AAGA,WAAS,WAAoB;AAC3B,QAAI,aAAa;AACjB,eAAW,SAAS,OAAO,KAAK,IAAI,GAAG;AACrC,UAAI,CAAC,cAAc,KAAK,GAAG;AACzB,qBAAa;AAAA,MACf;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,SAAS,MAAM;AAC7B,WAAO,OAAO,OAAO,MAAM,EAAE,MAAM,CAAA,UAAS,UAAU,IAAI;AAAA,EAC5D,CAAC;AAGD,QAAM,UAAU,SAAS,MAAM;AAC7B,WAAO,OAAO,OAAO,KAAK,EAAE,KAAK,OAAK,CAAC;AAAA,EACzC,CAAC;AAGD,WAAS,cAAiC,OAAU,OAAmB;AACnE,SAAiC,KAAe,IAAI;AACtD,QAAI,QAAQ,KAAe,GAAG;AAC5B,oBAAc,KAAe;AAAA,IAC/B;AAAA,EACF;AAGA,WAAS,cAAc,OAAe,OAA4B;AAChE,WAAO,KAAK,IAAI;AAAA,EAClB;AAGA,WAAS,gBAAgB,OAAe,YAAY,MAAY;AAC9D,YAAQ,KAAK,IAAI;AACjB,QAAI,WAAW;AACb,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAGA,WAAS,MAAM,QAA2B;AACxC,UAAM,cAAc,SAAS,EAAE,GAAG,gBAAgB,GAAG,WAAW;AAChE,eAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACjC,WAAiC,GAAG,IAAI,gBAAgB,YAAY,GAAc,CAAC;AACrF,aAAO,GAAG,IAAI;AACd,cAAQ,GAAG,IAAI;AACf,YAAM,GAAG,IAAI;AAAA,IACf;AAAA,EACF;AAGA,WAAS,aAAa,UAA6C;AACjE,WAAO,OAAO,MAA6B;AACzC,6BAAG;AAGH,iBAAW,SAAS,OAAO,KAAK,IAAI,GAAG;AACrC,gBAAQ,KAAK,IAAI;AAAA,MACnB;AAEA,UAAI,CAAC,YAAY;AACf;AAAA,MACF;AAEA,mBAAa,QAAQ;AACrB,UAAI;AACF,cAAM,SAAS,IAAI;AAAA,MACrB,UAAA;AACE,qBAAa,QAAQ;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAGA,WAAS,cAAiC,OAAU;AAClD,UAAM,WAAW;AACjB,WAAO;AAAA,MACL,YAAY,KAAK,KAAK;AAAA,MACtB,uBAAuB,CAAC,UAAgB,cAAc,OAAO,KAAK;AAAA,MAClE,QAAQ,MAAM,gBAAgB,QAAQ;AAAA,MACtC,OAAO,QAAQ,QAAQ,IAAI,OAAO,QAAQ,IAAI;AAAA,IAAA;AAAA,EAElD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -54,7 +54,14 @@ function usePlatformContext(options = {}) {
54
54
  }
55
55
  platformContext.value = {
56
56
  isIntegrated: hasPluginParam,
57
- theme: localStorage.getItem("mld-theme") || "system",
57
+ theme: (() => {
58
+ try {
59
+ const s = localStorage.getItem("mld-settings");
60
+ if (s) return JSON.parse(s).theme || "system";
61
+ } catch {
62
+ }
63
+ return "system";
64
+ })(),
58
65
  platformOrigin: platformOrigin || void 0
59
66
  };
60
67
  }
@@ -1 +1 @@
1
- {"version":3,"file":"usePlatformContext.js","sources":["../../src/composables/usePlatformContext.ts"],"sourcesContent":["import { ref, computed, onMounted, onUnmounted } from 'vue'\nimport type { PlatformContext, PlatformContextOptions, PlatformEvent } from '../types'\n\nconst platformContext = ref<PlatformContext>({\n isIntegrated: false,\n theme: 'system',\n})\n\n// Track allowed origins for postMessage security\nlet allowedOrigins: Set<string> = new Set()\nlet allowAnyOrigin = false\nlet initialized = false\nlet listenerCount = 0\nlet currentHandler: ((event: MessageEvent) => void) | null = null\n\n/**\n * Derive origin from URL (protocol + host)\n */\nfunction getOriginFromUrl(url: string): string | null {\n try {\n const parsed = new URL(url)\n return parsed.origin\n } catch {\n return null\n }\n}\n\n/**\n * Check if an origin is allowed for postMessage communication\n */\nfunction isOriginAllowed(origin: string): boolean {\n // Development mode: allow any origin (must be explicitly enabled)\n if (allowAnyOrigin) {\n console.warn('[MLD SDK] postMessage origin validation disabled - only use in development')\n return true\n }\n\n // Same origin is always allowed\n if (origin === window.location.origin) {\n return true\n }\n\n // Check against allowed origins list\n return allowedOrigins.has(origin)\n}\n\n/**\n * Platform context composable for plugin integration with MLD Platform.\n *\n * Provides secure communication with the parent platform via postMessage.\n *\n * @param options - Configuration options\n * @param options.allowedOrigins - List of allowed origins for postMessage\n * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)\n *\n * @example\n * ```typescript\n * // Basic usage - derives origin from platform injection\n * const { isIntegrated, user, theme } = usePlatformContext()\n *\n * // With explicit allowed origins\n * const { isIntegrated } = usePlatformContext({\n * allowedOrigins: ['https://mld.example.com']\n * })\n *\n * // Development mode (UNSAFE)\n * const { isIntegrated } = usePlatformContext({\n * allowAnyOrigin: import.meta.env.DEV\n * })\n * ```\n */\nexport function usePlatformContext(options: PlatformContextOptions = {}) {\n function detectPlatform(): void {\n // Check if running under MLD Platform by looking for platform-injected global\n const platformData = (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_PLATFORM__\n\n if (platformData) {\n platformContext.value = {\n ...platformData,\n isIntegrated: true,\n }\n\n // Derive platform origin from injected data\n if (platformData.platformOrigin) {\n allowedOrigins.add(platformData.platformOrigin)\n } else if (platformData.platformApiUrl) {\n const origin = getOriginFromUrl(platformData.platformApiUrl)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n } else {\n // Check for platform indicator in URL or localStorage\n const urlParams = new URLSearchParams(window.location.search)\n const hasPluginParam = urlParams.has('mld-plugin')\n\n // Try to get platform origin from URL parameter\n const platformOrigin = urlParams.get('mld-origin')\n if (platformOrigin) {\n const origin = getOriginFromUrl(platformOrigin)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n\n platformContext.value = {\n isIntegrated: hasPluginParam,\n theme: (localStorage.getItem('mld-theme') as 'light' | 'dark' | 'system') || 'system',\n platformOrigin: platformOrigin || undefined,\n }\n }\n\n // Add user-provided allowed origins\n if (options.allowedOrigins) {\n for (const origin of options.allowedOrigins) {\n const normalized = getOriginFromUrl(origin) || origin\n allowedOrigins.add(normalized)\n }\n }\n\n // Set development mode flag\n if (options.allowAnyOrigin) {\n allowAnyOrigin = true\n }\n }\n\n function handlePlatformMessage(event: MessageEvent): void {\n // Only accept messages from parent window (platform)\n if (event.source !== window.parent) return\n\n // Validate origin for security\n if (!isOriginAllowed(event.origin)) {\n console.warn(`[MLD SDK] Rejected postMessage from untrusted origin: ${event.origin}`)\n return\n }\n\n try {\n const platformEvent = event.data as PlatformEvent\n if (!platformEvent.type?.startsWith('mld:')) return\n\n switch (platformEvent.type) {\n case 'mld:theme-changed':\n platformContext.value.theme = platformEvent.payload as 'light' | 'dark' | 'system'\n break\n case 'mld:user-changed':\n platformContext.value.user = platformEvent.payload as PlatformContext['user']\n break\n }\n } catch {\n // Ignore invalid messages\n }\n }\n\n /**\n * Send a message to the parent platform.\n * Uses validated target origin for security.\n */\n function sendToPlatform(event: PlatformEvent): void {\n if (!platformContext.value.isIntegrated || window.parent === window) {\n return\n }\n\n // Determine target origin\n let targetOrigin: string\n\n if (platformContext.value.platformOrigin) {\n // Use explicitly configured platform origin\n targetOrigin = platformContext.value.platformOrigin\n } else if (allowedOrigins.size > 0) {\n // Use first allowed origin (typically the platform)\n targetOrigin = allowedOrigins.values().next().value as string\n } else if (allowAnyOrigin) {\n // Development mode fallback\n targetOrigin = '*'\n console.warn('[MLD SDK] Using wildcard origin for postMessage - only use in development')\n } else {\n // Safety: if no origin is configured, log warning and don't send\n console.warn('[MLD SDK] Cannot send postMessage: no platform origin configured')\n return\n }\n\n window.parent.postMessage(event, targetOrigin)\n }\n\n /**\n * Request navigation to a path in the platform.\n */\n function navigate(path: string): void {\n sendToPlatform({\n type: 'mld:navigate',\n payload: path,\n })\n }\n\n /**\n * Show a notification in the platform.\n */\n function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {\n sendToPlatform({\n type: 'mld:notification',\n payload: { message, type },\n })\n }\n\n onMounted(() => {\n if (!initialized) {\n detectPlatform()\n currentHandler = handlePlatformMessage\n window.addEventListener('message', handlePlatformMessage)\n initialized = true\n }\n listenerCount++\n })\n\n onUnmounted(() => {\n listenerCount--\n if (listenerCount <= 0 && currentHandler) {\n window.removeEventListener('message', currentHandler)\n currentHandler = null\n initialized = false\n listenerCount = 0\n }\n })\n\n const isIntegrated = computed(() => platformContext.value.isIntegrated)\n const plugin = computed(() => platformContext.value.plugin)\n const user = computed(() => platformContext.value.user)\n const theme = computed(() => platformContext.value.theme)\n const features = computed(() => platformContext.value.features)\n\n return {\n context: platformContext,\n isIntegrated,\n plugin,\n user,\n theme,\n features,\n navigate,\n notify,\n sendToPlatform,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usePlatformContext.js","sources":["../../src/composables/usePlatformContext.ts"],"sourcesContent":["import { ref, computed, onMounted, onUnmounted } from 'vue'\nimport type { PlatformContext, PlatformContextOptions, PlatformEvent } from '../types'\n\nconst platformContext = ref<PlatformContext>({\n isIntegrated: false,\n theme: 'system',\n})\n\n// Track allowed origins for postMessage security\nlet allowedOrigins: Set<string> = new Set()\nlet allowAnyOrigin = false\nlet initialized = false\nlet listenerCount = 0\nlet currentHandler: ((event: MessageEvent) => void) | null = null\n\n/**\n * Derive origin from URL (protocol + host)\n */\nfunction getOriginFromUrl(url: string): string | null {\n try {\n const parsed = new URL(url)\n return parsed.origin\n } catch {\n return null\n }\n}\n\n/**\n * Check if an origin is allowed for postMessage communication\n */\nfunction isOriginAllowed(origin: string): boolean {\n // Development mode: allow any origin (must be explicitly enabled)\n if (allowAnyOrigin) {\n console.warn('[MLD SDK] postMessage origin validation disabled - only use in development')\n return true\n }\n\n // Same origin is always allowed\n if (origin === window.location.origin) {\n return true\n }\n\n // Check against allowed origins list\n return allowedOrigins.has(origin)\n}\n\n/**\n * Platform context composable for plugin integration with MLD Platform.\n *\n * Provides secure communication with the parent platform via postMessage.\n *\n * @param options - Configuration options\n * @param options.allowedOrigins - List of allowed origins for postMessage\n * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)\n *\n * @example\n * ```typescript\n * // Basic usage - derives origin from platform injection\n * const { isIntegrated, user, theme } = usePlatformContext()\n *\n * // With explicit allowed origins\n * const { isIntegrated } = usePlatformContext({\n * allowedOrigins: ['https://mld.example.com']\n * })\n *\n * // Development mode (UNSAFE)\n * const { isIntegrated } = usePlatformContext({\n * allowAnyOrigin: import.meta.env.DEV\n * })\n * ```\n */\nexport function usePlatformContext(options: PlatformContextOptions = {}) {\n function detectPlatform(): void {\n // Check if running under MLD Platform by looking for platform-injected global\n const platformData = (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_PLATFORM__\n\n if (platformData) {\n platformContext.value = {\n ...platformData,\n isIntegrated: true,\n }\n\n // Derive platform origin from injected data\n if (platformData.platformOrigin) {\n allowedOrigins.add(platformData.platformOrigin)\n } else if (platformData.platformApiUrl) {\n const origin = getOriginFromUrl(platformData.platformApiUrl)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n } else {\n // Check for platform indicator in URL or localStorage\n const urlParams = new URLSearchParams(window.location.search)\n const hasPluginParam = urlParams.has('mld-plugin')\n\n // Try to get platform origin from URL parameter\n const platformOrigin = urlParams.get('mld-origin')\n if (platformOrigin) {\n const origin = getOriginFromUrl(platformOrigin)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n\n platformContext.value = {\n isIntegrated: hasPluginParam,\n theme: (() => {\n try {\n const s = localStorage.getItem('mld-settings')\n if (s) return (JSON.parse(s).theme as 'light' | 'dark' | 'system') || 'system'\n } catch { /* ignore */ }\n return 'system'\n })(),\n platformOrigin: platformOrigin || undefined,\n }\n }\n\n // Add user-provided allowed origins\n if (options.allowedOrigins) {\n for (const origin of options.allowedOrigins) {\n const normalized = getOriginFromUrl(origin) || origin\n allowedOrigins.add(normalized)\n }\n }\n\n // Set development mode flag\n if (options.allowAnyOrigin) {\n allowAnyOrigin = true\n }\n }\n\n function handlePlatformMessage(event: MessageEvent): void {\n // Only accept messages from parent window (platform)\n if (event.source !== window.parent) return\n\n // Validate origin for security\n if (!isOriginAllowed(event.origin)) {\n console.warn(`[MLD SDK] Rejected postMessage from untrusted origin: ${event.origin}`)\n return\n }\n\n try {\n const platformEvent = event.data as PlatformEvent\n if (!platformEvent.type?.startsWith('mld:')) return\n\n switch (platformEvent.type) {\n case 'mld:theme-changed':\n platformContext.value.theme = platformEvent.payload as 'light' | 'dark' | 'system'\n break\n case 'mld:user-changed':\n platformContext.value.user = platformEvent.payload as PlatformContext['user']\n break\n }\n } catch {\n // Ignore invalid messages\n }\n }\n\n /**\n * Send a message to the parent platform.\n * Uses validated target origin for security.\n */\n function sendToPlatform(event: PlatformEvent): void {\n if (!platformContext.value.isIntegrated || window.parent === window) {\n return\n }\n\n // Determine target origin\n let targetOrigin: string\n\n if (platformContext.value.platformOrigin) {\n // Use explicitly configured platform origin\n targetOrigin = platformContext.value.platformOrigin\n } else if (allowedOrigins.size > 0) {\n // Use first allowed origin (typically the platform)\n targetOrigin = allowedOrigins.values().next().value as string\n } else if (allowAnyOrigin) {\n // Development mode fallback\n targetOrigin = '*'\n console.warn('[MLD SDK] Using wildcard origin for postMessage - only use in development')\n } else {\n // Safety: if no origin is configured, log warning and don't send\n console.warn('[MLD SDK] Cannot send postMessage: no platform origin configured')\n return\n }\n\n window.parent.postMessage(event, targetOrigin)\n }\n\n /**\n * Request navigation to a path in the platform.\n */\n function navigate(path: string): void {\n sendToPlatform({\n type: 'mld:navigate',\n payload: path,\n })\n }\n\n /**\n * Show a notification in the platform.\n */\n function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {\n sendToPlatform({\n type: 'mld:notification',\n payload: { message, type },\n })\n }\n\n onMounted(() => {\n if (!initialized) {\n detectPlatform()\n currentHandler = handlePlatformMessage\n window.addEventListener('message', handlePlatformMessage)\n initialized = true\n }\n listenerCount++\n })\n\n onUnmounted(() => {\n listenerCount--\n if (listenerCount <= 0 && currentHandler) {\n window.removeEventListener('message', currentHandler)\n currentHandler = null\n initialized = false\n listenerCount = 0\n }\n })\n\n const isIntegrated = computed(() => platformContext.value.isIntegrated)\n const plugin = computed(() => platformContext.value.plugin)\n const user = computed(() => platformContext.value.user)\n const theme = computed(() => platformContext.value.theme)\n const features = computed(() => platformContext.value.features)\n\n return {\n context: platformContext,\n isIntegrated,\n plugin,\n user,\n theme,\n features,\n navigate,\n notify,\n sendToPlatform,\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,33 +1,31 @@
1
- import { watch, onMounted, ref } from "vue";
2
- const isDark = ref(false);
3
- let initialized = false;
1
+ import { computed, getCurrentInstance, onUnmounted } from "vue";
2
+ import { useSettingsStore } from "../stores/settings.js";
4
3
  function useTheme() {
4
+ const settings = useSettingsStore();
5
+ const mql = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
6
+ const isDark = computed(() => {
7
+ if (settings.theme === "system") {
8
+ return (mql == null ? void 0 : mql.matches) ?? false;
9
+ }
10
+ return settings.theme === "dark";
11
+ });
12
+ function onSystemChange() {
13
+ if (settings.theme === "system") {
14
+ settings.theme = "system";
15
+ }
16
+ }
17
+ mql == null ? void 0 : mql.addEventListener("change", onSystemChange);
18
+ if (getCurrentInstance()) {
19
+ onUnmounted(() => {
20
+ mql == null ? void 0 : mql.removeEventListener("change", onSystemChange);
21
+ });
22
+ }
5
23
  function toggleTheme() {
6
- isDark.value = !isDark.value;
24
+ settings.theme = isDark.value ? "light" : "dark";
7
25
  }
8
26
  function setTheme(theme) {
9
- isDark.value = theme === "dark";
27
+ settings.theme = theme;
10
28
  }
11
- watch(isDark, (dark) => {
12
- if (dark) {
13
- document.documentElement.classList.add("dark");
14
- } else {
15
- document.documentElement.classList.remove("dark");
16
- }
17
- localStorage.setItem("mld-theme", dark ? "dark" : "light");
18
- });
19
- onMounted(() => {
20
- if (initialized) return;
21
- initialized = true;
22
- const savedTheme = localStorage.getItem("mld-theme");
23
- if (savedTheme === "dark") {
24
- isDark.value = true;
25
- } else if (savedTheme === "light") {
26
- isDark.value = false;
27
- } else {
28
- isDark.value = window.matchMedia("(prefers-color-scheme: dark)").matches;
29
- }
30
- });
31
29
  return {
32
30
  isDark,
33
31
  toggleTheme,
@@ -1 +1 @@
1
- {"version":3,"file":"useTheme.js","sources":["../../src/composables/useTheme.ts"],"sourcesContent":["import { ref, watch, onMounted, type Ref } from 'vue'\n\nconst isDark = ref(false)\nlet initialized = false\n\nexport interface UseThemeReturn {\n isDark: Ref<boolean>\n toggleTheme: () => void\n setTheme: (theme: 'light' | 'dark') => void\n}\n\nexport function useTheme(): UseThemeReturn {\n function toggleTheme() {\n isDark.value = !isDark.value\n }\n\n function setTheme(theme: 'light' | 'dark') {\n isDark.value = theme === 'dark'\n }\n\n watch(isDark, (dark) => {\n if (dark) {\n document.documentElement.classList.add('dark')\n } else {\n document.documentElement.classList.remove('dark')\n }\n localStorage.setItem('mld-theme', dark ? 'dark' : 'light')\n })\n\n onMounted(() => {\n if (initialized) return\n initialized = true\n\n const savedTheme = localStorage.getItem('mld-theme')\n if (savedTheme === 'dark') {\n isDark.value = true\n } else if (savedTheme === 'light') {\n isDark.value = false\n } else {\n isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches\n }\n })\n\n return {\n isDark,\n toggleTheme,\n setTheme,\n }\n}\n"],"names":[],"mappings":";AAEA,MAAM,SAAS,IAAI,KAAK;AACxB,IAAI,cAAc;AAQX,SAAS,WAA2B;AACzC,WAAS,cAAc;AACrB,WAAO,QAAQ,CAAC,OAAO;AAAA,EACzB;AAEA,WAAS,SAAS,OAAyB;AACzC,WAAO,QAAQ,UAAU;AAAA,EAC3B;AAEA,QAAM,QAAQ,CAAC,SAAS;AACtB,QAAI,MAAM;AACR,eAAS,gBAAgB,UAAU,IAAI,MAAM;AAAA,IAC/C,OAAO;AACL,eAAS,gBAAgB,UAAU,OAAO,MAAM;AAAA,IAClD;AACA,iBAAa,QAAQ,aAAa,OAAO,SAAS,OAAO;AAAA,EAC3D,CAAC;AAED,YAAU,MAAM;AACd,QAAI,YAAa;AACjB,kBAAc;AAEd,UAAM,aAAa,aAAa,QAAQ,WAAW;AACnD,QAAI,eAAe,QAAQ;AACzB,aAAO,QAAQ;AAAA,IACjB,WAAW,eAAe,SAAS;AACjC,aAAO,QAAQ;AAAA,IACjB,OAAO;AACL,aAAO,QAAQ,OAAO,WAAW,8BAA8B,EAAE;AAAA,IACnE;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"useTheme.js","sources":["../../src/composables/useTheme.ts"],"sourcesContent":["import { computed, onUnmounted, getCurrentInstance, type Ref } from 'vue'\nimport { useSettingsStore } from '../stores/settings'\n\nexport interface UseThemeReturn {\n isDark: Ref<boolean>\n toggleTheme: () => void\n setTheme: (theme: 'light' | 'dark') => void\n}\n\nexport function useTheme(): UseThemeReturn {\n const settings = useSettingsStore()\n\n const mql = typeof window !== 'undefined'\n ? window.matchMedia('(prefers-color-scheme: dark)')\n : null\n\n const isDark = computed(() => {\n if (settings.theme === 'system') {\n return mql?.matches ?? false\n }\n return settings.theme === 'dark'\n })\n\n // React to system preference changes when in 'system' mode\n function onSystemChange() {\n if (settings.theme === 'system') {\n // Force reactivity by toggling theme to itself — the computed re-evaluates\n // because mql.matches changed, but Vue needs a trigger. We nudge the store.\n settings.theme = 'system'\n }\n }\n\n mql?.addEventListener('change', onSystemChange)\n\n if (getCurrentInstance()) {\n onUnmounted(() => {\n mql?.removeEventListener('change', onSystemChange)\n })\n }\n\n function toggleTheme() {\n settings.theme = isDark.value ? 'light' : 'dark'\n }\n\n function setTheme(theme: 'light' | 'dark') {\n settings.theme = theme\n }\n\n return {\n isDark,\n toggleTheme,\n setTheme,\n }\n}\n"],"names":[],"mappings":";;AASO,SAAS,WAA2B;AACzC,QAAM,WAAW,iBAAA;AAEjB,QAAM,MAAM,OAAO,WAAW,cAC1B,OAAO,WAAW,8BAA8B,IAChD;AAEJ,QAAM,SAAS,SAAS,MAAM;AAC5B,QAAI,SAAS,UAAU,UAAU;AAC/B,cAAO,2BAAK,YAAW;AAAA,IACzB;AACA,WAAO,SAAS,UAAU;AAAA,EAC5B,CAAC;AAGD,WAAS,iBAAiB;AACxB,QAAI,SAAS,UAAU,UAAU;AAG/B,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AAEA,6BAAK,iBAAiB,UAAU;AAEhC,MAAI,sBAAsB;AACxB,gBAAY,MAAM;AAChB,iCAAK,oBAAoB,UAAU;AAAA,IACrC,CAAC;AAAA,EACH;AAEA,WAAS,cAAc;AACrB,aAAS,QAAQ,OAAO,QAAQ,UAAU;AAAA,EAC5C;AAEA,WAAS,SAAS,OAAyB;AACzC,aAAS,QAAQ;AAAA,EACnB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -26,6 +26,7 @@ export interface UseWellPlateEditorReturn {
26
26
  redo: () => void;
27
27
  exportData: (format: 'json' | 'csv') => string;
28
28
  importData: (data: string, format: 'json' | 'csv') => boolean;
29
+ loadState: (state: Partial<PlateMapEditorState>) => void;
29
30
  reset: () => void;
30
31
  }
31
32
  export declare function useWellPlateEditor(initialState?: Partial<PlateMapEditorState>, options?: UseWellPlateEditorOptions): UseWellPlateEditorReturn;
@@ -22,7 +22,7 @@ const DEFAULT_PALETTE = [
22
22
  const MAX_HISTORY = 50;
23
23
  function createEmptyPlate(name, format, id) {
24
24
  return {
25
- id: `plate-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
25
+ id: id || `plate-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
26
26
  name,
27
27
  format,
28
28
  wells: {}
@@ -53,13 +53,10 @@ function useWellPlateEditor(initialState, options = {}) {
53
53
  const activeSampleId = computed(() => internalState.value.activeSampleId);
54
54
  const canUndo = computed(() => historyIndex.value >= 0);
55
55
  const canRedo = computed(() => historyIndex.value < history.value.length - 1);
56
- function deepClone(obj) {
57
- return structuredClone(obj);
58
- }
59
56
  function saveToHistory() {
60
57
  const entry = {
61
- plates: deepClone(internalState.value.plates),
62
- samples: deepClone(internalState.value.samples)
58
+ plates: structuredClone(internalState.value.plates),
59
+ samples: structuredClone(internalState.value.samples)
63
60
  };
64
61
  if (historyIndex.value < history.value.length - 1) {
65
62
  history.value = history.value.slice(0, historyIndex.value + 1);
@@ -179,8 +176,8 @@ function useWellPlateEditor(initialState, options = {}) {
179
176
  if (!canUndo.value) return;
180
177
  const entry = history.value[historyIndex.value];
181
178
  historyIndex.value--;
182
- internalState.value.plates = deepClone(entry.plates);
183
- internalState.value.samples = deepClone(entry.samples);
179
+ internalState.value.plates = structuredClone(entry.plates);
180
+ internalState.value.samples = structuredClone(entry.samples);
184
181
  const activePlateExists = internalState.value.plates.some((p) => p.id === internalState.value.activePlateId);
185
182
  if (!activePlateExists) {
186
183
  internalState.value.activePlateId = ((_a = internalState.value.plates[0]) == null ? void 0 : _a.id) || "";
@@ -191,8 +188,8 @@ function useWellPlateEditor(initialState, options = {}) {
191
188
  if (!canRedo.value) return;
192
189
  historyIndex.value++;
193
190
  const entry = history.value[historyIndex.value];
194
- internalState.value.plates = deepClone(entry.plates);
195
- internalState.value.samples = deepClone(entry.samples);
191
+ internalState.value.plates = structuredClone(entry.plates);
192
+ internalState.value.samples = structuredClone(entry.samples);
196
193
  internalState.value.selectedWells = [];
197
194
  }
198
195
  function exportData(format) {
@@ -272,6 +269,19 @@ function useWellPlateEditor(initialState, options = {}) {
272
269
  return false;
273
270
  }
274
271
  }
272
+ function loadState(state2) {
273
+ saveToHistory();
274
+ if (state2.plates && state2.plates.length > 0) {
275
+ internalState.value.plates = structuredClone(state2.plates);
276
+ internalState.value.activePlateId = state2.activePlateId ?? state2.plates[0].id;
277
+ }
278
+ if (state2.samples) {
279
+ internalState.value.samples = structuredClone(state2.samples);
280
+ }
281
+ internalState.value.selectedWells = state2.selectedWells ?? [];
282
+ internalState.value.activeSampleId = state2.activeSampleId;
283
+ updateSampleCounts();
284
+ }
275
285
  function reset() {
276
286
  const plate = createEmptyPlate("Plate 1", defaultFormat);
277
287
  internalState.value = {
@@ -306,6 +316,7 @@ function useWellPlateEditor(initialState, options = {}) {
306
316
  redo,
307
317
  exportData,
308
318
  importData,
319
+ loadState,
309
320
  reset
310
321
  };
311
322
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useWellPlateEditor.js","sources":["../../src/composables/useWellPlateEditor.ts"],"sourcesContent":["import { ref, computed, type ComputedRef } from 'vue'\nimport type {\n PlateMapEditorState,\n PlateMap,\n SampleType,\n WellPlateFormat,\n} from '../types'\n\nconst DEFAULT_PALETTE = [\n '#3B82F6', // blue\n '#10B981', // green\n '#EF4444', // red\n '#F59E0B', // amber\n '#8B5CF6', // purple\n '#F97316', // orange\n '#06B6D4', // cyan\n '#14B8A6', // teal\n '#6B7280', // gray\n]\n\nconst MAX_HISTORY = 50\n\ninterface HistoryEntry {\n plates: PlateMap[]\n samples: SampleType[]\n}\n\nexport interface UseWellPlateEditorOptions {\n maxHistory?: number\n defaultFormat?: WellPlateFormat\n}\n\nexport interface UseWellPlateEditorReturn {\n state: ComputedRef<PlateMapEditorState>\n plates: ComputedRef<PlateMap[]>\n activePlate: ComputedRef<PlateMap | undefined>\n samples: ComputedRef<SampleType[]>\n selectedWells: ComputedRef<string[]>\n activeSampleId: ComputedRef<string | undefined>\n canUndo: ComputedRef<boolean>\n canRedo: ComputedRef<boolean>\n setActivePlate: (plateId: string) => void\n setActiveSample: (sampleId: string | undefined) => void\n setSelectedWells: (wellIds: string[]) => void\n addPlate: (name?: string, format?: WellPlateFormat) => PlateMap\n removePlate: (plateId: string) => void\n addSample: (name: string, color?: string) => SampleType\n removeSample: (sampleId: string) => void\n assignSample: (wellIds: string[], sampleId: string | undefined) => void\n clearWells: (wellIds: string[]) => void\n undo: () => void\n redo: () => void\n exportData: (format: 'json' | 'csv') => string\n importData: (data: string, format: 'json' | 'csv') => boolean\n reset: () => void\n}\n\nfunction createEmptyPlate(\n name: string,\n format: WellPlateFormat,\n id?: string\n): PlateMap {\n return {\n id: id || `plate-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n name,\n format,\n wells: {},\n }\n}\n\nfunction generateSampleId(): string {\n return `sample-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\nexport function useWellPlateEditor(\n initialState?: Partial<PlateMapEditorState>,\n options: UseWellPlateEditorOptions = {}\n): UseWellPlateEditorReturn {\n const { maxHistory = MAX_HISTORY, defaultFormat = 96 } = options\n\n const defaultPlate = createEmptyPlate('Plate 1', defaultFormat)\n\n const internalState = ref<PlateMapEditorState>({\n plates: initialState?.plates || [defaultPlate],\n activePlateId: initialState?.activePlateId || defaultPlate.id,\n samples: initialState?.samples || [],\n selectedWells: initialState?.selectedWells || [],\n activeSampleId: initialState?.activeSampleId,\n })\n\n const history = ref<HistoryEntry[]>([])\n const historyIndex = ref(-1)\n\n const state = computed(() => internalState.value)\n const plates = computed(() => internalState.value.plates)\n const activePlate = computed(() =>\n internalState.value.plates.find(p => p.id === internalState.value.activePlateId)\n )\n const samples = computed(() => internalState.value.samples)\n const selectedWells = computed(() => internalState.value.selectedWells)\n const activeSampleId = computed(() => internalState.value.activeSampleId)\n\n const canUndo = computed(() => historyIndex.value >= 0)\n const canRedo = computed(() => historyIndex.value < history.value.length - 1)\n\n function deepClone<T>(obj: T): T {\n return structuredClone(obj)\n }\n\n function saveToHistory() {\n const entry: HistoryEntry = {\n plates: deepClone(internalState.value.plates),\n samples: deepClone(internalState.value.samples),\n }\n\n if (historyIndex.value < history.value.length - 1) {\n history.value = history.value.slice(0, historyIndex.value + 1)\n }\n\n history.value.push(entry)\n\n if (history.value.length > maxHistory) {\n history.value.shift()\n } else {\n historyIndex.value++\n }\n }\n\n function setActivePlate(plateId: string) {\n if (internalState.value.plates.some(p => p.id === plateId)) {\n internalState.value.activePlateId = plateId\n internalState.value.selectedWells = []\n }\n }\n\n function setActiveSample(sampleId: string | undefined) {\n internalState.value.activeSampleId = sampleId\n }\n\n function setSelectedWells(wellIds: string[]) {\n internalState.value.selectedWells = wellIds\n }\n\n function addPlate(name?: string, format?: WellPlateFormat): PlateMap {\n saveToHistory()\n const plateNumber = internalState.value.plates.length + 1\n const plate = createEmptyPlate(\n name || `Plate ${plateNumber}`,\n format || defaultFormat\n )\n internalState.value.plates.push(plate)\n internalState.value.activePlateId = plate.id\n internalState.value.selectedWells = []\n return plate\n }\n\n function removePlate(plateId: string) {\n if (internalState.value.plates.length <= 1) return\n\n saveToHistory()\n const index = internalState.value.plates.findIndex(p => p.id === plateId)\n if (index === -1) return\n\n internalState.value.plates.splice(index, 1)\n if (internalState.value.activePlateId === plateId) {\n internalState.value.activePlateId = internalState.value.plates[0].id\n internalState.value.selectedWells = []\n }\n }\n\n function addSample(name: string, color?: string): SampleType {\n saveToHistory()\n const sample: SampleType = {\n id: generateSampleId(),\n name,\n color: color || DEFAULT_PALETTE[internalState.value.samples.length % DEFAULT_PALETTE.length],\n count: 0,\n }\n internalState.value.samples.push(sample)\n return sample\n }\n\n function removeSample(sampleId: string) {\n saveToHistory()\n const index = internalState.value.samples.findIndex(s => s.id === sampleId)\n if (index === -1) return\n\n internalState.value.samples.splice(index, 1)\n\n for (const plate of internalState.value.plates) {\n for (const well of Object.values(plate.wells)) {\n if (well.sampleType === sampleId) {\n delete well.sampleType\n well.state = 'empty'\n }\n }\n }\n\n if (internalState.value.activeSampleId === sampleId) {\n internalState.value.activeSampleId = undefined\n }\n\n updateSampleCounts()\n }\n\n function assignSample(wellIds: string[], sampleId: string | undefined) {\n if (wellIds.length === 0) return\n\n saveToHistory()\n const plate = activePlate.value\n if (!plate) return\n\n for (const wellId of wellIds) {\n const well = plate.wells[wellId] || {\n id: wellId,\n row: wellId.charCodeAt(0) - 65,\n col: parseInt(wellId.slice(1)) - 1,\n state: 'empty',\n }\n\n if (sampleId) {\n well.sampleType = sampleId\n well.state = 'filled'\n } else {\n delete well.sampleType\n well.state = 'empty'\n }\n\n plate.wells[wellId] = well\n }\n updateSampleCounts()\n }\n\n function clearWells(wellIds: string[]) {\n assignSample(wellIds, undefined)\n }\n\n function updateSampleCounts() {\n const counts: Record<string, number> = {}\n\n for (const plate of internalState.value.plates) {\n for (const well of Object.values(plate.wells)) {\n if (well.sampleType) {\n counts[well.sampleType] = (counts[well.sampleType] || 0) + 1\n }\n }\n }\n\n for (const sample of internalState.value.samples) {\n sample.count = counts[sample.id] || 0\n }\n }\n\n function undo() {\n if (!canUndo.value) return\n\n const entry = history.value[historyIndex.value]\n historyIndex.value--\n\n internalState.value.plates = deepClone(entry.plates)\n internalState.value.samples = deepClone(entry.samples)\n\n const activePlateExists = internalState.value.plates.some(p => p.id === internalState.value.activePlateId)\n if (!activePlateExists) {\n internalState.value.activePlateId = internalState.value.plates[0]?.id || ''\n }\n internalState.value.selectedWells = []\n }\n\n function redo() {\n if (!canRedo.value) return\n\n historyIndex.value++\n const entry = history.value[historyIndex.value]\n\n internalState.value.plates = deepClone(entry.plates)\n internalState.value.samples = deepClone(entry.samples)\n internalState.value.selectedWells = []\n }\n\n function exportData(format: 'json' | 'csv'): string {\n if (format === 'json') {\n return JSON.stringify({\n plates: internalState.value.plates,\n samples: internalState.value.samples,\n }, null, 2)\n }\n\n const sampleMap = new Map(internalState.value.samples.map(s => [s.id, s.name]))\n const rows = ['Plate,Well,Sample Type,Sample Name']\n\n for (const plate of internalState.value.plates) {\n for (const [wellId, well] of Object.entries(plate.wells)) {\n if (well.sampleType) {\n const sampleName = sampleMap.get(well.sampleType) || ''\n rows.push(`\"${plate.name}\",\"${wellId}\",\"${well.sampleType}\",\"${sampleName}\"`)\n }\n }\n }\n\n return rows.join('\\n')\n }\n\n function importData(data: string, format: 'json' | 'csv'): boolean {\n try {\n saveToHistory()\n\n if (format === 'json') {\n const parsed = JSON.parse(data)\n if (parsed.plates && Array.isArray(parsed.plates)) {\n internalState.value.plates = parsed.plates\n internalState.value.activePlateId = parsed.plates[0]?.id || ''\n }\n if (parsed.samples && Array.isArray(parsed.samples)) {\n internalState.value.samples = parsed.samples\n }\n internalState.value.selectedWells = []\n updateSampleCounts()\n return true\n }\n\n const lines = data.trim().split('\\n')\n if (lines.length < 2) return false\n\n const plateMap = new Map<string, PlateMap>()\n const sampleMap = new Map<string, SampleType>()\n\n for (let i = 1; i < lines.length; i++) {\n const parts = lines[i].match(/(?:[^\",]+|\"[^\"]*\")+/g)\n if (!parts || parts.length < 4) continue\n\n const plateName = parts[0].replace(/^\"|\"$/g, '')\n const wellId = parts[1].replace(/^\"|\"$/g, '')\n const sampleId = parts[2].replace(/^\"|\"$/g, '')\n const sampleName = parts[3].replace(/^\"|\"$/g, '')\n\n if (!plateMap.has(plateName)) {\n plateMap.set(plateName, createEmptyPlate(plateName, defaultFormat))\n }\n\n if (sampleId && !sampleMap.has(sampleId)) {\n sampleMap.set(sampleId, {\n id: sampleId,\n name: sampleName || sampleId,\n color: DEFAULT_PALETTE[sampleMap.size % DEFAULT_PALETTE.length],\n count: 0,\n })\n }\n\n const plate = plateMap.get(plateName)!\n plate.wells[wellId] = {\n id: wellId,\n row: wellId.charCodeAt(0) - 65,\n col: parseInt(wellId.slice(1)) - 1,\n state: sampleId ? 'filled' : 'empty',\n sampleType: sampleId || undefined,\n }\n }\n\n internalState.value.plates = Array.from(plateMap.values())\n internalState.value.samples = Array.from(sampleMap.values())\n internalState.value.activePlateId = internalState.value.plates[0]?.id || ''\n internalState.value.selectedWells = []\n updateSampleCounts()\n return true\n } catch {\n return false\n }\n }\n\n function reset() {\n const plate = createEmptyPlate('Plate 1', defaultFormat)\n internalState.value = {\n plates: [plate],\n activePlateId: plate.id,\n samples: [],\n selectedWells: [],\n activeSampleId: undefined,\n }\n history.value = []\n historyIndex.value = -1\n }\n\n return {\n state,\n plates,\n activePlate,\n samples,\n selectedWells,\n activeSampleId,\n canUndo,\n canRedo,\n setActivePlate,\n setActiveSample,\n setSelectedWells,\n addPlate,\n removePlate,\n addSample,\n removeSample,\n assignSample,\n clearWells,\n undo,\n redo,\n exportData,\n importData,\n reset,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useWellPlateEditor.js","sources":["../../src/composables/useWellPlateEditor.ts"],"sourcesContent":["import { ref, computed, type ComputedRef } from 'vue'\nimport type {\n PlateMapEditorState,\n PlateMap,\n SampleType,\n WellPlateFormat,\n} from '../types'\n\nconst DEFAULT_PALETTE = [\n '#3B82F6', // blue\n '#10B981', // green\n '#EF4444', // red\n '#F59E0B', // amber\n '#8B5CF6', // purple\n '#F97316', // orange\n '#06B6D4', // cyan\n '#14B8A6', // teal\n '#6B7280', // gray\n]\n\nconst MAX_HISTORY = 50\n\ninterface HistoryEntry {\n plates: PlateMap[]\n samples: SampleType[]\n}\n\nexport interface UseWellPlateEditorOptions {\n maxHistory?: number\n defaultFormat?: WellPlateFormat\n}\n\nexport interface UseWellPlateEditorReturn {\n state: ComputedRef<PlateMapEditorState>\n plates: ComputedRef<PlateMap[]>\n activePlate: ComputedRef<PlateMap | undefined>\n samples: ComputedRef<SampleType[]>\n selectedWells: ComputedRef<string[]>\n activeSampleId: ComputedRef<string | undefined>\n canUndo: ComputedRef<boolean>\n canRedo: ComputedRef<boolean>\n setActivePlate: (plateId: string) => void\n setActiveSample: (sampleId: string | undefined) => void\n setSelectedWells: (wellIds: string[]) => void\n addPlate: (name?: string, format?: WellPlateFormat) => PlateMap\n removePlate: (plateId: string) => void\n addSample: (name: string, color?: string) => SampleType\n removeSample: (sampleId: string) => void\n assignSample: (wellIds: string[], sampleId: string | undefined) => void\n clearWells: (wellIds: string[]) => void\n undo: () => void\n redo: () => void\n exportData: (format: 'json' | 'csv') => string\n importData: (data: string, format: 'json' | 'csv') => boolean\n loadState: (state: Partial<PlateMapEditorState>) => void\n reset: () => void\n}\n\nfunction createEmptyPlate(\n name: string,\n format: WellPlateFormat,\n id?: string\n): PlateMap {\n return {\n id: id || `plate-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n name,\n format,\n wells: {},\n }\n}\n\nfunction generateSampleId(): string {\n return `sample-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\nexport function useWellPlateEditor(\n initialState?: Partial<PlateMapEditorState>,\n options: UseWellPlateEditorOptions = {}\n): UseWellPlateEditorReturn {\n const { maxHistory = MAX_HISTORY, defaultFormat = 96 } = options\n\n const defaultPlate = createEmptyPlate('Plate 1', defaultFormat)\n\n const internalState = ref<PlateMapEditorState>({\n plates: initialState?.plates || [defaultPlate],\n activePlateId: initialState?.activePlateId || defaultPlate.id,\n samples: initialState?.samples || [],\n selectedWells: initialState?.selectedWells || [],\n activeSampleId: initialState?.activeSampleId,\n })\n\n const history = ref<HistoryEntry[]>([])\n const historyIndex = ref(-1)\n\n const state = computed(() => internalState.value)\n const plates = computed(() => internalState.value.plates)\n const activePlate = computed(() =>\n internalState.value.plates.find(p => p.id === internalState.value.activePlateId)\n )\n const samples = computed(() => internalState.value.samples)\n const selectedWells = computed(() => internalState.value.selectedWells)\n const activeSampleId = computed(() => internalState.value.activeSampleId)\n\n const canUndo = computed(() => historyIndex.value >= 0)\n const canRedo = computed(() => historyIndex.value < history.value.length - 1)\n\n function saveToHistory() {\n const entry: HistoryEntry = {\n plates: structuredClone(internalState.value.plates),\n samples: structuredClone(internalState.value.samples),\n }\n\n if (historyIndex.value < history.value.length - 1) {\n history.value = history.value.slice(0, historyIndex.value + 1)\n }\n\n history.value.push(entry)\n\n if (history.value.length > maxHistory) {\n history.value.shift()\n } else {\n historyIndex.value++\n }\n }\n\n function setActivePlate(plateId: string) {\n if (internalState.value.plates.some(p => p.id === plateId)) {\n internalState.value.activePlateId = plateId\n internalState.value.selectedWells = []\n }\n }\n\n function setActiveSample(sampleId: string | undefined) {\n internalState.value.activeSampleId = sampleId\n }\n\n function setSelectedWells(wellIds: string[]) {\n internalState.value.selectedWells = wellIds\n }\n\n function addPlate(name?: string, format?: WellPlateFormat): PlateMap {\n saveToHistory()\n const plateNumber = internalState.value.plates.length + 1\n const plate = createEmptyPlate(\n name || `Plate ${plateNumber}`,\n format || defaultFormat\n )\n internalState.value.plates.push(plate)\n internalState.value.activePlateId = plate.id\n internalState.value.selectedWells = []\n return plate\n }\n\n function removePlate(plateId: string) {\n if (internalState.value.plates.length <= 1) return\n\n saveToHistory()\n const index = internalState.value.plates.findIndex(p => p.id === plateId)\n if (index === -1) return\n\n internalState.value.plates.splice(index, 1)\n if (internalState.value.activePlateId === plateId) {\n internalState.value.activePlateId = internalState.value.plates[0].id\n internalState.value.selectedWells = []\n }\n }\n\n function addSample(name: string, color?: string): SampleType {\n saveToHistory()\n const sample: SampleType = {\n id: generateSampleId(),\n name,\n color: color || DEFAULT_PALETTE[internalState.value.samples.length % DEFAULT_PALETTE.length],\n count: 0,\n }\n internalState.value.samples.push(sample)\n return sample\n }\n\n function removeSample(sampleId: string) {\n saveToHistory()\n const index = internalState.value.samples.findIndex(s => s.id === sampleId)\n if (index === -1) return\n\n internalState.value.samples.splice(index, 1)\n\n for (const plate of internalState.value.plates) {\n for (const well of Object.values(plate.wells)) {\n if (well.sampleType === sampleId) {\n delete well.sampleType\n well.state = 'empty'\n }\n }\n }\n\n if (internalState.value.activeSampleId === sampleId) {\n internalState.value.activeSampleId = undefined\n }\n\n updateSampleCounts()\n }\n\n function assignSample(wellIds: string[], sampleId: string | undefined) {\n if (wellIds.length === 0) return\n\n saveToHistory()\n const plate = activePlate.value\n if (!plate) return\n\n for (const wellId of wellIds) {\n const well = plate.wells[wellId] || {\n id: wellId,\n row: wellId.charCodeAt(0) - 65,\n col: parseInt(wellId.slice(1)) - 1,\n state: 'empty',\n }\n\n if (sampleId) {\n well.sampleType = sampleId\n well.state = 'filled'\n } else {\n delete well.sampleType\n well.state = 'empty'\n }\n\n plate.wells[wellId] = well\n }\n updateSampleCounts()\n }\n\n function clearWells(wellIds: string[]) {\n assignSample(wellIds, undefined)\n }\n\n function updateSampleCounts() {\n const counts: Record<string, number> = {}\n\n for (const plate of internalState.value.plates) {\n for (const well of Object.values(plate.wells)) {\n if (well.sampleType) {\n counts[well.sampleType] = (counts[well.sampleType] || 0) + 1\n }\n }\n }\n\n for (const sample of internalState.value.samples) {\n sample.count = counts[sample.id] || 0\n }\n }\n\n function undo() {\n if (!canUndo.value) return\n\n const entry = history.value[historyIndex.value]\n historyIndex.value--\n\n internalState.value.plates = structuredClone(entry.plates)\n internalState.value.samples = structuredClone(entry.samples)\n\n const activePlateExists = internalState.value.plates.some(p => p.id === internalState.value.activePlateId)\n if (!activePlateExists) {\n internalState.value.activePlateId = internalState.value.plates[0]?.id || ''\n }\n internalState.value.selectedWells = []\n }\n\n function redo() {\n if (!canRedo.value) return\n\n historyIndex.value++\n const entry = history.value[historyIndex.value]\n\n internalState.value.plates = structuredClone(entry.plates)\n internalState.value.samples = structuredClone(entry.samples)\n internalState.value.selectedWells = []\n }\n\n function exportData(format: 'json' | 'csv'): string {\n if (format === 'json') {\n return JSON.stringify({\n plates: internalState.value.plates,\n samples: internalState.value.samples,\n }, null, 2)\n }\n\n const sampleMap = new Map(internalState.value.samples.map(s => [s.id, s.name]))\n const rows = ['Plate,Well,Sample Type,Sample Name']\n\n for (const plate of internalState.value.plates) {\n for (const [wellId, well] of Object.entries(plate.wells)) {\n if (well.sampleType) {\n const sampleName = sampleMap.get(well.sampleType) || ''\n rows.push(`\"${plate.name}\",\"${wellId}\",\"${well.sampleType}\",\"${sampleName}\"`)\n }\n }\n }\n\n return rows.join('\\n')\n }\n\n function importData(data: string, format: 'json' | 'csv'): boolean {\n try {\n saveToHistory()\n\n if (format === 'json') {\n const parsed = JSON.parse(data)\n if (parsed.plates && Array.isArray(parsed.plates)) {\n internalState.value.plates = parsed.plates\n internalState.value.activePlateId = parsed.plates[0]?.id || ''\n }\n if (parsed.samples && Array.isArray(parsed.samples)) {\n internalState.value.samples = parsed.samples\n }\n internalState.value.selectedWells = []\n updateSampleCounts()\n return true\n }\n\n const lines = data.trim().split('\\n')\n if (lines.length < 2) return false\n\n const plateMap = new Map<string, PlateMap>()\n const sampleMap = new Map<string, SampleType>()\n\n for (let i = 1; i < lines.length; i++) {\n const parts = lines[i].match(/(?:[^\",]+|\"[^\"]*\")+/g)\n if (!parts || parts.length < 4) continue\n\n const plateName = parts[0].replace(/^\"|\"$/g, '')\n const wellId = parts[1].replace(/^\"|\"$/g, '')\n const sampleId = parts[2].replace(/^\"|\"$/g, '')\n const sampleName = parts[3].replace(/^\"|\"$/g, '')\n\n if (!plateMap.has(plateName)) {\n plateMap.set(plateName, createEmptyPlate(plateName, defaultFormat))\n }\n\n if (sampleId && !sampleMap.has(sampleId)) {\n sampleMap.set(sampleId, {\n id: sampleId,\n name: sampleName || sampleId,\n color: DEFAULT_PALETTE[sampleMap.size % DEFAULT_PALETTE.length],\n count: 0,\n })\n }\n\n const plate = plateMap.get(plateName)!\n plate.wells[wellId] = {\n id: wellId,\n row: wellId.charCodeAt(0) - 65,\n col: parseInt(wellId.slice(1)) - 1,\n state: sampleId ? 'filled' : 'empty',\n sampleType: sampleId || undefined,\n }\n }\n\n internalState.value.plates = Array.from(plateMap.values())\n internalState.value.samples = Array.from(sampleMap.values())\n internalState.value.activePlateId = internalState.value.plates[0]?.id || ''\n internalState.value.selectedWells = []\n updateSampleCounts()\n return true\n } catch {\n return false\n }\n }\n\n function loadState(state: Partial<PlateMapEditorState>) {\n saveToHistory()\n if (state.plates && state.plates.length > 0) {\n internalState.value.plates = structuredClone(state.plates)\n internalState.value.activePlateId = state.activePlateId ?? state.plates[0].id\n }\n if (state.samples) {\n internalState.value.samples = structuredClone(state.samples)\n }\n internalState.value.selectedWells = state.selectedWells ?? []\n internalState.value.activeSampleId = state.activeSampleId\n updateSampleCounts()\n }\n\n function reset() {\n const plate = createEmptyPlate('Plate 1', defaultFormat)\n internalState.value = {\n plates: [plate],\n activePlateId: plate.id,\n samples: [],\n selectedWells: [],\n activeSampleId: undefined,\n }\n history.value = []\n historyIndex.value = -1\n }\n\n return {\n state,\n plates,\n activePlate,\n samples,\n selectedWells,\n activeSampleId,\n canUndo,\n canRedo,\n setActivePlate,\n setActiveSample,\n setSelectedWells,\n addPlate,\n removePlate,\n addSample,\n removeSample,\n assignSample,\n clearWells,\n undo,\n redo,\n exportData,\n importData,\n loadState,\n reset,\n }\n}\n"],"names":["state"],"mappings":"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"}