@morscherlab/mld-sdk 0.9.6 → 0.9.8

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 (33) hide show
  1. package/dist/components/ExperimentPopover.vue.d.ts +4 -0
  2. package/dist/components/ExperimentPopover.vue.js +31 -17
  3. package/dist/components/ExperimentPopover.vue.js.map +1 -1
  4. package/dist/components/FormulaInput.vue.js +24 -18
  5. package/dist/components/FormulaInput.vue.js.map +1 -1
  6. package/dist/components/MoleculeInput.vue.js +15 -6
  7. package/dist/components/MoleculeInput.vue.js.map +1 -1
  8. package/dist/components/PlateMapEditor.vue.js +1 -1
  9. package/dist/components/PlateMapEditor.vue.js.map +1 -1
  10. package/dist/composables/useAuth.js +26 -25
  11. package/dist/composables/useAuth.js.map +1 -1
  12. package/dist/composables/useAutoGroup.js +7 -32
  13. package/dist/composables/useAutoGroup.js.map +1 -1
  14. package/dist/composables/useForm.js +1 -1
  15. package/dist/composables/useForm.js.map +1 -1
  16. package/dist/composables/useWellPlateEditor.d.ts +1 -0
  17. package/dist/composables/useWellPlateEditor.js +21 -10
  18. package/dist/composables/useWellPlateEditor.js.map +1 -1
  19. package/dist/stores/settings.js +17 -30
  20. package/dist/stores/settings.js.map +1 -1
  21. package/dist/styles.css +68 -14
  22. package/package.json +1 -1
  23. package/src/components/ExperimentPopover.vue +16 -3
  24. package/src/components/FormulaInput.vue +17 -16
  25. package/src/components/MoleculeInput.vue +29 -14
  26. package/src/components/PlateMapEditor.vue +1 -1
  27. package/src/composables/useAuth.ts +29 -31
  28. package/src/composables/useAutoGroup.ts +7 -33
  29. package/src/composables/useForm.ts +1 -1
  30. package/src/composables/useWellPlateEditor.ts +22 -10
  31. package/src/stores/settings.ts +22 -38
  32. package/src/styles/components/experiment-popover.css +25 -1
  33. package/src/styles/components/formula-input.css +13 -6
