@morscherlab/mld-sdk 0.9.7 → 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.
- package/dist/components/FormulaInput.vue.js +24 -18
- package/dist/components/FormulaInput.vue.js.map +1 -1
- package/dist/components/MoleculeInput.vue.js +15 -6
- package/dist/components/MoleculeInput.vue.js.map +1 -1
- package/dist/components/PlateMapEditor.vue.js +1 -1
- package/dist/components/PlateMapEditor.vue.js.map +1 -1
- package/dist/composables/useAuth.js +26 -25
- package/dist/composables/useAuth.js.map +1 -1
- package/dist/composables/useAutoGroup.js +7 -32
- package/dist/composables/useAutoGroup.js.map +1 -1
- package/dist/composables/useForm.js +1 -1
- package/dist/composables/useForm.js.map +1 -1
- package/dist/composables/useWellPlateEditor.d.ts +1 -0
- package/dist/composables/useWellPlateEditor.js +21 -10
- package/dist/composables/useWellPlateEditor.js.map +1 -1
- package/dist/stores/settings.js +17 -30
- package/dist/stores/settings.js.map +1 -1
- package/dist/styles.css +24 -12
- package/package.json +1 -1
- package/src/components/FormulaInput.vue +17 -16
- package/src/components/MoleculeInput.vue +29 -14
- package/src/components/PlateMapEditor.vue +1 -1
- package/src/composables/useAuth.ts +29 -31
- package/src/composables/useAutoGroup.ts +7 -33
- package/src/composables/useForm.ts +1 -1
- package/src/composables/useWellPlateEditor.ts +22 -10
- package/src/stores/settings.ts +22 -38
- package/src/styles/components/formula-input.css +13 -6
|
@@ -6,33 +6,37 @@ const _hoisted_3 = {
|
|
|
6
6
|
key: 0,
|
|
7
7
|
class: "mld-formula-input__preview"
|
|
8
8
|
};
|
|
9
|
-
const _hoisted_4 = {
|
|
10
|
-
|
|
9
|
+
const _hoisted_4 = {
|
|
10
|
+
key: 0,
|
|
11
|
+
class: "mld-formula-input__preview-formula"
|
|
12
|
+
};
|
|
13
|
+
const _hoisted_5 = { key: 0 };
|
|
14
|
+
const _hoisted_6 = {
|
|
11
15
|
key: 1,
|
|
12
16
|
style: { "vertical-align": "sub", "font-size": "0.75em", "line-height": "0" }
|
|
13
17
|
};
|
|
14
|
-
const
|
|
18
|
+
const _hoisted_7 = {
|
|
15
19
|
key: 2,
|
|
16
20
|
style: { "vertical-align": "super", "font-size": "0.75em", "line-height": "0" }
|
|
17
21
|
};
|
|
18
|
-
const
|
|
22
|
+
const _hoisted_8 = {
|
|
19
23
|
key: 3,
|
|
20
24
|
style: { "color": "var(--text-secondary)" }
|
|
21
25
|
};
|
|
22
|
-
const
|
|
26
|
+
const _hoisted_9 = {
|
|
23
27
|
key: 4,
|
|
24
28
|
style: { "margin": "0 0.125em", "color": "var(--text-muted)" }
|
|
25
29
|
};
|
|
26
|
-
const
|
|
30
|
+
const _hoisted_10 = {
|
|
27
31
|
key: 5,
|
|
28
32
|
style: { "vertical-align": "super", "font-size": "0.75em", "line-height": "0" }
|
|
29
33
|
};
|
|
30
|
-
const _hoisted_10 = {
|
|
31
|
-
key: 0,
|
|
32
|
-
class: "mld-formula-input__mw"
|
|
33
|
-
};
|
|
34
34
|
const _hoisted_11 = {
|
|
35
35
|
key: 1,
|
|
36
|
+
class: "mld-formula-input__mw"
|
|
37
|
+
};
|
|
38
|
+
const _hoisted_12 = {
|
|
39
|
+
key: 0,
|
|
36
40
|
class: "mld-formula-input__error-text"
|
|
37
41
|
};
|
|
38
42
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
@@ -101,16 +105,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
101
105
|
"aria-label": "Chemical formula",
|
|
102
106
|
onInput: handleInput
|
|
103
107
|
}, null, 42, _hoisted_2),
|
|
104
|
-
__props.showPreview && __props.modelValue ? (openBlock(), createElementBlock("div", _hoisted_3, [
|
|
105
|
-
(openBlock(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
__props.showPreview && __props.modelValue || __props.showMW && __props.modelValue && !getError(__props.modelValue) && getMW(__props.modelValue) !== null ? (openBlock(), createElementBlock("div", _hoisted_3, [
|
|
109
|
+
__props.showPreview && __props.modelValue ? (openBlock(), createElementBlock("span", _hoisted_4, [
|
|
110
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(getParts(__props.modelValue), (part, i) => {
|
|
111
|
+
return openBlock(), createElementBlock(Fragment, { key: i }, [
|
|
112
|
+
part.type === "element" ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(part.text), 1)) : part.type === "subscript" ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(part.text), 1)) : part.type === "superscript" ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(part.text), 1)) : part.type === "paren" ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(part.text), 1)) : part.type === "dot" ? (openBlock(), createElementBlock("span", _hoisted_9, toDisplayString(part.text), 1)) : part.type === "charge" ? (openBlock(), createElementBlock("span", _hoisted_10, toDisplayString(part.text), 1)) : createCommentVNode("", true)
|
|
113
|
+
], 64);
|
|
114
|
+
}), 128))
|
|
115
|
+
])) : createCommentVNode("", true),
|
|
116
|
+
__props.showMW && __props.modelValue && !getError(__props.modelValue) && getMW(__props.modelValue) !== null ? (openBlock(), createElementBlock("span", _hoisted_11, toDisplayString(formatMW(__props.modelValue)), 1)) : createCommentVNode("", true)
|
|
110
117
|
])) : createCommentVNode("", true)
|
|
111
118
|
]),
|
|
112
|
-
__props.
|
|
113
|
-
__props.modelValue && getError(__props.modelValue) ? (openBlock(), createElementBlock("div", _hoisted_11, toDisplayString(getError(__props.modelValue)), 1)) : createCommentVNode("", true)
|
|
119
|
+
__props.modelValue && getError(__props.modelValue) ? (openBlock(), createElementBlock("div", _hoisted_12, toDisplayString(getError(__props.modelValue)), 1)) : createCommentVNode("", true)
|
|
114
120
|
], 2);
|
|
115
121
|
};
|
|
116
122
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormulaInput.vue.js","sources":["../../src/components/FormulaInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useChemicalFormula, type FormulaPart } from '../composables/useChemicalFormula'\n\ninterface Props {\n modelValue?: string\n showPreview?: boolean\n showMW?: boolean\n placeholder?: string\n error?: boolean\n disabled?: boolean\n size?: 'sm' | 'md' | 'lg'\n}\n\nwithDefaults(defineProps<Props>(), {\n modelValue: '',\n showPreview: true,\n showMW: true,\n placeholder: 'e.g. Ca(OH)2',\n error: false,\n disabled: false,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n 'mw': [mw: number | null]\n}>()\n\nconst { parseFormula, calculateMW, renderFormulaParts } = useChemicalFormula()\n\nfunction getParseResult(value: string) {\n if (!value) return null\n return parseFormula(value)\n}\n\nfunction getMW(value: string): number | null {\n const result = getParseResult(value)\n if (!result || !result.valid) return null\n return calculateMW(result.elements)\n}\n\nfunction getParts(value: string): FormulaPart[] {\n if (!value) return []\n return renderFormulaParts(value)\n}\n\nfunction getError(value: string): string | null {\n if (!value) return null\n const result = getParseResult(value)\n if (!result) return null\n if (!result.valid) return result.error || 'Invalid formula'\n return null\n}\n\nfunction formatMW(value: string): string {\n const mw = getMW(value)\n if (mw === null) return ''\n return `${mw.toFixed(2)} g/mol`\n}\n\nfunction handleInput(event: Event) {\n const target = event.target as HTMLInputElement\n emit('update:modelValue', target.value)\n emit('mw', getMW(target.value))\n}\n</script>\n\n<template>\n <div\n :class=\"[\n 'mld-formula-input',\n error ? 'mld-formula-input--error' : '',\n disabled ? 'mld-formula-input--disabled' : '',\n ]\"\n >\n <div class=\"mld-formula-input__field\">\n <input\n type=\"text\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :class=\"[\n 'mld-formula-input__input',\n `mld-formula-input__input--${size}`,\n ]\"\n aria-label=\"Chemical formula\"\n @input=\"handleInput\"\n />\n\n <div\n v-if=\"showPreview && modelValue\"\n class=\"mld-formula-input__preview\"\n >\n <template v-for=\"(part, i) in getParts(modelValue)\" :key=\"i\">\n
|
|
1
|
+
{"version":3,"file":"FormulaInput.vue.js","sources":["../../src/components/FormulaInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useChemicalFormula, type FormulaPart } from '../composables/useChemicalFormula'\n\ninterface Props {\n modelValue?: string\n showPreview?: boolean\n showMW?: boolean\n placeholder?: string\n error?: boolean\n disabled?: boolean\n size?: 'sm' | 'md' | 'lg'\n}\n\nwithDefaults(defineProps<Props>(), {\n modelValue: '',\n showPreview: true,\n showMW: true,\n placeholder: 'e.g. Ca(OH)2',\n error: false,\n disabled: false,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n 'mw': [mw: number | null]\n}>()\n\nconst { parseFormula, calculateMW, renderFormulaParts } = useChemicalFormula()\n\nfunction getParseResult(value: string) {\n if (!value) return null\n return parseFormula(value)\n}\n\nfunction getMW(value: string): number | null {\n const result = getParseResult(value)\n if (!result || !result.valid) return null\n return calculateMW(result.elements)\n}\n\nfunction getParts(value: string): FormulaPart[] {\n if (!value) return []\n return renderFormulaParts(value)\n}\n\nfunction getError(value: string): string | null {\n if (!value) return null\n const result = getParseResult(value)\n if (!result) return null\n if (!result.valid) return result.error || 'Invalid formula'\n return null\n}\n\nfunction formatMW(value: string): string {\n const mw = getMW(value)\n if (mw === null) return ''\n return `${mw.toFixed(2)} g/mol`\n}\n\nfunction handleInput(event: Event) {\n const target = event.target as HTMLInputElement\n emit('update:modelValue', target.value)\n emit('mw', getMW(target.value))\n}\n</script>\n\n<template>\n <div\n :class=\"[\n 'mld-formula-input',\n error ? 'mld-formula-input--error' : '',\n disabled ? 'mld-formula-input--disabled' : '',\n ]\"\n >\n <div class=\"mld-formula-input__field\">\n <input\n type=\"text\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :class=\"[\n 'mld-formula-input__input',\n `mld-formula-input__input--${size}`,\n ]\"\n aria-label=\"Chemical formula\"\n @input=\"handleInput\"\n />\n\n <div\n v-if=\"(showPreview && modelValue) || (showMW && modelValue && !getError(modelValue) && getMW(modelValue) !== null)\"\n class=\"mld-formula-input__preview\"\n >\n <span v-if=\"showPreview && modelValue\" class=\"mld-formula-input__preview-formula\">\n <template v-for=\"(part, i) in getParts(modelValue)\" :key=\"i\">\n <span v-if=\"part.type === 'element'\">{{ part.text }}</span>\n <span v-else-if=\"part.type === 'subscript'\" style=\"vertical-align: sub; font-size: 0.75em; line-height: 0;\">{{ part.text }}</span>\n <span v-else-if=\"part.type === 'superscript'\" style=\"vertical-align: super; font-size: 0.75em; line-height: 0;\">{{ part.text }}</span>\n <span v-else-if=\"part.type === 'paren'\" style=\"color: var(--text-secondary);\">{{ part.text }}</span>\n <span v-else-if=\"part.type === 'dot'\" style=\"margin: 0 0.125em; color: var(--text-muted);\">{{ part.text }}</span>\n <span v-else-if=\"part.type === 'charge'\" style=\"vertical-align: super; font-size: 0.75em; line-height: 0;\">{{ part.text }}</span>\n </template>\n </span>\n <span\n v-if=\"showMW && modelValue && !getError(modelValue) && getMW(modelValue) !== null\"\n class=\"mld-formula-input__mw\"\n >\n {{ formatMW(modelValue) }}\n </span>\n </div>\n </div>\n\n <div\n v-if=\"modelValue && getError(modelValue)\"\n class=\"mld-formula-input__error-text\"\n >\n {{ getError(modelValue) }}\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/formula-input.css';\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createElementVNode","_openBlock","_Fragment","_toDisplayString"],"mappings":"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"}
|
|
@@ -108,27 +108,37 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
108
108
|
});
|
|
109
109
|
return win.__jsmeLoadPromise__;
|
|
110
110
|
}
|
|
111
|
+
function waitForPaint() {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
111
116
|
async function initJSME() {
|
|
112
117
|
var _a, _b;
|
|
113
|
-
if (
|
|
118
|
+
if (props.readonly) return;
|
|
114
119
|
try {
|
|
115
120
|
isLoading.value = true;
|
|
116
121
|
loadError.value = null;
|
|
117
122
|
await waitForJSME();
|
|
118
|
-
await nextTick();
|
|
119
|
-
if (!containerRef.value) return;
|
|
120
123
|
const win = getJSMEState();
|
|
121
124
|
if (!((_a = win.JSApplet) == null ? void 0 : _a.JSME)) {
|
|
122
125
|
throw new Error("JSME library not available after loading");
|
|
123
126
|
}
|
|
127
|
+
isLoading.value = false;
|
|
128
|
+
await nextTick();
|
|
129
|
+
if (!containerRef.value) return;
|
|
130
|
+
await waitForPaint();
|
|
131
|
+
if (!containerRef.value) return;
|
|
132
|
+
const rect = containerRef.value.getBoundingClientRect();
|
|
133
|
+
const width = Math.floor(rect.width) || 400;
|
|
124
134
|
const editorId = `jsme-${Date.now()}`;
|
|
125
135
|
containerRef.value.id = editorId;
|
|
126
136
|
const instance = new win.JSApplet.JSME(
|
|
127
137
|
editorId,
|
|
128
|
-
|
|
138
|
+
`${width}px`,
|
|
129
139
|
`${props.height}px`,
|
|
130
140
|
{
|
|
131
|
-
options: "query,hydrogens,paste
|
|
141
|
+
options: "query,hydrogens,paste"
|
|
132
142
|
}
|
|
133
143
|
);
|
|
134
144
|
jsmeInstance.value = instance;
|
|
@@ -136,7 +146,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
136
146
|
instance.readMolFile(props.modelValue.molfile);
|
|
137
147
|
}
|
|
138
148
|
instance.setCallBack("AfterStructureModified", handleStructureChange);
|
|
139
|
-
isLoading.value = false;
|
|
140
149
|
} catch (err) {
|
|
141
150
|
const message = err instanceof Error ? err.message : "Failed to load molecule editor";
|
|
142
151
|
loadError.value = message;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MoleculeInput.vue.js","sources":["../../src/components/MoleculeInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'\nimport type { MoleculeData } from '../types'\n\ninterface Props {\n modelValue?: MoleculeData\n disabled?: boolean\n readonly?: boolean\n height?: number\n showSmiles?: boolean\n placeholder?: string\n error?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n readonly: false,\n height: 300,\n showSmiles: true,\n placeholder: 'Draw a chemical structure',\n error: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [data: MoleculeData | undefined]\n 'error': [message: string]\n}>()\n\n// State\nconst containerRef = ref<HTMLDivElement | null>(null)\nconst jsmeInstance = ref<unknown>(null)\nconst isLoading = ref(true)\nconst loadError = ref<string | null>(null)\nconst debounceTimer = ref<ReturnType<typeof setTimeout> | null>(null)\n\n// Computed\nconst hasStructure = computed(() => {\n return props.modelValue?.smiles && props.modelValue.smiles.length > 0\n})\n\n// Use window-level state to persist across component instances and hot-reloads\ninterface JSMEGlobalState {\n __jsmeCallbacks__?: Array<() => void>\n __jsmeLoading__?: boolean\n __jsmeLoaded__?: boolean\n __jsmeLoadPromise__?: Promise<void>\n JSApplet?: { JSME?: new (id: string, width: string, height: string, options?: object) => unknown }\n jsmeOnLoad?: () => void\n}\n\nfunction getJSMEState(): JSMEGlobalState {\n const win = window as unknown as JSMEGlobalState\n if (!win.__jsmeCallbacks__) {\n win.__jsmeCallbacks__ = []\n }\n return win\n}\n\nfunction waitForJSME(): Promise<void> {\n const win = getJSMEState()\n\n // Already loaded\n if (win.JSApplet?.JSME) {\n return Promise.resolve()\n }\n\n // Reuse existing promise if loading\n if (win.__jsmeLoadPromise__) {\n return win.__jsmeLoadPromise__\n }\n\n win.__jsmeLoadPromise__ = new Promise((resolve, reject) => {\n // Double-check after promise creation\n if (win.JSApplet?.JSME) {\n resolve()\n return\n }\n\n // Set up global callback FIRST (before checking for existing script)\n const originalOnLoad = win.jsmeOnLoad\n win.jsmeOnLoad = () => {\n win.__jsmeLoaded__ = true\n originalOnLoad?.()\n win.__jsmeCallbacks__?.forEach(cb => cb())\n win.__jsmeCallbacks__ = []\n resolve()\n }\n\n // Add to callback queue\n win.__jsmeCallbacks__?.push(resolve)\n\n // Check if script already exists\n const existingScript = document.querySelector('script[data-jsme]')\n if (existingScript) {\n // Script exists, poll for JSApplet.JSME (in case jsmeOnLoad already fired)\n const checkReady = setInterval(() => {\n if (win.JSApplet?.JSME) {\n clearInterval(checkReady)\n win.__jsmeLoaded__ = true\n win.__jsmeCallbacks__?.forEach(cb => cb())\n win.__jsmeCallbacks__ = []\n resolve()\n }\n }, 100)\n\n setTimeout(() => {\n clearInterval(checkReady)\n if (!win.__jsmeLoaded__ && !win.JSApplet?.JSME) {\n reject(new Error('JSME initialization timeout'))\n }\n }, 15000)\n return\n }\n\n // Load the script\n win.__jsmeLoading__ = true\n const script = document.createElement('script')\n script.src = 'https://jsme-editor.github.io/dist/jsme/jsme.nocache.js'\n script.integrity = 'sha384-l6tNzsc/eAJ7uql0dGAcHYI5ANVEV7DrJYjzXp3t13L+3OzLnfpzJO0Uio7mUSjY'\n script.crossOrigin = 'anonymous'\n script.async = true\n script.setAttribute('data-jsme', 'true')\n\n script.onerror = () => {\n win.__jsmeLoading__ = false\n win.__jsmeLoadPromise__ = undefined\n reject(new Error('Failed to load JSME script'))\n }\n\n // Timeout\n setTimeout(() => {\n if (!win.__jsmeLoaded__ && !win.JSApplet?.JSME) {\n reject(new Error('JSME initialization timeout'))\n }\n }, 15000)\n\n document.head.appendChild(script)\n })\n\n return win.__jsmeLoadPromise__\n}\n\n// JSME initialization\nasync function initJSME() {\n if (!containerRef.value || props.readonly) return\n\n try {\n isLoading.value = true\n loadError.value = null\n\n // Wait for JSME to be ready\n await waitForJSME()\n\n // Wait for DOM to be ready\n await nextTick()\n\n if (!containerRef.value) return\n\n // Get JSME constructor from window\n const win = getJSMEState()\n\n if (!win.JSApplet?.JSME) {\n throw new Error('JSME library not available after loading')\n }\n\n // Create JSME instance\n const editorId = `jsme-${Date.now()}`\n containerRef.value.id = editorId\n\n const instance = new win.JSApplet.JSME(\n editorId,\n '100%',\n `${props.height}px`,\n {\n options: 'query,hydrogens,paste,depict',\n }\n )\n\n jsmeInstance.value = instance\n\n // Set initial value if provided\n if (props.modelValue?.molfile) {\n (instance as { readMolFile: (mol: string) => void }).readMolFile(props.modelValue.molfile)\n }\n\n // Set up change callback\n (instance as { setCallBack: (event: string, callback: () => void) => void }).setCallBack('AfterStructureModified', handleStructureChange)\n\n isLoading.value = false\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load molecule editor'\n loadError.value = message\n emit('error', message)\n isLoading.value = false\n }\n}\n\nfunction handleStructureChange() {\n // Debounce the change event\n if (debounceTimer.value) {\n clearTimeout(debounceTimer.value)\n }\n\n debounceTimer.value = setTimeout(() => {\n if (!jsmeInstance.value) return\n\n const instance = jsmeInstance.value as {\n smiles: () => string\n molFile: () => string\n }\n\n const smiles = instance.smiles()\n const molfile = instance.molFile()\n\n if (!smiles || smiles.length === 0) {\n emit('update:modelValue', undefined)\n } else {\n emit('update:modelValue', { smiles, molfile })\n }\n }, 300)\n}\n\nfunction clearStructure() {\n if (jsmeInstance.value) {\n (jsmeInstance.value as { reset: () => void }).reset()\n }\n emit('update:modelValue', undefined)\n}\n\n// Watch for external value changes\nwatch(() => props.modelValue, (newValue) => {\n if (!jsmeInstance.value) return\n\n const instance = jsmeInstance.value as {\n smiles: () => string\n readMolFile: (mol: string) => void\n reset: () => void\n }\n\n // Only update if the external value differs from current\n const currentSmiles = instance.smiles()\n if (newValue?.smiles !== currentSmiles) {\n if (newValue?.molfile) {\n instance.readMolFile(newValue.molfile)\n } else {\n instance.reset()\n }\n }\n})\n\n// Lifecycle\nonMounted(() => {\n if (!props.readonly) {\n initJSME()\n } else {\n isLoading.value = false\n }\n})\n\nonUnmounted(() => {\n if (debounceTimer.value) {\n clearTimeout(debounceTimer.value)\n }\n jsmeInstance.value = null\n})\n</script>\n\n<template>\n <div\n :class=\"[\n 'mld-molecule-input',\n disabled ? 'mld-molecule-input--disabled' : '',\n readonly ? 'mld-molecule-input--readonly' : '',\n error ? 'mld-molecule-input--error' : '',\n ]\"\n >\n <!-- Loading state -->\n <div\n v-if=\"isLoading && !readonly\"\n class=\"mld-molecule-input__skeleton\"\n :style=\"{ height: `${height}px` }\"\n >\n <svg\n class=\"mld-molecule-input__skeleton-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-molecule-input__skeleton-text\">Loading molecule editor...</span>\n </div>\n\n <!-- Error state -->\n <div\n v-else-if=\"loadError\"\n class=\"mld-molecule-input__error\"\n >\n <svg\n class=\"mld-molecule-input__error-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n {{ loadError }}\n </div>\n\n <!-- Readonly mode - show empty or placeholder -->\n <template v-else-if=\"readonly\">\n <div\n v-if=\"hasStructure\"\n class=\"mld-molecule-input__readonly\"\n :style=\"{ height: `${height}px` }\"\n >\n <!-- In readonly mode, we just display a placeholder since we don't have SVG rendering -->\n <div class=\"mld-molecule-input__empty\">\n <svg\n class=\"mld-molecule-input__empty-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-molecule-input__empty-text\">Structure defined (readonly)</span>\n </div>\n </div>\n <div\n v-else\n class=\"mld-molecule-input__empty\"\n :style=\"{ height: `${height}px` }\"\n >\n <svg\n class=\"mld-molecule-input__empty-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-molecule-input__empty-text\">No structure</span>\n </div>\n </template>\n\n <!-- Editor mode -->\n <template v-else>\n <div\n ref=\"containerRef\"\n class=\"mld-molecule-input__editor\"\n :style=\"{ height: `${height}px` }\"\n role=\"application\"\n aria-label=\"Molecule structure editor\"\n />\n\n <!-- Actions toolbar -->\n <div class=\"mld-molecule-input__actions\">\n <button\n type=\"button\"\n class=\"mld-molecule-input__action-btn\"\n :disabled=\"!hasStructure || disabled\"\n aria-label=\"Clear structure\"\n @click=\"clearStructure\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n </button>\n </div>\n </template>\n\n <!-- SMILES display -->\n <div\n v-if=\"showSmiles && hasStructure && !loadError\"\n class=\"mld-molecule-input__smiles\"\n >\n <span class=\"mld-molecule-input__smiles-label\">SMILES:</span>\n {{ modelValue?.smiles }}\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/molecule-input.css';\n</style>\n"],"names":["_a","_b","_createElementBlock","_normalizeClass","_createElementVNode","_openBlock","_createTextVNode","_Fragment","_toDisplayString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MoleculeInput.vue.js","sources":["../../src/components/MoleculeInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'\nimport type { MoleculeData } from '../types'\n\ninterface Props {\n modelValue?: MoleculeData\n disabled?: boolean\n readonly?: boolean\n height?: number\n showSmiles?: boolean\n placeholder?: string\n error?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n readonly: false,\n height: 300,\n showSmiles: true,\n placeholder: 'Draw a chemical structure',\n error: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [data: MoleculeData | undefined]\n 'error': [message: string]\n}>()\n\n// State\nconst containerRef = ref<HTMLDivElement | null>(null)\nconst jsmeInstance = ref<unknown>(null)\nconst isLoading = ref(true)\nconst loadError = ref<string | null>(null)\nconst debounceTimer = ref<ReturnType<typeof setTimeout> | null>(null)\n\n// Computed\nconst hasStructure = computed(() => {\n return props.modelValue?.smiles && props.modelValue.smiles.length > 0\n})\n\n// Use window-level state to persist across component instances and hot-reloads\ninterface JSMEGlobalState {\n __jsmeCallbacks__?: Array<() => void>\n __jsmeLoading__?: boolean\n __jsmeLoaded__?: boolean\n __jsmeLoadPromise__?: Promise<void>\n JSApplet?: { JSME?: new (id: string, width: string, height: string, options?: object) => unknown }\n jsmeOnLoad?: () => void\n}\n\nfunction getJSMEState(): JSMEGlobalState {\n const win = window as unknown as JSMEGlobalState\n if (!win.__jsmeCallbacks__) {\n win.__jsmeCallbacks__ = []\n }\n return win\n}\n\nfunction waitForJSME(): Promise<void> {\n const win = getJSMEState()\n\n // Already loaded\n if (win.JSApplet?.JSME) {\n return Promise.resolve()\n }\n\n // Reuse existing promise if loading\n if (win.__jsmeLoadPromise__) {\n return win.__jsmeLoadPromise__\n }\n\n win.__jsmeLoadPromise__ = new Promise((resolve, reject) => {\n // Double-check after promise creation\n if (win.JSApplet?.JSME) {\n resolve()\n return\n }\n\n // Set up global callback FIRST (before checking for existing script)\n const originalOnLoad = win.jsmeOnLoad\n win.jsmeOnLoad = () => {\n win.__jsmeLoaded__ = true\n originalOnLoad?.()\n win.__jsmeCallbacks__?.forEach(cb => cb())\n win.__jsmeCallbacks__ = []\n resolve()\n }\n\n // Add to callback queue\n win.__jsmeCallbacks__?.push(resolve)\n\n // Check if script already exists\n const existingScript = document.querySelector('script[data-jsme]')\n if (existingScript) {\n // Script exists, poll for JSApplet.JSME (in case jsmeOnLoad already fired)\n const checkReady = setInterval(() => {\n if (win.JSApplet?.JSME) {\n clearInterval(checkReady)\n win.__jsmeLoaded__ = true\n win.__jsmeCallbacks__?.forEach(cb => cb())\n win.__jsmeCallbacks__ = []\n resolve()\n }\n }, 100)\n\n setTimeout(() => {\n clearInterval(checkReady)\n if (!win.__jsmeLoaded__ && !win.JSApplet?.JSME) {\n reject(new Error('JSME initialization timeout'))\n }\n }, 15000)\n return\n }\n\n // Load the script\n win.__jsmeLoading__ = true\n const script = document.createElement('script')\n script.src = 'https://jsme-editor.github.io/dist/jsme/jsme.nocache.js'\n script.integrity = 'sha384-l6tNzsc/eAJ7uql0dGAcHYI5ANVEV7DrJYjzXp3t13L+3OzLnfpzJO0Uio7mUSjY'\n script.crossOrigin = 'anonymous'\n script.async = true\n script.setAttribute('data-jsme', 'true')\n\n script.onerror = () => {\n win.__jsmeLoading__ = false\n win.__jsmeLoadPromise__ = undefined\n reject(new Error('Failed to load JSME script'))\n }\n\n // Timeout\n setTimeout(() => {\n if (!win.__jsmeLoaded__ && !win.JSApplet?.JSME) {\n reject(new Error('JSME initialization timeout'))\n }\n }, 15000)\n\n document.head.appendChild(script)\n })\n\n return win.__jsmeLoadPromise__\n}\n\n// Wait for the browser to complete a paint cycle\nfunction waitForPaint(): Promise<void> {\n return new Promise(resolve => {\n requestAnimationFrame(() => requestAnimationFrame(() => resolve()))\n })\n}\n\n// JSME initialization — two phases:\n// 1. Load the script (no DOM needed)\n// 2. Reveal the editor div, wait for paint, then mount JSME with explicit px dimensions\nasync function initJSME() {\n if (props.readonly) return\n\n try {\n isLoading.value = true\n loadError.value = null\n\n // Phase 1: load the JSME script (independent of DOM)\n await waitForJSME()\n\n const win = getJSMEState()\n if (!win.JSApplet?.JSME) {\n throw new Error('JSME library not available after loading')\n }\n\n // Phase 2: reveal the editor div by clearing the loading state\n isLoading.value = false\n await nextTick()\n\n if (!containerRef.value) return\n\n // Wait for a full paint cycle so GWT can measure the container\n await waitForPaint()\n\n if (!containerRef.value) return\n\n // Use explicit pixel width — GWT cannot resolve percentage widths reliably\n const rect = containerRef.value.getBoundingClientRect()\n const width = Math.floor(rect.width) || 400\n\n // Mount JSME into the now-painted container\n const editorId = `jsme-${Date.now()}`\n containerRef.value.id = editorId\n\n const instance = new win.JSApplet.JSME(\n editorId,\n `${width}px`,\n `${props.height}px`,\n {\n options: 'query,hydrogens,paste',\n }\n )\n\n jsmeInstance.value = instance\n\n // Set initial value if provided\n if (props.modelValue?.molfile) {\n (instance as { readMolFile: (mol: string) => void }).readMolFile(props.modelValue.molfile)\n }\n\n // Set up change callback\n (instance as { setCallBack: (event: string, callback: () => void) => void }).setCallBack('AfterStructureModified', handleStructureChange)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load molecule editor'\n loadError.value = message\n emit('error', message)\n isLoading.value = false\n }\n}\n\nfunction handleStructureChange() {\n // Debounce the change event\n if (debounceTimer.value) {\n clearTimeout(debounceTimer.value)\n }\n\n debounceTimer.value = setTimeout(() => {\n if (!jsmeInstance.value) return\n\n const instance = jsmeInstance.value as {\n smiles: () => string\n molFile: () => string\n }\n\n const smiles = instance.smiles()\n const molfile = instance.molFile()\n\n if (!smiles || smiles.length === 0) {\n emit('update:modelValue', undefined)\n } else {\n emit('update:modelValue', { smiles, molfile })\n }\n }, 300)\n}\n\nfunction clearStructure() {\n if (jsmeInstance.value) {\n (jsmeInstance.value as { reset: () => void }).reset()\n }\n emit('update:modelValue', undefined)\n}\n\n// Watch for external value changes\nwatch(() => props.modelValue, (newValue) => {\n if (!jsmeInstance.value) return\n\n const instance = jsmeInstance.value as {\n smiles: () => string\n readMolFile: (mol: string) => void\n reset: () => void\n }\n\n // Only update if the external value differs from current\n const currentSmiles = instance.smiles()\n if (newValue?.smiles !== currentSmiles) {\n if (newValue?.molfile) {\n instance.readMolFile(newValue.molfile)\n } else {\n instance.reset()\n }\n }\n})\n\n// Lifecycle\nonMounted(() => {\n if (!props.readonly) {\n initJSME()\n } else {\n isLoading.value = false\n }\n})\n\nonUnmounted(() => {\n if (debounceTimer.value) {\n clearTimeout(debounceTimer.value)\n }\n jsmeInstance.value = null\n})\n</script>\n\n<template>\n <div\n :class=\"[\n 'mld-molecule-input',\n disabled ? 'mld-molecule-input--disabled' : '',\n readonly ? 'mld-molecule-input--readonly' : '',\n error ? 'mld-molecule-input--error' : '',\n ]\"\n >\n <!-- Loading state -->\n <div\n v-if=\"isLoading && !readonly\"\n class=\"mld-molecule-input__skeleton\"\n :style=\"{ height: `${height}px` }\"\n >\n <svg\n class=\"mld-molecule-input__skeleton-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-molecule-input__skeleton-text\">Loading molecule editor...</span>\n </div>\n\n <!-- Error state -->\n <div\n v-else-if=\"loadError\"\n class=\"mld-molecule-input__error\"\n >\n <svg\n class=\"mld-molecule-input__error-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n {{ loadError }}\n </div>\n\n <!-- Readonly mode - show empty or placeholder -->\n <template v-else-if=\"readonly\">\n <div\n v-if=\"hasStructure\"\n class=\"mld-molecule-input__readonly\"\n :style=\"{ height: `${height}px` }\"\n >\n <!-- In readonly mode, we just display a placeholder since we don't have SVG rendering -->\n <div class=\"mld-molecule-input__empty\">\n <svg\n class=\"mld-molecule-input__empty-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-molecule-input__empty-text\">Structure defined (readonly)</span>\n </div>\n </div>\n <div\n v-else\n class=\"mld-molecule-input__empty\"\n :style=\"{ height: `${height}px` }\"\n >\n <svg\n class=\"mld-molecule-input__empty-icon\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-molecule-input__empty-text\">No structure</span>\n </div>\n </template>\n\n <!-- Editor mode -->\n <template v-else>\n <div\n ref=\"containerRef\"\n class=\"mld-molecule-input__editor\"\n :style=\"{ height: `${height}px` }\"\n role=\"application\"\n aria-label=\"Molecule structure editor\"\n />\n\n <!-- Actions toolbar -->\n <div class=\"mld-molecule-input__actions\">\n <button\n type=\"button\"\n class=\"mld-molecule-input__action-btn\"\n :disabled=\"!hasStructure || disabled\"\n aria-label=\"Clear structure\"\n @click=\"clearStructure\"\n >\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n </button>\n </div>\n </template>\n\n <!-- SMILES display -->\n <div\n v-if=\"showSmiles && hasStructure && !loadError\"\n class=\"mld-molecule-input__smiles\"\n >\n <span class=\"mld-molecule-input__smiles-label\">SMILES:</span>\n {{ modelValue?.smiles }}\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/molecule-input.css';\n</style>\n"],"names":["_a","_b","_createElementBlock","_normalizeClass","_createElementVNode","_openBlock","_createTextVNode","_Fragment","_toDisplayString"],"mappings":"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"}
|
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASA,UAAM,cAA4C;AAAA,MAChD,GAAG;AAAA;AAAA,MACH,GAAG;AAAA;AAAA,MACH,GAAG;AAAA;AAAA,MACH,GAAG;AAAA;AAAA,IAAA;AAEL,UAAM,aAA6B,CAAC,KAAK,KAAK,KAAK,GAAG;AActD,UAAM,QAAQ;AAYd,UAAM,OAAO;AAYb,UAAM,SAAS,mBAAmB,MAAM,YAAY;AAAA,MAClD,eAAe,MAAM;AAAA,IAAA,CACtB;AAED,UAAM,gBAAgB,IAAI,EAAE;AAC5B,UAAM,kBAAkB,IAAI,KAAK;AACjC,UAAM,aAAa,IAAI,EAAE;AACzB,UAAM,eAAe,IAAoB,MAAM;AAG/C,UAAM,aAAa,IAA+B,oBAAI,KAAK;AAG3D,aAAS,aAAa,SAAiB,YAAkC;AACvE,UAAI,CAAC,WAAW,MAAM,IAAI,OAAO,GAAG;AAClC,mBAAW,MAAM,IAAI,SAAS,WAAW,aAAa,WAAW,MAAM,CAAC;AAAA,MAC1E;AACA,aAAO,WAAW,MAAM,IAAI,OAAO;AAAA,IACrC;AAEA,UAAM,eAAe,SAAS,MAAM;AAClC,YAAM,SAAiC,CAAA;AACvC,iBAAW,UAAU,OAAO,QAAQ,OAAO;AACzC,YAAI,OAAO,OAAO;AAChB,iBAAO,OAAO,EAAE,IAAI,OAAO;AAAA,QAC7B;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,YAAY,SAAS,MAAM;AAC/B,YAAM,QAAQ,OAAO,YAAY;AACjC,UAAI,CAAC,MAAO,QAAO,CAAA;AAEnB,YAAM,QAAuC,CAAA;AAC7C,iBAAW,CAAC,QAAQ,IAAI,KAAK,OAAO,QAAQ,MAAM,KAAK,GAAG;AACxD,cAAM,MAAM,IAAI;AAAA,UACd,OAAO,KAAK;AAAA,UACZ,YAAY,KAAK;AAAA,UACjB,OAAO,KAAK;AAAA,QAAA;AAAA,MAEhB;AACA,aAAO;AAAA,IACT,CAAC;AAGD,aAAS,kBAAkB,SAAyB;AAClD,YAAM,QAAQ,OAAO,OAAO,MAAM,KAAK,CAAA,MAAK,EAAE,OAAO,OAAO;AAC5D,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO,OAAO,OAAO,MAAM,KAAK,EAAE,OAAO,CAAA,MAAK,EAAE,UAAU,EAAE;AAAA,IAC9D;AAEA;AAAA,MACE,MAAM,OAAO,MAAM;AAAA,MACnB,CAAC,aAAa,KAAK,qBAAqB,EAAE,GAAG,UAAU;AAAA,MACvD,EAAE,MAAM,KAAA;AAAA,IAAK;AAGf;AAAA,MACE,MAAM,MAAM;AAAA,MACZ,CAAC,aAAa;AACZ,YAAI,SAAU,QAAO,UAAU,QAAQ;AAAA,MACzC;AAAA,IAAA;AAGF,aAAS,sBAAsB,SAAmB;AAChD,aAAO,iBAAiB,OAAO;AAAA,IACjC;AAEA,aAAS,kBAAkB,QAAoB;AAC7C,YAAM,cAAc,OAAO,eAAe,UAAU,OAAO,KAAK,SAAY,OAAO;AACnF,aAAO,gBAAgB,WAAW;AAAA,IACpC;AAEA,aAAS,qBAAqB;AAC5B,YAAM,QAAQ,OAAO,cAAc;AACnC,UAAI,MAAM,WAAW,EAAG;AAExB,aAAO,aAAa,OAAO,OAAO,eAAe,KAAK;AACtD,WAAK,iBAAiB,OAAO,OAAO,eAAe,KAAK;AAAA,IAC1D;AAEA,aAAS,mBAAmB;AAC1B,YAAM,QAAQ,OAAO,cAAc;AACnC,UAAI,MAAM,WAAW,EAAG;AAExB,aAAO,WAAW,KAAK;AACvB,WAAK,eAAe,KAAK;AAAA,IAC3B;AAEA,aAAS,kBAAkB;AACzB,UAAI,CAAC,cAAc,MAAM,OAAQ;AACjC,aAAO,UAAU,cAAc,MAAM,KAAA,CAAM;AAC3C,oBAAc,QAAQ;AAAA,IACxB;AAEA,aAAS,mBAAmB,UAAkB;AAC5C,aAAO,aAAa,QAAQ;AAAA,IAC9B;AAEA,aAAS,iBAAiB;AACxB,UAAI,OAAO,OAAO,MAAM,UAAU,MAAM,UAAW;AACnD,YAAM,QAAQ,OAAO,SAAA;AACrB,WAAK,aAAa,EAAE,IAAI,MAAM,IAAI,MAAM,MAAM,MAAM;AAAA,IACtD;AAEA,aAAS,kBAAkB,SAAiB;AAC1C,aAAO,YAAY,OAAO;AAC1B,iBAAW,MAAM,OAAO,OAAO;AAC/B,WAAK,gBAAgB,OAAO;AAAA,IAC9B;AAEA,aAAS,aAAa;AACpB,aAAO,KAAA;AACP,WAAK,MAAM;AAAA,IACb;AAEA,aAAS,aAAa;AACpB,aAAO,KAAA;AACP,WAAK,MAAM;AAAA,IACb;AAEA,aAAS,aAAa,QAAwB;AAC5C,YAAM,OAAO,OAAO,WAAW,MAAM;AACrC,WAAK,UAAU,MAAM,MAAM;AAE3B,YAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAA0B,oBAAiC;AAC3F,YAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,YAAM,IAAI,SAAS,cAAc,GAAG;AACpC,QAAE,OAAO;AACT,QAAE,WAAW,aAAa,MAAM;AAChC,QAAE,MAAA;AACF,UAAI,gBAAgB,GAAG;AAAA,IACzB;AAEA,aAAS,eAAe;AACtB,YAAM,UAAU,OAAO,WAAW,WAAW,OAAO,aAAa,KAAK;AACtE,WAAK,UAAU,OAAO;AACtB,UAAI,SAAS;AACX,wBAAgB,QAAQ;AACxB,mBAAW,QAAQ;AAAA,MACrB;AAAA,IACF;AAEA,aAAS,cAAc,OAAsB;AAC3C,YAAM,UAAU,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ;AACjE,UAAI,QAAQ;AACV,cAAM,eAAA;AACN,cAAM,WAAW,WAAA,IAAe,WAAA;AAChC;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,QAAQ,YAAY,MAAM,QAAQ;AACzD,UAAI,YAAY,OAAO,cAAc,MAAM,SAAS,GAAG;AACrD,cAAM,eAAA;AACN,yBAAA;AACA;AAAA,MACF;AAEA,YAAM,MAAM,SAAS,MAAM,GAAG;AAC9B,YAAM,mBAAmB,OAAO,KAAK,OAAO,KAAK,OAAO,QAAQ,MAAM,UAAU;AAChF,UAAI,kBAAkB;AACpB,eAAO,gBAAgB,OAAO,QAAQ,MAAM,MAAM,CAAC,EAAE,EAAE;AACvD,YAAI,OAAO,cAAc,MAAM,SAAS,GAAG;AACzC,6BAAA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,cAAU,MAAM;AACd,eAAS,iBAAiB,WAAW,aAAa;AAAA,IACpD,CAAC;AAED,gBAAY,MAAM;AAChB,eAAS,oBAAoB,WAAW,aAAa;AAAA,IACvD,CAAC;;;0BAICA,mBAkRM,OAAA;AAAA,QAlRA,+EAAgE,QAAA,aAAW,CAAA;AAAA,MAAA;QAE/EC,mBA0KM,OA1KN,YA0KM;AAAA,UAxKO,QAAA,eAAXC,UAAA,GAAAF,mBAoGM,OApGN,YAoGM;AAAA,YAlGJC,mBA4CM,OA5CN,YA4CM;AAAA,gCA3CJD,mBA6BSG,UAAA,MAAAC,WA5BkBC,cAAO,OAAO,OAAK,CAApC,OAAO,UAAK;;oCADtBL,mBA6BS,UAAA;AAAA,kBA3BN,KAAK,MAAM;AAAA,kBACZ,MAAK;AAAA,kBACJ,OAAKM,eAAA,CAAA,yBAAA,EAAA,iCAA+D,MAAM,SAAOD,MAAAA,MAAA,MAAA,EAAO,YAAY,UAAnBA,gBAAAA,IAA0B,IAAA,CAAE,CAAA;AAAA,kBAC7G,qBAAOA,MAAA,MAAA,EAAO,eAAe,MAAM,EAAE;AAAA,gBAAA;kBAEtCJ,mBAGE,QAAA;AAAA,oBAFA,OAAM;AAAA,oBACL,OAAKM,eAAA,EAAA,iBAAqB,YAAY,aAAa,MAAM,IAAI,KAAK,CAAA,EAAA,CAAA;AAAA,kBAAA;kBAErEN,mBAAgE,QAAhE,YAAgEO,gBAApB,MAAM,IAAI,GAAA,CAAA;AAAA,kBAE9C,kBAAkB,MAAM,EAAE,IAAA,KADlCN,aAAAF,mBAKO,QALP,YAKOQ,gBADF,kBAAkB,MAAM,EAAE,CAAA,GAAA,CAAA;kBAGvBH,MAAA,MAAA,EAAO,OAAO,MAAM,SAAM,kBADlCL,mBAUS,UAAA;AAAA;oBARP,MAAK;AAAA,oBACL,OAAM;AAAA,oBACL,cAAU,UAAY,MAAM,IAAI;AAAA,oBAChC,SAAKS,cAAA,CAAA,WAAO,kBAAkB,MAAM,EAAE,GAAA,CAAA,MAAA,CAAA;AAAA,kBAAA;oBAEvCR,mBAEM,OAAA;AAAA,sBAFD,MAAK;AAAA,sBAAO,QAAO;AAAA,sBAAe,SAAQ;AAAA,sBAAY,gBAAa;AAAA,sBAAI,kBAAe;AAAA,sBAAQ,mBAAgB;AAAA,oBAAA;sBACjHA,mBAAuB,QAAA,EAAjB,GAAE,cAAY;AAAA,sBAAGA,mBAAuB,QAAA,EAAjB,GAAE,cAAY;AAAA,oBAAA;;;;cAMzC,QAAA,kBAAkBI,cAAO,OAAO,MAAM,SAAS,QAAA,0BADvDL,mBAWS,UAAA;AAAA;gBATP,MAAK;AAAA,gBACL,OAAM;AAAA,gBACN,cAAW;AAAA,gBACV,SAAO;AAAA,cAAA;gBAERC,mBAEM,OAAA;AAAA,kBAFD,MAAK;AAAA,kBAAO,QAAO;AAAA,kBAAe,SAAQ;AAAA,kBAAY,gBAAa;AAAA,kBAAI,kBAAe;AAAA,kBAAQ,mBAAgB;AAAA,gBAAA;kBACjHA,mBAAqB,QAAA,EAAf,GAAE,YAAU;AAAA,kBAAGA,mBAAqB,QAAA,EAAf,GAAE,YAAU;AAAA,gBAAA;gBAEzCA,mBAAgB,cAAV,OAAG,EAAA;AAAA,cAAA;;wCAIbA,mBAAwC,OAAA,EAAnC,OAAM,2BAAA,GAA0B,MAAA,EAAA;AAAA,YAGrCA,mBAgDM,OAhDN,YAgDM;AAAA,cA/CJA,mBAUS,UAAA;AAAA,gBATP,MAAK;AAAA,gBACJ,UAAQ,CAAGI,MAAA,MAAA,EAAO,QAAQ;AAAA,gBAC3B,OAAM;AAAA,gBACN,OAAM;AAAA,gBACL,SAAO;AAAA,cAAA;gBAERJ,mBAEM,OAAA;AAAA,kBAFD,MAAK;AAAA,kBAAO,QAAO;AAAA,kBAAe,SAAQ;AAAA,kBAAY,gBAAa;AAAA,kBAAI,kBAAe;AAAA,kBAAQ,mBAAgB;AAAA,gBAAA;kBACjHA,mBAA0B,QAAA,EAApB,GAAE,iBAAe;AAAA,kBAAGA,mBAAqE,QAAA,EAA/D,GAAE,4DAA0D;AAAA,gBAAA;;cAIhGA,mBAUS,UAAA;AAAA,gBATP,MAAK;AAAA,gBACJ,UAAQ,CAAGI,MAAA,MAAA,EAAO,QAAQ;AAAA,gBAC3B,OAAM;AAAA,gBACN,OAAM;AAAA,gBACL,SAAO;AAAA,cAAA;gBAERJ,mBAEM,OAAA;AAAA,kBAFD,MAAK;AAAA,kBAAO,QAAO;AAAA,kBAAe,SAAQ;AAAA,kBAAY,gBAAa;AAAA,kBAAI,kBAAe;AAAA,kBAAQ,mBAAgB;AAAA,gBAAA;kBACjHA,mBAA2B,QAAA,EAArB,GAAE,kBAAgB;AAAA,kBAAGA,mBAAmE,QAAA,EAA7D,GAAE,0DAAwD;AAAA,gBAAA;;0CAI/FA,mBAAyC,OAAA,EAApC,OAAM,4BAAA,GAA2B,MAAA,EAAA;AAAA,cAEtCA,mBASS,UAAA;AAAA,gBARP,MAAK;AAAA,gBACL,OAAM;AAAA,gBACN,OAAM;AAAA,gBACL,+CAAO,gBAAA,QAAe;AAAA,cAAA;gBAEvBA,mBAEM,OAAA;AAAA,kBAFD,MAAK;AAAA,kBAAO,QAAO;AAAA,kBAAe,SAAQ;AAAA,kBAAY,gBAAa;AAAA,kBAAI,kBAAe;AAAA,kBAAQ,mBAAgB;AAAA,gBAAA;kBACjHA,mBAAqB,QAAA,EAAf,GAAE,YAAU;AAAA,kBAAGA,mBAA0B,QAAA,EAApB,GAAE,iBAAe;AAAA,kBAAGA,mBAAsD,QAAA,EAAhD,GAAE,6CAA2C;AAAA,gBAAA;;cAItGA,mBASS,UAAA;AAAA,gBARP,MAAK;AAAA,gBACL,OAAM;AAAA,gBACN,OAAM;AAAA,gBACL,+CAAO,aAAY,MAAA;AAAA,cAAA;gBAEpBA,mBAEM,OAAA;AAAA,kBAFD,MAAK;AAAA,kBAAO,QAAO;AAAA,kBAAe,SAAQ;AAAA,kBAAY,gBAAa;AAAA,kBAAI,kBAAe;AAAA,kBAAQ,mBAAgB;AAAA,gBAAA;kBACjHA,mBAAqB,QAAA,EAAf,GAAE,YAAU;AAAA,kBAAGA,mBAA0B,QAAA,EAApB,GAAE,iBAAe;AAAA,kBAAGA,mBAAsD,QAAA,EAAhD,GAAE,6CAA2C;AAAA,gBAAA;;;;UAQlGI,MAAA,MAAA,EAAO,YAAY,sBAD3BK,YAUEC,aAAA;AAAA;YARC,eAAaN,MAAA,MAAA,EAAO,cAAc;AAAA,YAClC,QAAQA,MAAA,MAAA,EAAO,YAAY,MAAM;AAAA,YACjC,OAAO,UAAA;AAAA,YACP,iBAAe,aAAA;AAAA,YACf,MAAM,QAAA;AAAA,YACP,kBAAe;AAAA,YACf,8BAAA;AAAA,YACC,uBAAoB;AAAA,UAAA;UAKfA,MAAA,MAAA,EAAO,cAAc,MAAM,SAAM,KADzCH,UAAA,GAAAF,mBAuBM,OAvBN,aAuBM;AAAA,YAnBJC,mBAEO,QAFP,aAEO;AAAA,cADLA,mBAAwD,gCAA7CI,MAAA,MAAA,EAAO,cAAc,MAAM,MAAM,GAAA,CAAA;AAAA,0DAAY,oBAC1D,EAAA;AAAA,YAAA;wCACAJ,mBAAwC,OAAA,EAAnC,OAAM,2BAAA,GAA0B,MAAA,EAAA;AAAA,YAE7BI,MAAA,MAAA,EAAO,eAAe,sBAD9BL,mBAOS,UAAA;AAAA;cALP,MAAK;AAAA,cACL,OAAM;AAAA,cACL,SAAO;AAAA,YAAA,GACT,aACQQ,iBAAGH,mBAAO,QAAQ,MAAM,KAAK,CAAA,MAAK,EAAE,OAAOA,MAAA,MAAA,EAAO,eAAe,KAAK,MAAnEA,mBAAsE,IAAI,GAAA,CAAA;YAEtFJ,mBAMS,UAAA;AAAA,cALP,MAAK;AAAA,cACL,OAAM;AAAA,cACL,SAAO;AAAA,YAAA,GACT,SAED;AAAA,UAAA;;;QAiCO,QAAA,eAAXC,UAAA,GAAAF,mBA8CM,OA9CN,aA8CM;AAAA,UA7CJC,mBA4CM,OA5CN,aA4CM;AAAA,YA3CJ,OAAA,EAAA,MAAA,OAAA,EAAA,IAAAA,mBAA6D,MAAA,EAAzD,OAAM,kCAAA,GAAkC,gBAAY,EAAA;AAAA,YAExDW,YAQEC,aAAA;AAAA,cAPC,eAAaR,MAAA,MAAA,EAAO,eAAe;AAAA,cACnC,SAASA,MAAA,MAAA,EAAO,QAAQ;AAAA,cACxB,UAAU,QAAA;AAAA,cACV,MAAM,QAAA,SAAI,OAAA,OAAA;AAAA,cACV,uBAAkB,OAAA,CAAA,MAAA,OAAA,CAAA,IAAA,CAAA,WAAEA,MAAA,MAAA,EAAO,gBAAgB,MAAM;AAAA,cACjD,eAAc;AAAA,cACd,gBAAe;AAAA,YAAA;YAIP,QAAA,mBAAXH,UAAA,GAAAF,mBAkBM,OAlBN,aAkBM;AAAA,cAjBJC,mBAgBM,OAhBN,aAgBM;AAAA,+BAfJA,mBAME,SAAA;AAAA,+EALS,cAAa,QAAA;AAAA,kBACtB,MAAK;AAAA,kBACL,aAAY;AAAA,kBACZ,OAAM;AAAA,kBACL,kBAAa,iBAAe,CAAA,OAAA,CAAA;AAAA,gBAAA;+BAJpB,cAAA,KAAa;AAAA,gBAAA;gBAMxBA,mBAOS,UAAA;AAAA,kBANP,MAAK;AAAA,kBACJ,UAAQ,CAAG,cAAA,MAAc,KAAA;AAAA,kBAC1B,OAAM;AAAA,kBACL,SAAO;AAAA,gBAAA,GACT,SAED,GAAA,WAAA;AAAA,cAAA;;;;;sBAkBRS,YAiDWI,UAAA,EAjDD,IAAG,UAAM;AAAA,UAET,gBAAA,sBADRd,mBA+CM,OAAA;AAAA;YA7CJ,OAAM;AAAA,YACL,6DAAY,gBAAA,QAAe,OAAA,CAAA,MAAA,CAAA;AAAA,UAAA;YAE5BC,mBAyCM,OAzCN,aAyCM;AAAA,cAxCJ,OAAA,EAAA,MAAA,OAAA,EAAA,IAAAA,mBAA+D,MAAA,EAA3D,OAAM,gCAAA,GAAgC,oBAAgB,EAAA;AAAA,cAE1DA,mBASM,OATN,aASM;AAAA,gBARJ,OAAA,EAAA,MAAA,OAAA,EAAA,IAAAA,mBAA2D,SAAA,EAApD,OAAM,gCAAA,GAAgC,UAAM,EAAA;AAAA,+BACnDA,mBAMS,UAAA;AAAA,+EALE,aAAY,QAAA;AAAA,kBACrB,OAAM;AAAA,gBAAA;kBAENA,mBAAkC,UAAA,EAA1B,OAAM,OAAA,GAAO,QAAI,EAAA;AAAA,kBACzBA,mBAAgC,UAAA,EAAxB,OAAM,MAAA,GAAM,OAAG,EAAA;AAAA,gBAAA;iCAJd,aAAA,KAAY;AAAA,gBAAA;;cAQzBA,mBAQM,OARN,aAQM;AAAA,gBAPJ,OAAA,EAAA,MAAA,OAAA,EAAA,IAAAA,mBAAyD,SAAA,EAAlD,OAAM,gCAAA,GAAgC,QAAI,EAAA;AAAA,+BACjDA,mBAKE,YAAA;AAAA,+EAJS,WAAU,QAAA;AAAA,kBACnB,MAAK;AAAA,kBACL,OAAM;AAAA,kBACN,aAAY;AAAA,gBAAA;+BAHH,WAAA,KAAU;AAAA,gBAAA;;cAOvBA,mBAgBM,OAhBN,aAgBM;AAAA,gBAfJA,mBAMS,UAAA;AAAA,kBALP,MAAK;AAAA,kBACL,OAAM;AAAA,kBACL,+CAAO,gBAAA,QAAe;AAAA,gBAAA,GACxB,UAED;AAAA,gBACAA,mBAOS,UAAA;AAAA,kBANP,MAAK;AAAA,kBACJ,UAAQ,CAAG,WAAA,MAAW,KAAA;AAAA,kBACvB,OAAM;AAAA,kBACL,SAAO;AAAA,gBAAA,GACT,YAED,GAAA,WAAA;AAAA,cAAA;;;;;;;;"}
|
|
@@ -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
|
-
|
|
172
|
+
_refreshTimerId = window.setTimeout(() => {
|
|
174
173
|
refreshToken();
|
|
175
174
|
}, delay);
|
|
176
175
|
}
|
|
177
176
|
function stopTokenRefresh() {
|
|
178
|
-
if (
|
|
179
|
-
window.clearTimeout(
|
|
180
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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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"}
|