@@ -1 +1 @@
1
- {"version":3,"file":"PlateMapEditor.vue.js","sources":["../../src/components/PlateMapEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, watch, onMounted, onUnmounted } from 'vue'\nimport type { PlateMapEditorState, WellPlateFormat, SampleType, Well } from '../types'\nimport { useWellPlateEditor } from '../composables/useWellPlateEditor'\nimport WellPlate from './WellPlate.vue'\nimport SampleLegend from './SampleLegend.vue'\n\n// Slot colors matching MSExpDesigner\ntype SlotPosition = 'R' | 'G' | 'B' | 'Y'\nconst SLOT_COLORS: Record<SlotPosition, string> = {\n R: '#ef4444', // red\n G: '#22c55e', // green\n B: '#3b82f6', // blue\n Y: '#eab308', // yellow\n}\nconst SLOT_ORDER: SlotPosition[] = ['R', 'G', 'B', 'Y']\n\ninterface Props {\n modelValue?: PlateMapEditorState\n format?: WellPlateFormat\n maxPlates?: number\n samples?: SampleType[]\n showToolbar?: boolean\n showSidebar?: boolean\n allowAddPlates?: boolean\n allowAddSamples?: boolean\n size?: 'sm' | 'md' | 'lg' | 'xl' | 'fill'\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n modelValue: undefined,\n format: 96,\n maxPlates: 10,\n samples: () => [],\n showToolbar: true,\n showSidebar: true,\n allowAddPlates: true,\n allowAddSamples: true,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [state: PlateMapEditorState]\n 'plate-add': [plate: { id: string; name: string }]\n 'plate-remove': [plateId: string]\n 'sample-assign': [wellIds: string[], sampleId: string | undefined]\n 'wells-clear': [wellIds: string[]]\n 'undo': []\n 'redo': []\n 'export': [data: string, format: 'json' | 'csv']\n 'import': [success: boolean]\n}>()\n\nconst editor = useWellPlateEditor(props.modelValue, {\n defaultFormat: props.format,\n})\n\nconst newSampleName = ref('')\nconst showImportModal = ref(false)\nconst importText = ref('')\nconst importFormat = ref<'json' | 'csv'>('json')\n\n// Track slot assignment for each plate\nconst plateSlots = ref<Map<string, SlotPosition>>(new Map())\n\n// Assign slots to plates on creation\nfunction getPlateSlot(plateId: string, plateIndex: number): SlotPosition {\n if (!plateSlots.value.has(plateId)) {\n plateSlots.value.set(plateId, SLOT_ORDER[plateIndex % SLOT_ORDER.length])\n }\n return plateSlots.value.get(plateId)!\n}\n\nconst sampleColors = computed(() => {\n const colors: Record<string, string> = {}\n for (const sample of editor.samples.value) {\n if (sample.color) {\n colors[sample.id] = sample.color\n }\n }\n return colors\n})\n\nconst wellsData = computed(() => {\n const plate = editor.activePlate.value\n if (!plate) return {}\n\n const wells: Record<string, Partial<Well>> = {}\n for (const [wellId, well] of Object.entries(plate.wells)) {\n wells[wellId] = {\n state: well.state,\n sampleType: well.sampleType,\n value: well.value,\n }\n }\n return wells\n})\n\n// Count samples in a plate\nfunction getPlateWellCount(plateId: string): number {\n const plate = editor.plates.value.find(p => p.id === plateId)\n if (!plate) return 0\n return Object.values(plate.wells).filter(w => w.sampleType).length\n}\n\nwatch(\n () => editor.state.value,\n (newState) => emit('update:modelValue', { ...newState }),\n { deep: true }\n)\n\nwatch(\n () => props.modelValue,\n (newValue) => {\n if (newValue) editor.reset()\n }\n)\n\nfunction handleSelectionChange(wellIds: string[]) {\n editor.setSelectedWells(wellIds)\n}\n\nfunction handleSampleClick(sample: SampleType) {\n const newSampleId = editor.activeSampleId.value === sample.id ? undefined : sample.id\n editor.setActiveSample(newSampleId)\n}\n\nfunction handleAssignSample() {\n const wells = editor.selectedWells.value\n if (wells.length === 0) return\n\n editor.assignSample(wells, editor.activeSampleId.value)\n emit('sample-assign', wells, editor.activeSampleId.value)\n}\n\nfunction handleClearWells() {\n const wells = editor.selectedWells.value\n if (wells.length === 0) return\n\n editor.clearWells(wells)\n emit('wells-clear', wells)\n}\n\nfunction handleAddSample() {\n if (!newSampleName.value.trim()) return\n editor.addSample(newSampleName.value.trim())\n newSampleName.value = ''\n}\n\nfunction handleRemoveSample(sampleId: string) {\n editor.removeSample(sampleId)\n}\n\nfunction handleAddPlate() {\n if (editor.plates.value.length >= props.maxPlates) return\n const plate = editor.addPlate()\n emit('plate-add', { id: plate.id, name: plate.name })\n}\n\nfunction handleRemovePlate(plateId: string) {\n editor.removePlate(plateId)\n plateSlots.value.delete(plateId)\n emit('plate-remove', plateId)\n}\n\nfunction handleUndo() {\n editor.undo()\n emit('undo')\n}\n\nfunction handleRedo() {\n editor.redo()\n emit('redo')\n}\n\nfunction handleExport(format: 'json' | 'csv') {\n const data = editor.exportData(format)\n emit('export', data, format)\n\n const blob = new Blob([data], { type: format === 'json' ? 'application/json' : 'text/csv' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = `plate-map.${format}`\n a.click()\n URL.revokeObjectURL(url)\n}\n\nfunction handleImport() {\n const success = editor.importData(importText.value, importFormat.value)\n emit('import', success)\n if (success) {\n showImportModal.value = false\n importText.value = ''\n }\n}\n\nfunction handleKeyDown(event: KeyboardEvent) {\n const isUndo = (event.metaKey || event.ctrlKey) && event.key === 'z'\n if (isUndo) {\n event.preventDefault()\n event.shiftKey ? handleRedo() : handleUndo()\n return\n }\n\n const isDelete = event.key === 'Delete' || event.key === 'Backspace'\n if (isDelete && editor.selectedWells.value.length > 0) {\n event.preventDefault()\n handleClearWells()\n return\n }\n\n const num = parseInt(event.key)\n const isValidSampleKey = num >= 1 && num <= 9 && editor.samples.value.length >= num\n if (isValidSampleKey) {\n editor.setActiveSample(editor.samples.value[num - 1].id)\n if (editor.selectedWells.value.length > 0) {\n handleAssignSample()\n }\n }\n}\n\nonMounted(() => {\n document.addEventListener('keydown', handleKeyDown)\n})\n\nonUnmounted(() => {\n document.removeEventListener('keydown', handleKeyDown)\n})\n</script>\n\n<template>\n <div :class=\"['mld-plate-editor', { 'mld-plate-editor--with-sidebar': showSidebar }]\">\n <!-- Main plate area -->\n <div class=\"mld-plate-editor__main\">\n <!-- Toolbar -->\n <div v-if=\"showToolbar\" class=\"mld-plate-editor__toolbar\">\n <!-- Plate tabs -->\n <div class=\"mld-plate-editor__tabs\">\n <button\n v-for=\"(plate, index) in editor.plates.value\"\n :key=\"plate.id\"\n type=\"button\"\n :class=\"['mld-plate-editor__tab', { 'mld-plate-editor__tab--active': plate.id === editor.activePlate.value?.id }]\"\n @click=\"editor.setActivePlate(plate.id)\"\n >\n <span\n class=\"mld-plate-editor__tab-slot\"\n :style=\"{ backgroundColor: SLOT_COLORS[getPlateSlot(plate.id, index)] }\"\n />\n <span class=\"mld-plate-editor__tab-name\">{{ plate.name }}</span>\n <span\n v-if=\"getPlateWellCount(plate.id) > 0\"\n class=\"mld-plate-editor__tab-count\"\n >\n {{ getPlateWellCount(plate.id) }}\n </span>\n <button\n v-if=\"editor.plates.value.length > 1\"\n type=\"button\"\n class=\"mld-plate-editor__tab-remove\"\n :aria-label=\"`Remove ${plate.name}`\"\n @click.stop=\"handleRemovePlate(plate.id)\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n </button>\n </button>\n\n <button\n v-if=\"allowAddPlates && editor.plates.value.length < maxPlates\"\n type=\"button\"\n class=\"mld-plate-editor__add-plate\"\n aria-label=\"Add plate\"\n @click=\"handleAddPlate\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M5 12h14\" /><path d=\"M12 5v14\" />\n </svg>\n <span>Add</span>\n </button>\n </div>\n\n <div class=\"mld-plate-editor__spacer\" />\n\n <!-- Actions -->\n <div class=\"mld-plate-editor__actions\">\n <button\n type=\"button\"\n :disabled=\"!editor.canUndo.value\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Undo (Ctrl+Z)\"\n @click=\"handleUndo\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 14 4 9l5-5\" /><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\" />\n </svg>\n </button>\n\n <button\n type=\"button\"\n :disabled=\"!editor.canRedo.value\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Redo (Ctrl+Shift+Z)\"\n @click=\"handleRedo\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m15 14 5-5-5-5\" /><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\" />\n </svg>\n </button>\n\n <div class=\"mld-plate-editor__divider\" />\n\n <button\n type=\"button\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Import\"\n @click=\"showImportModal = true\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 3v12\" /><path d=\"m17 8-5-5-5 5\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n </svg>\n </button>\n\n <button\n type=\"button\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Export JSON\"\n @click=\"handleExport('json')\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 15V3\" /><path d=\"m7 10 5 5 5-5\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Well plate -->\n <WellPlate\n v-if=\"editor.activePlate.value\"\n :model-value=\"editor.selectedWells.value\"\n :format=\"editor.activePlate.value.format\"\n :wells=\"wellsData\"\n :sample-colors=\"sampleColors\"\n :size=\"size\"\n selection-mode=\"rectangle\"\n show-sample-type-indicator\n @update:model-value=\"handleSelectionChange\"\n />\n\n <!-- Selection info bar -->\n <div\n v-if=\"editor.selectedWells.value.length > 0\"\n class=\"mld-plate-editor__selection-bar\"\n >\n <span class=\"mld-plate-editor__selection-count\">\n <strong>{{ editor.selectedWells.value.length }}</strong> wells selected\n </span>\n <div class=\"mld-plate-editor__spacer\" />\n <button\n v-if=\"editor.activeSampleId.value\"\n type=\"button\"\n class=\"mld-plate-editor__assign-btn\"\n @click=\"handleAssignSample\"\n >\n Assign {{ editor.samples.value.find(s => s.id === editor.activeSampleId.value)?.name }}\n </button>\n <button\n type=\"button\"\n class=\"mld-plate-editor__clear-btn\"\n @click=\"handleClearWells\"\n >\n Clear\n </button>\n </div>\n\n <!-- Legend -->\n <div class=\"mld-plate-editor__legend\">\n <div class=\"mld-plate-editor__legend-items\">\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">Sample</span>\n </div>\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(59, 130, 246, 0.15); border: 1px solid rgba(59, 130, 246, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">Control</span>\n </div>\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(249, 115, 22, 0.15); border: 1px solid rgba(249, 115, 22, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">Blank</span>\n </div>\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">QC</span>\n </div>\n </div>\n <span class=\"mld-plate-editor__legend-hint\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72\" /><path d=\"m14 7 3 3\" /><path d=\"M5 6v4\" /><path d=\"M19 14v4\" /><path d=\"M10 2v2\" /><path d=\"M7 8H3\" /><path d=\"M21 16h-4\" /><path d=\"M11 3H9\" />\n </svg>\n Drag to select\n </span>\n </div>\n </div>\n\n <!-- Sidebar -->\n <div v-if=\"showSidebar\" class=\"mld-plate-editor__sidebar\">\n <div class=\"mld-plate-editor__sidebar-panel\">\n <h3 class=\"mld-plate-editor__sidebar-title\">Sample Types</h3>\n\n <SampleLegend\n :model-value=\"editor.activeSampleId.value\"\n :samples=\"editor.samples.value\"\n :editable=\"allowAddSamples\"\n :size=\"size === 'lg' ? 'md' : 'sm'\"\n @update:model-value=\"editor.setActiveSample($event)\"\n @sample-click=\"handleSampleClick\"\n @sample-remove=\"handleRemoveSample\"\n />\n\n <!-- Add sample -->\n <div v-if=\"allowAddSamples\" class=\"mld-plate-editor__add-sample\">\n <div class=\"mld-plate-editor__add-sample-form\">\n <input\n v-model=\"newSampleName\"\n type=\"text\"\n placeholder=\"New sample...\"\n class=\"mld-plate-editor__add-sample-input\"\n @keyup.enter=\"handleAddSample\"\n />\n <button\n type=\"button\"\n :disabled=\"!newSampleName.trim()\"\n class=\"mld-plate-editor__add-sample-btn\"\n @click=\"handleAddSample\"\n >\n Add\n </button>\n </div>\n </div>\n\n <!-- Keyboard shortcuts -->\n <div class=\"mld-plate-editor__shortcuts\">\n <h4 class=\"mld-plate-editor__shortcuts-title\">Shortcuts</h4>\n <div class=\"mld-plate-editor__shortcuts-list\">\n <div><kbd class=\"mld-plate-editor__shortcut-key\">1-9</kbd> Quick assign</div>\n <div><kbd class=\"mld-plate-editor__shortcut-key\">Del</kbd> Clear wells</div>\n <div><kbd class=\"mld-plate-editor__shortcut-key\">Ctrl+Z</kbd> Undo</div>\n <div><kbd class=\"mld-plate-editor__shortcut-key\">Ctrl+A</kbd> Select all</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Import modal -->\n <Teleport to=\"body\">\n <div\n v-if=\"showImportModal\"\n class=\"mld-plate-editor__modal-overlay\"\n @click.self=\"showImportModal = false\"\n >\n <div class=\"mld-plate-editor__modal\">\n <h3 class=\"mld-plate-editor__modal-title\">Import Plate Map</h3>\n\n <div class=\"mld-plate-editor__modal-field\">\n <label class=\"mld-plate-editor__modal-label\">Format</label>\n <select\n v-model=\"importFormat\"\n class=\"mld-plate-editor__modal-select\"\n >\n <option value=\"json\">JSON</option>\n <option value=\"csv\">CSV</option>\n </select>\n </div>\n\n <div class=\"mld-plate-editor__modal-field\">\n <label class=\"mld-plate-editor__modal-label\">Data</label>\n <textarea\n v-model=\"importText\"\n rows=\"8\"\n class=\"mld-plate-editor__modal-textarea\"\n placeholder=\"Paste your data here...\"\n />\n </div>\n\n <div class=\"mld-plate-editor__modal-actions\">\n <button\n type=\"button\"\n class=\"mld-plate-editor__modal-cancel\"\n @click=\"showImportModal = false\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n :disabled=\"!importText.trim()\"\n class=\"mld-plate-editor__modal-submit\"\n @click=\"handleImport\"\n >\n Import\n </button>\n </div>\n </div>\n </div>\n </Teleport>\n </div>\n</template>\n\n<style>\n@import '../styles/components/plate-map-editor.css';\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_openBlock","_Fragment","_renderList","_unref","_normalizeClass","_normalizeStyle","_toDisplayString","_withModifiers","_createBlock","WellPlate","_createVNode","SampleLegend","_Teleport"],"mappings":"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"}
1
+ {"version":3,"file":"PlateMapEditor.vue.js","sources":["../../src/components/PlateMapEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, watch, onMounted, onUnmounted } from 'vue'\nimport type { PlateMapEditorState, WellPlateFormat, SampleType, Well } from '../types'\nimport { useWellPlateEditor } from '../composables/useWellPlateEditor'\nimport WellPlate from './WellPlate.vue'\nimport SampleLegend from './SampleLegend.vue'\n\n// Slot colors matching MSExpDesigner\ntype SlotPosition = 'R' | 'G' | 'B' | 'Y'\nconst SLOT_COLORS: Record<SlotPosition, string> = {\n R: '#ef4444', // red\n G: '#22c55e', // green\n B: '#3b82f6', // blue\n Y: '#eab308', // yellow\n}\nconst SLOT_ORDER: SlotPosition[] = ['R', 'G', 'B', 'Y']\n\ninterface Props {\n modelValue?: PlateMapEditorState\n format?: WellPlateFormat\n maxPlates?: number\n samples?: SampleType[]\n showToolbar?: boolean\n showSidebar?: boolean\n allowAddPlates?: boolean\n allowAddSamples?: boolean\n size?: 'sm' | 'md' | 'lg' | 'xl' | 'fill'\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n modelValue: undefined,\n format: 96,\n maxPlates: 10,\n samples: () => [],\n showToolbar: true,\n showSidebar: true,\n allowAddPlates: true,\n allowAddSamples: true,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [state: PlateMapEditorState]\n 'plate-add': [plate: { id: string; name: string }]\n 'plate-remove': [plateId: string]\n 'sample-assign': [wellIds: string[], sampleId: string | undefined]\n 'wells-clear': [wellIds: string[]]\n 'undo': []\n 'redo': []\n 'export': [data: string, format: 'json' | 'csv']\n 'import': [success: boolean]\n}>()\n\nconst editor = useWellPlateEditor(props.modelValue, {\n defaultFormat: props.format,\n})\n\nconst newSampleName = ref('')\nconst showImportModal = ref(false)\nconst importText = ref('')\nconst importFormat = ref<'json' | 'csv'>('json')\n\n// Track slot assignment for each plate\nconst plateSlots = ref<Map<string, SlotPosition>>(new Map())\n\n// Assign slots to plates on creation\nfunction getPlateSlot(plateId: string, plateIndex: number): SlotPosition {\n if (!plateSlots.value.has(plateId)) {\n plateSlots.value.set(plateId, SLOT_ORDER[plateIndex % SLOT_ORDER.length])\n }\n return plateSlots.value.get(plateId)!\n}\n\nconst sampleColors = computed(() => {\n const colors: Record<string, string> = {}\n for (const sample of editor.samples.value) {\n if (sample.color) {\n colors[sample.id] = sample.color\n }\n }\n return colors\n})\n\nconst wellsData = computed(() => {\n const plate = editor.activePlate.value\n if (!plate) return {}\n\n const wells: Record<string, Partial<Well>> = {}\n for (const [wellId, well] of Object.entries(plate.wells)) {\n wells[wellId] = {\n state: well.state,\n sampleType: well.sampleType,\n value: well.value,\n }\n }\n return wells\n})\n\n// Count samples in a plate\nfunction getPlateWellCount(plateId: string): number {\n const plate = editor.plates.value.find(p => p.id === plateId)\n if (!plate) return 0\n return Object.values(plate.wells).filter(w => w.sampleType).length\n}\n\nwatch(\n () => editor.state.value,\n (newState) => emit('update:modelValue', { ...newState }),\n { deep: true }\n)\n\nwatch(\n () => props.modelValue,\n (newValue) => {\n if (newValue) editor.loadState(newValue)\n }\n)\n\nfunction handleSelectionChange(wellIds: string[]) {\n editor.setSelectedWells(wellIds)\n}\n\nfunction handleSampleClick(sample: SampleType) {\n const newSampleId = editor.activeSampleId.value === sample.id ? undefined : sample.id\n editor.setActiveSample(newSampleId)\n}\n\nfunction handleAssignSample() {\n const wells = editor.selectedWells.value\n if (wells.length === 0) return\n\n editor.assignSample(wells, editor.activeSampleId.value)\n emit('sample-assign', wells, editor.activeSampleId.value)\n}\n\nfunction handleClearWells() {\n const wells = editor.selectedWells.value\n if (wells.length === 0) return\n\n editor.clearWells(wells)\n emit('wells-clear', wells)\n}\n\nfunction handleAddSample() {\n if (!newSampleName.value.trim()) return\n editor.addSample(newSampleName.value.trim())\n newSampleName.value = ''\n}\n\nfunction handleRemoveSample(sampleId: string) {\n editor.removeSample(sampleId)\n}\n\nfunction handleAddPlate() {\n if (editor.plates.value.length >= props.maxPlates) return\n const plate = editor.addPlate()\n emit('plate-add', { id: plate.id, name: plate.name })\n}\n\nfunction handleRemovePlate(plateId: string) {\n editor.removePlate(plateId)\n plateSlots.value.delete(plateId)\n emit('plate-remove', plateId)\n}\n\nfunction handleUndo() {\n editor.undo()\n emit('undo')\n}\n\nfunction handleRedo() {\n editor.redo()\n emit('redo')\n}\n\nfunction handleExport(format: 'json' | 'csv') {\n const data = editor.exportData(format)\n emit('export', data, format)\n\n const blob = new Blob([data], { type: format === 'json' ? 'application/json' : 'text/csv' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = `plate-map.${format}`\n a.click()\n URL.revokeObjectURL(url)\n}\n\nfunction handleImport() {\n const success = editor.importData(importText.value, importFormat.value)\n emit('import', success)\n if (success) {\n showImportModal.value = false\n importText.value = ''\n }\n}\n\nfunction handleKeyDown(event: KeyboardEvent) {\n const isUndo = (event.metaKey || event.ctrlKey) && event.key === 'z'\n if (isUndo) {\n event.preventDefault()\n event.shiftKey ? handleRedo() : handleUndo()\n return\n }\n\n const isDelete = event.key === 'Delete' || event.key === 'Backspace'\n if (isDelete && editor.selectedWells.value.length > 0) {\n event.preventDefault()\n handleClearWells()\n return\n }\n\n const num = parseInt(event.key)\n const isValidSampleKey = num >= 1 && num <= 9 && editor.samples.value.length >= num\n if (isValidSampleKey) {\n editor.setActiveSample(editor.samples.value[num - 1].id)\n if (editor.selectedWells.value.length > 0) {\n handleAssignSample()\n }\n }\n}\n\nonMounted(() => {\n document.addEventListener('keydown', handleKeyDown)\n})\n\nonUnmounted(() => {\n document.removeEventListener('keydown', handleKeyDown)\n})\n</script>\n\n<template>\n <div :class=\"['mld-plate-editor', { 'mld-plate-editor--with-sidebar': showSidebar }]\">\n <!-- Main plate area -->\n <div class=\"mld-plate-editor__main\">\n <!-- Toolbar -->\n <div v-if=\"showToolbar\" class=\"mld-plate-editor__toolbar\">\n <!-- Plate tabs -->\n <div class=\"mld-plate-editor__tabs\">\n <button\n v-for=\"(plate, index) in editor.plates.value\"\n :key=\"plate.id\"\n type=\"button\"\n :class=\"['mld-plate-editor__tab', { 'mld-plate-editor__tab--active': plate.id === editor.activePlate.value?.id }]\"\n @click=\"editor.setActivePlate(plate.id)\"\n >\n <span\n class=\"mld-plate-editor__tab-slot\"\n :style=\"{ backgroundColor: SLOT_COLORS[getPlateSlot(plate.id, index)] }\"\n />\n <span class=\"mld-plate-editor__tab-name\">{{ plate.name }}</span>\n <span\n v-if=\"getPlateWellCount(plate.id) > 0\"\n class=\"mld-plate-editor__tab-count\"\n >\n {{ getPlateWellCount(plate.id) }}\n </span>\n <button\n v-if=\"editor.plates.value.length > 1\"\n type=\"button\"\n class=\"mld-plate-editor__tab-remove\"\n :aria-label=\"`Remove ${plate.name}`\"\n @click.stop=\"handleRemovePlate(plate.id)\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n </button>\n </button>\n\n <button\n v-if=\"allowAddPlates && editor.plates.value.length < maxPlates\"\n type=\"button\"\n class=\"mld-plate-editor__add-plate\"\n aria-label=\"Add plate\"\n @click=\"handleAddPlate\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M5 12h14\" /><path d=\"M12 5v14\" />\n </svg>\n <span>Add</span>\n </button>\n </div>\n\n <div class=\"mld-plate-editor__spacer\" />\n\n <!-- Actions -->\n <div class=\"mld-plate-editor__actions\">\n <button\n type=\"button\"\n :disabled=\"!editor.canUndo.value\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Undo (Ctrl+Z)\"\n @click=\"handleUndo\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 14 4 9l5-5\" /><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11\" />\n </svg>\n </button>\n\n <button\n type=\"button\"\n :disabled=\"!editor.canRedo.value\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Redo (Ctrl+Shift+Z)\"\n @click=\"handleRedo\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m15 14 5-5-5-5\" /><path d=\"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13\" />\n </svg>\n </button>\n\n <div class=\"mld-plate-editor__divider\" />\n\n <button\n type=\"button\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Import\"\n @click=\"showImportModal = true\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 3v12\" /><path d=\"m17 8-5-5-5 5\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n </svg>\n </button>\n\n <button\n type=\"button\"\n class=\"mld-plate-editor__action-btn\"\n title=\"Export JSON\"\n @click=\"handleExport('json')\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 15V3\" /><path d=\"m7 10 5 5 5-5\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Well plate -->\n <WellPlate\n v-if=\"editor.activePlate.value\"\n :model-value=\"editor.selectedWells.value\"\n :format=\"editor.activePlate.value.format\"\n :wells=\"wellsData\"\n :sample-colors=\"sampleColors\"\n :size=\"size\"\n selection-mode=\"rectangle\"\n show-sample-type-indicator\n @update:model-value=\"handleSelectionChange\"\n />\n\n <!-- Selection info bar -->\n <div\n v-if=\"editor.selectedWells.value.length > 0\"\n class=\"mld-plate-editor__selection-bar\"\n >\n <span class=\"mld-plate-editor__selection-count\">\n <strong>{{ editor.selectedWells.value.length }}</strong> wells selected\n </span>\n <div class=\"mld-plate-editor__spacer\" />\n <button\n v-if=\"editor.activeSampleId.value\"\n type=\"button\"\n class=\"mld-plate-editor__assign-btn\"\n @click=\"handleAssignSample\"\n >\n Assign {{ editor.samples.value.find(s => s.id === editor.activeSampleId.value)?.name }}\n </button>\n <button\n type=\"button\"\n class=\"mld-plate-editor__clear-btn\"\n @click=\"handleClearWells\"\n >\n Clear\n </button>\n </div>\n\n <!-- Legend -->\n <div class=\"mld-plate-editor__legend\">\n <div class=\"mld-plate-editor__legend-items\">\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">Sample</span>\n </div>\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(59, 130, 246, 0.15); border: 1px solid rgba(59, 130, 246, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">Control</span>\n </div>\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(249, 115, 22, 0.15); border: 1px solid rgba(249, 115, 22, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">Blank</span>\n </div>\n <div class=\"mld-plate-editor__legend-item\">\n <div class=\"mld-plate-editor__legend-swatch\" style=\"background-color: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.4)\" />\n <span class=\"mld-plate-editor__legend-label\">QC</span>\n </div>\n </div>\n <span class=\"mld-plate-editor__legend-hint\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72\" /><path d=\"m14 7 3 3\" /><path d=\"M5 6v4\" /><path d=\"M19 14v4\" /><path d=\"M10 2v2\" /><path d=\"M7 8H3\" /><path d=\"M21 16h-4\" /><path d=\"M11 3H9\" />\n </svg>\n Drag to select\n </span>\n </div>\n </div>\n\n <!-- Sidebar -->\n <div v-if=\"showSidebar\" class=\"mld-plate-editor__sidebar\">\n <div class=\"mld-plate-editor__sidebar-panel\">\n <h3 class=\"mld-plate-editor__sidebar-title\">Sample Types</h3>\n\n <SampleLegend\n :model-value=\"editor.activeSampleId.value\"\n :samples=\"editor.samples.value\"\n :editable=\"allowAddSamples\"\n :size=\"size === 'lg' ? 'md' : 'sm'\"\n @update:model-value=\"editor.setActiveSample($event)\"\n @sample-click=\"handleSampleClick\"\n @sample-remove=\"handleRemoveSample\"\n />\n\n <!-- Add sample -->\n <div v-if=\"allowAddSamples\" class=\"mld-plate-editor__add-sample\">\n <div class=\"mld-plate-editor__add-sample-form\">\n <input\n v-model=\"newSampleName\"\n type=\"text\"\n placeholder=\"New sample...\"\n class=\"mld-plate-editor__add-sample-input\"\n @keyup.enter=\"handleAddSample\"\n />\n <button\n type=\"button\"\n :disabled=\"!newSampleName.trim()\"\n class=\"mld-plate-editor__add-sample-btn\"\n @click=\"handleAddSample\"\n >\n Add\n </button>\n </div>\n </div>\n\n <!-- Keyboard shortcuts -->\n <div class=\"mld-plate-editor__shortcuts\">\n <h4 class=\"mld-plate-editor__shortcuts-title\">Shortcuts</h4>\n <div class=\"mld-plate-editor__shortcuts-list\">\n <div><kbd class=\"mld-plate-editor__shortcut-key\">1-9</kbd> Quick assign</div>\n <div><kbd class=\"mld-plate-editor__shortcut-key\">Del</kbd> Clear wells</div>\n <div><kbd class=\"mld-plate-editor__shortcut-key\">Ctrl+Z</kbd> Undo</div>\n <div><kbd class=\"mld-plate-editor__shortcut-key\">Ctrl+A</kbd> Select all</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Import modal -->\n <Teleport to=\"body\">\n <div\n v-if=\"showImportModal\"\n class=\"mld-plate-editor__modal-overlay\"\n @click.self=\"showImportModal = false\"\n >\n <div class=\"mld-plate-editor__modal\">\n <h3 class=\"mld-plate-editor__modal-title\">Import Plate Map</h3>\n\n <div class=\"mld-plate-editor__modal-field\">\n <label class=\"mld-plate-editor__modal-label\">Format</label>\n <select\n v-model=\"importFormat\"\n class=\"mld-plate-editor__modal-select\"\n >\n <option value=\"json\">JSON</option>\n <option value=\"csv\">CSV</option>\n </select>\n </div>\n\n <div class=\"mld-plate-editor__modal-field\">\n <label class=\"mld-plate-editor__modal-label\">Data</label>\n <textarea\n v-model=\"importText\"\n rows=\"8\"\n class=\"mld-plate-editor__modal-textarea\"\n placeholder=\"Paste your data here...\"\n />\n </div>\n\n <div class=\"mld-plate-editor__modal-actions\">\n <button\n type=\"button\"\n class=\"mld-plate-editor__modal-cancel\"\n @click=\"showImportModal = false\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n :disabled=\"!importText.trim()\"\n class=\"mld-plate-editor__modal-submit\"\n @click=\"handleImport\"\n >\n Import\n </button>\n </div>\n </div>\n </div>\n </Teleport>\n </div>\n</template>\n\n<style>\n@import '../styles/components/plate-map-editor.css';\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_openBlock","_Fragment","_renderList","_unref","_normalizeClass","_normalizeStyle","_toDisplayString","_withModifiers","_createBlock","WellPlate","_createVNode","SampleLegend","_Teleport"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import axios from "axios";
2
- import { ref, onMounted, onUnmounted, watch } from "vue";
2
+ import { ref, getCurrentInstance, onMounted, onUnmounted, watch } from "vue";
3
3
  import { useAuthStore } from "../stores/auth.js";
4
4
  import { useSettingsStore } from "../stores/settings.js";
5
5
  const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
@@ -9,7 +9,6 @@ let _refreshTimerId = null;
9
9
  function useAuth() {
10
10
  const authStore = useAuthStore();
11
11
  const settingsStore = useSettingsStore();
12
- const refreshTimerId = ref(_refreshTimerId);
13
12
  const isRefreshing = ref(false);
14
13
  function getApiBaseUrl() {
15
14
  return settingsStore.getApiBaseUrl();
@@ -170,14 +169,14 @@ function useAuth() {
170
169
  return;
171
170
  }
172
171
  const delay = refreshAt - now;
173
- refreshTimerId.value = window.setTimeout(() => {
172
+ _refreshTimerId = window.setTimeout(() => {
174
173
  refreshToken();
175
174
  }, delay);
176
175
  }
177
176
  function stopTokenRefresh() {
178
- if (refreshTimerId.value !== null) {
179
- window.clearTimeout(refreshTimerId.value);
180
- refreshTimerId.value = null;
177
+ if (_refreshTimerId !== null) {
178
+ window.clearTimeout(_refreshTimerId);
179
+ _refreshTimerId = null;
181
180
  }
182
181
  }
183
182
  function checkAndRefreshIfNeeded() {
@@ -245,26 +244,28 @@ function useAuth() {
245
244
  }
246
245
  }
247
246
  let checkInterval = null;
248
- onMounted(() => {
249
- checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS);
250
- });
251
- onUnmounted(() => {
252
- stopTokenRefresh();
253
- if (checkInterval !== null) {
254
- window.clearInterval(checkInterval);
255
- checkInterval = null;
256
- }
257
- });
258
- watch(
259
- () => authStore.tokenExpires,
260
- (newExpires) => {
261
- if (newExpires) {
262
- scheduleTokenRefresh();
263
- } else {
264
- stopTokenRefresh();
247
+ if (getCurrentInstance()) {
248
+ onMounted(() => {
249
+ checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS);
250
+ });
251
+ onUnmounted(() => {
252
+ stopTokenRefresh();
253
+ if (checkInterval !== null) {
254
+ window.clearInterval(checkInterval);
255
+ checkInterval = null;
265
256
  }
266
- }
267
- );
257
+ });
258
+ watch(
259
+ () => authStore.tokenExpires,
260
+ (newExpires) => {
261
+ if (newExpires) {
262
+ scheduleTokenRefresh();
263
+ } else {
264
+ stopTokenRefresh();
265
+ }
266
+ }
267
+ );
268
+ }
268
269
  return {
269
270
  // Core auth methods
270
271
  login,
@@ -1 +1 @@
1
- {"version":3,"file":"useAuth.js","sources":["../../src/composables/useAuth.ts"],"sourcesContent":["import axios from 'axios'\nimport { ref, onMounted, onUnmounted, watch, type Ref } from 'vue'\nimport { useAuthStore } from '../stores/auth'\nimport { useSettingsStore } from '../stores/settings'\nimport type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'\n\ninterface UserResponse {\n id: string\n username: string\n shortname: string | null\n email: string | null\n role: string\n is_active: boolean\n}\n\ninterface RefreshResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\n// Token refresh configuration\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000 // Refresh 5 minutes before expiry\nconst TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1000 // Check every minute\n\n/**\n * Authentication composable with automatic token refresh.\n *\n * Features:\n * - Automatic token refresh before expiration\n * - Login/logout/register functionality\n * - Token verification on startup\n * - User profile management\n *\n * @example\n * ```typescript\n * const { login, logout, isAuthenticated, user } = useAuth()\n *\n * // Login\n * const success = await login('username', 'password')\n *\n * // Automatic refresh is enabled by default\n * // Tokens are refreshed 5 minutes before expiration\n * ```\n */\nexport interface UseAuthReturn {\n login: (username: string, password: string) => Promise<boolean>\n logout: () => void\n register: (username: string, password: string, email?: string) => Promise<boolean>\n verifyToken: () => Promise<boolean>\n fetchAuthConfig: () => Promise<AuthConfig>\n initializeAuth: () => Promise<void>\n getCurrentUser: () => Promise<UserInfo | null>\n getAuthHeader: () => Record<string, string>\n updateProfile: (data: { email?: string; shortname?: string; currentPassword?: string; newPassword?: string }) => Promise<{ success: boolean; error?: string }>\n refreshToken: () => Promise<boolean>\n isRefreshing: Ref<boolean>\n}\n\n// Module-level singletons to prevent duplicate refresh requests and timers\n// across multiple useAuth() instances\nlet _refreshPromise: Promise<boolean> | null = null\nlet _refreshTimerId: number | null = null\n\nexport function useAuth(): UseAuthReturn {\n const authStore = useAuthStore()\n const settingsStore = useSettingsStore()\n\n // Track refresh timer (module-level to prevent timer multiplication)\n const refreshTimerId = ref<number | null>(_refreshTimerId)\n const isRefreshing = ref(false)\n\n function getApiBaseUrl(): string {\n return settingsStore.getApiBaseUrl()\n }\n\n async function fetchAuthConfig(): Promise<AuthConfig> {\n try {\n const response = await axios.get<{\n auth_required: boolean\n passkey_enabled: boolean\n passkey_registered?: boolean\n registration_enabled?: boolean\n database_mode?: string\n }>(`${getApiBaseUrl()}/setup/config/public`)\n\n const config: AuthConfig = {\n authRequired: response.data.auth_required,\n passkeyEnabled: response.data.passkey_enabled,\n passkeyRegistered: response.data.passkey_registered ?? false,\n registrationEnabled: response.data.registration_enabled ?? false,\n databaseMode: response.data.database_mode ?? 'none',\n }\n\n authStore.setAuthConfig(config)\n return config\n } catch (error) {\n console.error('Failed to fetch auth config:', error)\n return {\n authRequired: false,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n }\n }\n }\n\n async function login(username: string, password: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n const response = await axios.post<LoginResponse>(\n `${getApiBaseUrl()}/auth/login`,\n { username, password }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n authStore.setUsername(username)\n\n await getCurrentUser()\n\n // Start auto-refresh after successful login\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Login failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function register(username: string, password: string, email?: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n await axios.post<UserResponse>(\n `${getApiBaseUrl()}/users/register`,\n { username, password, email }\n )\n\n return await login(username, password)\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Registration failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function getCurrentUser(): Promise<UserInfo | null> {\n if (!authStore.token) {\n return null\n }\n\n try {\n const response = await axios.get<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n\n authStore.setUserInfo(userInfo)\n return userInfo\n } catch {\n return null\n }\n }\n\n async function verifyToken(): Promise<boolean> {\n if (!authStore.token) {\n return false\n }\n\n try {\n const response = await axios.get<TokenVerifyResponse>(\n `${getApiBaseUrl()}/auth/verify`,\n {\n headers: {\n Authorization: `Bearer ${authStore.token}`,\n },\n }\n )\n\n if (response.data.valid && response.data.username) {\n authStore.setUsername(response.data.username)\n return true\n }\n\n authStore.clearToken()\n return false\n } catch {\n authStore.clearToken()\n return false\n }\n }\n\n /**\n * Refresh the authentication token.\n * Called automatically before token expiration.\n * Uses promise caching to prevent concurrent refresh requests.\n */\n async function refreshToken(): Promise<boolean> {\n if (!authStore.token) return false\n if (_refreshPromise) return _refreshPromise\n\n _refreshPromise = (async () => {\n isRefreshing.value = true\n\n try {\n const response = await axios.post<RefreshResponse>(\n `${getApiBaseUrl()}/auth/refresh`,\n {},\n { headers: getAuthHeader() }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n\n // Reschedule next refresh\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n // If refresh fails, the token may have been revoked\n // Clear auth state and let user re-login\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n console.warn('[Auth] Token refresh failed - session expired')\n authStore.clearToken()\n stopTokenRefresh()\n }\n return false\n } finally {\n isRefreshing.value = false\n _refreshPromise = null\n }\n })()\n\n return _refreshPromise\n }\n\n /**\n * Schedule automatic token refresh before expiration.\n */\n function scheduleTokenRefresh(): void {\n // Clear any existing timer\n stopTokenRefresh()\n\n if (!authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (refreshAt <= now) {\n // Token is already close to expiring or expired, refresh now\n refreshToken()\n return\n }\n\n // Schedule refresh\n const delay = refreshAt - now\n refreshTimerId.value = window.setTimeout(() => {\n refreshToken()\n }, delay)\n }\n\n /**\n * Stop automatic token refresh.\n */\n function stopTokenRefresh(): void {\n if (refreshTimerId.value !== null) {\n window.clearTimeout(refreshTimerId.value)\n refreshTimerId.value = null\n }\n }\n\n /**\n * Check if token needs refresh and refresh if necessary.\n * Called periodically as a safety net.\n */\n function checkAndRefreshIfNeeded(): void {\n if (!authStore.token || !authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (now >= refreshAt) {\n refreshToken()\n }\n }\n\n async function initializeAuth(): Promise<void> {\n authStore.initialize()\n await fetchAuthConfig()\n\n if (authStore.token) {\n const valid = await verifyToken()\n if (valid) {\n await getCurrentUser()\n // Start auto-refresh for existing session\n scheduleTokenRefresh()\n }\n }\n }\n\n function logout(): void {\n stopTokenRefresh()\n authStore.logout()\n }\n\n function getAuthHeader(): Record<string, string> {\n if (authStore.token) {\n return { Authorization: `Bearer ${authStore.token}` }\n }\n return {}\n }\n\n async function updateProfile(data: {\n email?: string\n shortname?: string\n currentPassword?: string\n newPassword?: string\n }): Promise<{ success: boolean; error?: string }> {\n if (!authStore.token) {\n return { success: false, error: 'Not authenticated' }\n }\n\n try {\n const requestData: UpdateProfileRequest = {}\n if (data.email !== undefined) requestData.email = data.email\n if (data.shortname !== undefined) requestData.shortname = data.shortname\n if (data.currentPassword) requestData.current_password = data.currentPassword\n if (data.newPassword) requestData.new_password = data.newPassword\n\n const response = await axios.put<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n requestData,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n authStore.setUserInfo(userInfo)\n\n return { success: true }\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n return { success: false, error: error.response.data.detail || 'Update failed' }\n }\n return { success: false, error: 'Network error. Please try again.' }\n }\n }\n\n // Set up periodic check as safety net\n let checkInterval: number | null = null\n\n onMounted(() => {\n // Start periodic check\n checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)\n })\n\n onUnmounted(() => {\n // Clean up on unmount\n stopTokenRefresh()\n if (checkInterval !== null) {\n window.clearInterval(checkInterval)\n checkInterval = null\n }\n })\n\n // Watch for token changes to reschedule refresh\n watch(\n () => authStore.tokenExpires,\n (newExpires) => {\n if (newExpires) {\n scheduleTokenRefresh()\n } else {\n stopTokenRefresh()\n }\n }\n )\n\n return {\n // Core auth methods\n login,\n logout,\n register,\n verifyToken,\n fetchAuthConfig,\n initializeAuth,\n getCurrentUser,\n getAuthHeader,\n updateProfile,\n\n // Token refresh\n refreshToken,\n isRefreshing,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAuth.js","sources":["../../src/composables/useAuth.ts"],"sourcesContent":["import axios from 'axios'\nimport { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'\nimport { useAuthStore } from '../stores/auth'\nimport { useSettingsStore } from '../stores/settings'\nimport type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'\n\ninterface UserResponse {\n id: string\n username: string\n shortname: string | null\n email: string | null\n role: string\n is_active: boolean\n}\n\ninterface RefreshResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\n// Token refresh configuration\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000 // Refresh 5 minutes before expiry\nconst TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1000 // Check every minute\n\n/**\n * Authentication composable with automatic token refresh.\n *\n * Features:\n * - Automatic token refresh before expiration\n * - Login/logout/register functionality\n * - Token verification on startup\n * - User profile management\n *\n * @example\n * ```typescript\n * const { login, logout, isAuthenticated, user } = useAuth()\n *\n * // Login\n * const success = await login('username', 'password')\n *\n * // Automatic refresh is enabled by default\n * // Tokens are refreshed 5 minutes before expiration\n * ```\n */\nexport interface UseAuthReturn {\n login: (username: string, password: string) => Promise<boolean>\n logout: () => void\n register: (username: string, password: string, email?: string) => Promise<boolean>\n verifyToken: () => Promise<boolean>\n fetchAuthConfig: () => Promise<AuthConfig>\n initializeAuth: () => Promise<void>\n getCurrentUser: () => Promise<UserInfo | null>\n getAuthHeader: () => Record<string, string>\n updateProfile: (data: { email?: string; shortname?: string; currentPassword?: string; newPassword?: string }) => Promise<{ success: boolean; error?: string }>\n refreshToken: () => Promise<boolean>\n isRefreshing: Ref<boolean>\n}\n\n// Module-level singletons to prevent duplicate refresh requests and timers\n// across multiple useAuth() instances\nlet _refreshPromise: Promise<boolean> | null = null\nlet _refreshTimerId: number | null = null\n\nexport function useAuth(): UseAuthReturn {\n const authStore = useAuthStore()\n const settingsStore = useSettingsStore()\n\n const isRefreshing = ref(false)\n\n function getApiBaseUrl(): string {\n return settingsStore.getApiBaseUrl()\n }\n\n async function fetchAuthConfig(): Promise<AuthConfig> {\n try {\n const response = await axios.get<{\n auth_required: boolean\n passkey_enabled: boolean\n passkey_registered?: boolean\n registration_enabled?: boolean\n database_mode?: string\n }>(`${getApiBaseUrl()}/setup/config/public`)\n\n const config: AuthConfig = {\n authRequired: response.data.auth_required,\n passkeyEnabled: response.data.passkey_enabled,\n passkeyRegistered: response.data.passkey_registered ?? false,\n registrationEnabled: response.data.registration_enabled ?? false,\n databaseMode: response.data.database_mode ?? 'none',\n }\n\n authStore.setAuthConfig(config)\n return config\n } catch (error) {\n console.error('Failed to fetch auth config:', error)\n return {\n authRequired: false,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n }\n }\n }\n\n async function login(username: string, password: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n const response = await axios.post<LoginResponse>(\n `${getApiBaseUrl()}/auth/login`,\n { username, password }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n authStore.setUsername(username)\n\n await getCurrentUser()\n\n // Start auto-refresh after successful login\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Login failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function register(username: string, password: string, email?: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n await axios.post<UserResponse>(\n `${getApiBaseUrl()}/users/register`,\n { username, password, email }\n )\n\n return await login(username, password)\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Registration failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function getCurrentUser(): Promise<UserInfo | null> {\n if (!authStore.token) {\n return null\n }\n\n try {\n const response = await axios.get<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n\n authStore.setUserInfo(userInfo)\n return userInfo\n } catch {\n return null\n }\n }\n\n async function verifyToken(): Promise<boolean> {\n if (!authStore.token) {\n return false\n }\n\n try {\n const response = await axios.get<TokenVerifyResponse>(\n `${getApiBaseUrl()}/auth/verify`,\n {\n headers: {\n Authorization: `Bearer ${authStore.token}`,\n },\n }\n )\n\n if (response.data.valid && response.data.username) {\n authStore.setUsername(response.data.username)\n return true\n }\n\n authStore.clearToken()\n return false\n } catch {\n authStore.clearToken()\n return false\n }\n }\n\n /**\n * Refresh the authentication token.\n * Called automatically before token expiration.\n * Uses promise caching to prevent concurrent refresh requests.\n */\n async function refreshToken(): Promise<boolean> {\n if (!authStore.token) return false\n if (_refreshPromise) return _refreshPromise\n\n _refreshPromise = (async () => {\n isRefreshing.value = true\n\n try {\n const response = await axios.post<RefreshResponse>(\n `${getApiBaseUrl()}/auth/refresh`,\n {},\n { headers: getAuthHeader() }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n\n // Reschedule next refresh\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n // If refresh fails, the token may have been revoked\n // Clear auth state and let user re-login\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n console.warn('[Auth] Token refresh failed - session expired')\n authStore.clearToken()\n stopTokenRefresh()\n }\n return false\n } finally {\n isRefreshing.value = false\n _refreshPromise = null\n }\n })()\n\n return _refreshPromise\n }\n\n /**\n * Schedule automatic token refresh before expiration.\n */\n function scheduleTokenRefresh(): void {\n // Clear any existing timer\n stopTokenRefresh()\n\n if (!authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (refreshAt <= now) {\n // Token is already close to expiring or expired, refresh now\n refreshToken()\n return\n }\n\n // Schedule refresh\n const delay = refreshAt - now\n _refreshTimerId = window.setTimeout(() => {\n refreshToken()\n }, delay)\n }\n\n /**\n * Stop automatic token refresh.\n */\n function stopTokenRefresh(): void {\n if (_refreshTimerId !== null) {\n window.clearTimeout(_refreshTimerId)\n _refreshTimerId = null\n }\n }\n\n /**\n * Check if token needs refresh and refresh if necessary.\n * Called periodically as a safety net.\n */\n function checkAndRefreshIfNeeded(): void {\n if (!authStore.token || !authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (now >= refreshAt) {\n refreshToken()\n }\n }\n\n async function initializeAuth(): Promise<void> {\n authStore.initialize()\n await fetchAuthConfig()\n\n if (authStore.token) {\n const valid = await verifyToken()\n if (valid) {\n await getCurrentUser()\n // Start auto-refresh for existing session\n scheduleTokenRefresh()\n }\n }\n }\n\n function logout(): void {\n stopTokenRefresh()\n authStore.logout()\n }\n\n function getAuthHeader(): Record<string, string> {\n if (authStore.token) {\n return { Authorization: `Bearer ${authStore.token}` }\n }\n return {}\n }\n\n async function updateProfile(data: {\n email?: string\n shortname?: string\n currentPassword?: string\n newPassword?: string\n }): Promise<{ success: boolean; error?: string }> {\n if (!authStore.token) {\n return { success: false, error: 'Not authenticated' }\n }\n\n try {\n const requestData: UpdateProfileRequest = {}\n if (data.email !== undefined) requestData.email = data.email\n if (data.shortname !== undefined) requestData.shortname = data.shortname\n if (data.currentPassword) requestData.current_password = data.currentPassword\n if (data.newPassword) requestData.new_password = data.newPassword\n\n const response = await axios.put<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n requestData,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n authStore.setUserInfo(userInfo)\n\n return { success: true }\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n return { success: false, error: error.response.data.detail || 'Update failed' }\n }\n return { success: false, error: 'Network error. Please try again.' }\n }\n }\n\n // Set up periodic check as safety net (only inside component setup)\n let checkInterval: number | null = null\n\n if (getCurrentInstance()) {\n onMounted(() => {\n checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)\n })\n\n onUnmounted(() => {\n stopTokenRefresh()\n if (checkInterval !== null) {\n window.clearInterval(checkInterval)\n checkInterval = null\n }\n })\n\n // Watch for token changes to reschedule refresh\n watch(\n () => authStore.tokenExpires,\n (newExpires) => {\n if (newExpires) {\n scheduleTokenRefresh()\n } else {\n stopTokenRefresh()\n }\n }\n )\n }\n\n return {\n // Core auth methods\n login,\n logout,\n register,\n verifyToken,\n fetchAuthConfig,\n initializeAuth,\n getCurrentUser,\n getAuthHeader,\n updateProfile,\n\n // Token refresh\n refreshToken,\n isRefreshing,\n }\n}\n"],"names":[],"mappings":"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"}
@@ -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":";AAYO,MAAM,iBAAiB;AAAA,EAC5B;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAC5C;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAAA,EAAW;AAC9C;AAEA,MAAM,uBAAuB,CAAC,KAAK,KAAK,GAAG;AAIpC,SAAS,iBAAiB,OAK/B;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,EAAE,WAAW,KAAK,oBAAoB,GAAG,eAAe,GAAG,aAAa,EAAA;AAAA,EACjF;AAEA,MAAI,gBAAgB;AACpB,MAAI,kBAAkB;AACtB,MAAI,iBAAiB;AAErB,aAAW,aAAa,sBAAsB;AAC5C,UAAM,cAAc,MAAM,IAAI,CAAA,SAAQ,KAAK,MAAM,SAAS,EAAE,MAAM;AAClE,UAAM,qCAAqB,IAAA;AAE3B,eAAW,SAAS,aAAa;AAC/B,qBAAe,IAAI,QAAQ,eAAe,IAAI,KAAK,KAAK,KAAK,CAAC;AAAA,IAChE;AAGA,QAAI,YAAY;AAChB,QAAI,gBAAgB;AACpB,eAAW,CAAC,OAAO,IAAI,KAAK,gBAAgB;AAC1C,UAAI,OAAO,iBAAkB,SAAS,iBAAiB,QAAQ,WAAY;AACzE,oBAAY;AACZ,wBAAgB;AAAA,MAClB;AAAA,IACF;AAEA,UAAM,iBAAiB,gBAAgB,MAAM;AAE7C,UAAM,cAAc,YAAY,IAAI,iBAAiB;AAErD,QACE,cAAc,mBACb,gBAAgB,mBACf,qBAAqB,QAAQ,SAAS,IAAI,qBAAqB,QAAQ,aAAiC,GAC1G;AACA,sBAAgB;AAChB,wBAAkB;AAClB,uBAAiB;AAAA,IACnB;AAAA,EACF;AAEA,QAAM,kBAAkB,MAAM,IAAI,CAAA,SAAQ,KAAK,MAAM,aAAa,EAAE,MAAM;AAC1E,QAAM,mBAAmB,gBAAgB,OAAO,CAAA,MAAK,KAAK,CAAC;AAC3D,QAAM,gBAAgB,iBAAiB,SAAS,IAAI,KAAK,IAAI,GAAG,gBAAgB,IAAI;AAEpF,SAAO;AAAA,IACL,WAAW;AAAA,IACX,oBAAoB;AAAA,IACpB;AAAA,IACA,aAAa;AAAA,EAAA;AAEjB;AAEO,SAAS,eACd,OACA,WACA,eACe;AACf,QAAM,WAA0B,CAAA;AAEhC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,aAAa,MAAM,CAAC,EAAE,MAAM,SAAS,EAAE;AAC7C,QAAI,aAAa,eAAe;AAC9B,eAAS,KAAK;AAAA,QACZ,QAAQ,MAAM,CAAC;AAAA,QACf,OAAO;AAAA,QACP;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEA,MAAM,kCAAkB,IAAI;AAAA,EAC1B;AAAA,EAAO;AAAA,EAAO;AAAA,EAAM;AAAA,EAAS;AAAA,EAAO;AAAA,EAAY;AAClD,CAAC;AAEM,SAAS,sBACd,QACA,WACe;AACf,QAAM,WAAW,OAAO,MAAM,SAAS;AACvC,SAAO,SAAS,KAAK,CAAA,QAAO,YAAY,IAAI,IAAI,YAAA,CAAa,CAAC,IAC1D,OACA;AACN;AAEO,SAAS,eACd,SACA,WACA,eACc;AACd,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAA;AAEjC,QAAM,cAAc,gBAAgB;AACpC,QAAM,OAAO,QAAQ,IAAI,CAAA,MAAK;AAC5B,UAAM,QAAQ,EAAE,MAAM,SAAS;AAC/B,UAAM,UAAU,MAAM,SAAS;AAC/B,WAAO;AAAA,MACL,MAAM,MAAM,GAAG,OAAO,EAAE,KAAK,SAAS;AAAA,MACtC,GAAG,MAAM,MAAM,OAAO;AAAA,IAAA;AAAA,EAE1B,CAAC;AAED,QAAM,cAAc;AACpB,QAAM,UAAwB,CAAA;AAC9B,WAAS,MAAM,GAAG,MAAM,aAAa,OAAO;AAC1C,UAAM,SAAS,KAAK,IAAI,CAAA,QAAO,IAAI,GAAG,CAAC;AACvC,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAClC,YAAQ,KAAK;AAAA,MACX,OAAO;AAAA,MACP,MAAM,QAAQ,IAAI,cAAc,SAAS,MAAM,CAAC;AAAA,MAChD,cAAc;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,MAAM,QAAQ,IAAI,WAAW;AAAA,IAAA,CAC9B;AAAA,EACH;AAEA,SAAO;AACT;AAEO,SAAS,aAAa,MAAwB;AACnD,QAAM,SAAmB,CAAA;AACzB,MAAI,UAAU;AACd,MAAI,WAAW;AAEf,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AACnB,QAAI,SAAS,KAAK;AAChB,iBAAW,CAAC;AAAA,IACd,WAAW,SAAS,OAAO,CAAC,UAAU;AACpC,aAAO,KAAK,QAAQ,MAAM;AAC1B,gBAAU;AAAA,IACZ,OAAO;AACL,iBAAW;AAAA,IACb;AAAA,EACF;AACA,SAAO,KAAK,QAAQ,MAAM;AAE1B,SAAO;AACT;AAEO,SAAS,SAAS,MAA6B;AACpD,QAAM,QAAQ,KAAK,KAAA,EAAO,MAAM,IAAI;AACpC,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AAEA,QAAM,UAAU,aAAa,MAAM,CAAC,CAAC;AACrC,QAAM,OAAiC,CAAA;AAEvC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,SAAS,aAAa,MAAM,CAAC,CAAC;AACpC,QAAI,OAAO,WAAW,QAAQ,OAAQ;AACtC,UAAM,MAA8B,CAAA;AACpC,YAAQ,QAAQ,CAAC,QAAQ,QAAQ;AAC/B,UAAI,MAAM,IAAI,OAAO,GAAG;AAAA,IAC1B,CAAC;AACD,SAAK,KAAK,GAAG;AAAA,EACf;AAGA,QAAM,iBAAiB,CAAC,UAAU,QAAQ,MAAM,eAAe,YAAY;AAC3E,QAAM,eACJ,QAAQ,KAAK,CAAA,MAAK,eAAe,SAAS,EAAE,YAAA,CAAa,CAAC,KAAK,QAAQ,CAAC;AAE1E,SAAO,EAAE,SAAS,SAAS,MAAM,aAAA;AACnC;AAEO,SAAS,cACd,YACA,SACA,eACA,gBACA,WACA,eAC+E;AAC/E,QAAM,kBAA4B,CAAA;AAClC,QAAM,YAAsB,CAAA;AAC5B,QAAM,oBAA8B,CAAA;AAEpC,WAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,UAAM,SAAS,eAAe,IAAI,CAAC;AACnC,QAAI,WAAW,WAAW;AACxB,sBAAgB,KAAK,WAAW,CAAC,CAAC;AAAA,IACpC,WAAW,WAAW,MAAM;AAC1B,gBAAU,KAAK,WAAW,CAAC,CAAC;AAAA,IAC9B,OAAO;AACL,wBAAkB,KAAK,WAAW,CAAC,CAAC;AAAA,IACtC;AAAA,EACF;AAGA,QAAM,+BAAe,IAAA;AACrB,QAAM,WAA0B,CAAA;AAChC,QAAM,iBAAiB,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAE9D,QAAM,cAAc,gBAAgB;AAEpC,aAAW,UAAU,mBAAmB;AACtC,UAAM,QAAQ,OAAO,MAAM,SAAS;AACpC,UAAM,UAAU,KAAK,IAAI,GAAG,MAAM,SAAS,WAAW;AACtD,UAAM,MAAM;AAAA,MACV,MAAM,MAAM,GAAG,OAAO,EAAE,KAAK,SAAS;AAAA,MACtC,GAAG,MAAM,MAAM,OAAO;AAAA,IAAA;AAIxB,UAAM,WAAqB,CAAA;AAC3B,eAAW,OAAO,gBAAgB;AAChC,UAAI,MAAM,IAAI,UAAU,MAAM,QAAQ,QAAQ;AAC5C,iBAAS,KAAK,IAAI,GAAG,CAAC;AAAA,MACxB;AAAA,IACF;AACA,UAAM,WAAW,SAAS,KAAK,KAAK;AAEpC,QAAI,CAAC,SAAS,IAAI,QAAQ,GAAG;AAC3B,eAAS,IAAI,UAAU,EAAE;AAAA,IAC3B;AACA,aAAS,IAAI,QAAQ,EAAG,KAAK,MAAM;AAGnC,UAAM,SAAiC,CAAA;AACvC,eAAW,OAAO,SAAS;AACzB,UAAI,IAAI,QAAQ,IAAI,QAAQ;AAC1B,eAAO,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK;AAAA,MAClC;AAAA,IACF;AACA,aAAS,KAAK,EAAE,YAAY,QAAQ,QAAQ,OAAO,UAAU;AAAA,EAC/D;AAGA,QAAM,SAAwB,CAAA;AAC9B,MAAI,WAAW;AACf,aAAW,CAAC,MAAM,OAAO,KAAK,UAAU;AACtC,WAAO,KAAK;AAAA,MACV;AAAA,MACA,OAAO,eAAe,WAAW,eAAe,MAAM;AAAA,MACtD;AAAA,IAAA,CACD;AACD;AAAA,EACF;AAGA,MAAI,UAAU,SAAS,GAAG;AACxB,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS;AAAA,IAAA,CACV;AACD,eAAW,UAAU,WAAW;AAC9B,eAAS,KAAK,EAAE,YAAY,QAAQ,QAAQ,CAAA,GAAI,OAAO,MAAM;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,UAAU,gBAAA;AAC7B;AAIO,SAAS,eAAe;AAC7B,QAAM,YAAY,IAAe,OAAO;AACxC,QAAM,UAAU,IAAI,EAAE;AACtB,QAAM,UAAU,IAA0B,IAAI;AAC9C,QAAM,YAAY,IAAI,GAAG;AACzB,QAAM,qBAAqB,IAAI,CAAC;AAChC,QAAM,gBAAgB,IAAI,CAAC;AAC3B,QAAM,WAAW,IAAmB,EAAE;AACtC,QAAM,SAAS,IAAkB,EAAE;AACnC,QAAM,aAAa,IAA4B,EAAE;AACjD,QAAM,gBAAgB,IAAI,oBAAI,KAAa;AAE3C,QAAM,UAAU,SAAS,MAAM;AAC7B,QAAI,UAAU,UAAU,SAAS,QAAQ,OAAO;AAC9C,aAAO,QAAQ,MAAM,KAAK,IAAI,OAAK,EAAE,QAAQ,MAAO,YAAY,CAAC;AAAA,IACnE;AACA,WAAO,QAAQ,MACZ,MAAM,IAAI,EACV,IAAI,CAAA,MAAK,EAAE,KAAA,CAAM,EACjB,OAAO,CAAA,MAAK,EAAE,SAAS,CAAC;AAAA,EAC7B,CAAC;AAED,QAAM,cAAc,SAAS,MAAM,SAAS,MAAM,SAAS,CAAC;AAE5D,QAAM,oBAAoB,SAAS,MAAM;AACvC,UAAM,iBAAiB,IAAI,IAAI,SAAS,MAAM,IAAI,CAAA,MAAK,EAAE,KAAK,CAAC;AAC/D,WAAO,QAAQ,MAAM,OAAO,CAAC,GAAG,MAAM,CAAC,eAAe,IAAI,CAAC,CAAC;AAAA,EAC9D,CAAC;AAED,QAAM,iBAAiB,SAAS,MAAM;AACpC,UAAM,0BAAU,IAAA;AAChB,eAAW,KAAK,SAAS,OAAO;AAC9B,UAAI,IAAI,EAAE,OAAO,EAAE,MAAM;AAAA,IAC3B;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,mBAAmB,SAAS,MAAM;AACtC,WAAO,OAAO,MAAM,IAAI,CAAA,SAAQ;AAAA,MAC9B,GAAG;AAAA,MACH,MAAM,WAAW,MAAM,IAAI,KAAK,KAAK,IAAI;AAAA,IAAA,EACzC;AAAA,EACJ,CAAC;AAED,QAAM,kBAAkB,SAAS,MAAM;AACrC,QAAI,iBAAiB,MAAM,WAAW,KAAK,cAAc,MAAM,SAAS,GAAG;AACzE,aAAO,EAAE,QAAQ,CAAA,GAAqB,UAAU,CAAA,GAAqB,iBAAiB,GAAC;AAAA,IACzF;AACA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,cAAc;AAAA,IAAA;AAAA,EAElB,CAAC;AAED,QAAM,SAAS,SAAS,MAAM,gBAAgB,MAAM,MAAM;AAC1D,QAAM,WAAW,SAAS,MAAM,gBAAgB,MAAM,QAAQ;AAC9D,QAAM,kBAAkB,SAAS,MAAM,gBAAgB,MAAM,eAAe;AAE5E,QAAM,SAAS,SAA0B,MAAM,gBAAgB,KAAK;AAEpE,WAAS,aAAa;AACpB,QAAI,UAAU,UAAU,SAAS,QAAQ,OAAO;AAC9C,oBAAA;AAAA,IACF,OAAO;AACL,sBAAA;AAAA,IACF;AAAA,EACF;AAEA,WAAS,kBAAkB;AACzB,UAAM,QAAQ,QAAQ;AACtB,QAAI,MAAM,WAAW,EAAG;AAExB,UAAM,WAAW,iBAAiB,KAAK;AACvC,cAAU,QAAQ,SAAS;AAC3B,uBAAmB,QAAQ,SAAS;AAIpC,aAAS,QAAQ,eAAe,OAAO,SAAS,WAAW,SAAS,kBAAkB;AAGtF,eAAW,WAAW,SAAS,OAAO;AACpC,cAAQ,SAAS,sBAAsB,QAAQ,QAAQ,SAAS,SAAS;AAAA,IAC3E;AAEA,UAAM,aAAa,MAAM;AAAA,MACvB,CAAC,GAAG,MAAM,CAAC,SAAS,MAAM,KAAK,CAAA,MAAK,EAAE,UAAU,CAAC;AAAA,IAAA;AAInD,UAAM,wBAAwB,WAAW,IAAI,CAAA,MAAK,EAAE,MAAM,SAAS,SAAS,EAAE,MAAM;AACpF,kBAAc,QAAQ,sBAAsB,SAAS,IACjD,KAAK,IAAI,GAAG,qBAAqB,IACjC,SAAS;AAEb,WAAO,QAAQ,eAAe,YAAY,SAAS,WAAW,cAAc,KAAK;AAGjF,eAAW,QAAQ,CAAA;AACnB,kBAAc,QAAQ,IAAI;AAAA,MACxB,OAAO,MAAM,OAAO,CAAA,MAAK,EAAE,cAAc,CAAC,EAAE,IAAI,CAAA,MAAK,EAAE,KAAK;AAAA,IAAA;AAAA,EAEhE;AAEA,WAAS,gBAAgB;AACvB,QAAI,CAAC,QAAQ,MAAO;AAEpB,UAAM,MAAM,QAAQ;AACpB,UAAM,gBAAgB,IAAI,QAAQ,OAAO,CAAA,MAAK,MAAM,IAAI,YAAY;AAEpE,WAAO,QAAQ,cAAc,IAAI,CAAC,KAAK,MAAM;AAC3C,YAAM,SAAS,IAAI,KAAK,IAAI,CAAA,MAAK,EAAE,GAAG,CAAC;AACvC,YAAM,SAAS,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAClC,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc;AAAA,QACd,aAAa,OAAO;AAAA,MAAA;AAAA,IAExB,CAAC;AAGD,aAAS,QAAQ,CAAA;AACjB,cAAU,QAAQ;AAClB,uBAAmB,QAAQ,IAAI,QAAQ;AAEvC,eAAW,QAAQ,CAAA;AACnB,eAAW,KAAK,OAAO,OAAO;AAC5B,iBAAW,MAAM,EAAE,KAAK,IAAI,EAAE;AAAA,IAChC;AACA,kBAAc,QAAQ,IAAI,IAAI,OAAO,MAAM,IAAI,CAAA,MAAK,EAAE,KAAK,CAAC;AAAA,EAC9D;AAEA,WAAS,iBAAiB,OAAe,QAAuB;AAC9D,UAAM,UAAU,SAAS,MAAM,KAAK,CAAA,MAAK,EAAE,UAAU,KAAK;AAC1D,QAAI,SAAS;AACX,cAAQ,SAAS;AAEjB,eAAS,QAAQ,CAAC,GAAG,SAAS,KAAK;AAAA,IACrC;AAAA,EACF;AAEA,WAAS,qBAAqB,QAAuB;AACnD,eAAW,WAAW,SAAS,OAAO;AACpC,cAAQ,SAAS;AAAA,IACnB;AACA,aAAS,QAAQ,CAAC,GAAG,SAAS,KAAK;AAAA,EACrC;AAEA,WAAS,YAAY,OAAe;AAClC,UAAM,SAAS,IAAI,IAAI,cAAc,KAAK;AAC1C,QAAI,OAAO,IAAI,KAAK,GAAG;AACrB,aAAO,OAAO,KAAK;AAAA,IACrB,OAAO;AACL,aAAO,IAAI,KAAK;AAAA,IAClB;AACA,kBAAc,QAAQ;AAAA,EACxB;AAEA,WAAS,YAAY,OAAe,MAAc;AAChD,eAAW,QAAQ,EAAE,GAAG,WAAW,OAAO,CAAC,KAAK,GAAG,KAAA;AAAA,EACrD;AAEA,WAAS,QAAQ;AACf,YAAQ,QAAQ;AAChB,YAAQ,QAAQ;AAChB,cAAU,QAAQ;AAClB,uBAAmB,QAAQ;AAC3B,kBAAc,QAAQ;AACtB,aAAS,QAAQ,CAAA;AACjB,WAAO,QAAQ,CAAA;AACf,eAAW,QAAQ,CAAA;AACnB,kBAAc,4BAAY,IAAA;AAAA,EAC5B;AAEA,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -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":"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"}
@@ -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;