@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
package/dist/styles.css
CHANGED
|
@@ -10582,21 +10582,27 @@ html.dark .mld-settings-modal__option-btn--active {
|
|
|
10582
10582
|
cursor: not-allowed;
|
|
10583
10583
|
background-color: var(--bg-tertiary);
|
|
10584
10584
|
}
|
|
10585
|
-
/* Preview */
|
|
10585
|
+
/* Preview row */
|
|
10586
10586
|
.mld-formula-input__preview {
|
|
10587
|
-
padding: 0.
|
|
10587
|
+
padding: 0.25rem 0.75rem;
|
|
10588
10588
|
background-color: var(--bg-tertiary);
|
|
10589
10589
|
border-top: 1px solid var(--border-color);
|
|
10590
|
-
|
|
10591
|
-
min-height: 1.75rem;
|
|
10590
|
+
min-height: 1.5rem;
|
|
10592
10591
|
display: flex;
|
|
10593
10592
|
align-items: center;
|
|
10593
|
+
justify-content: space-between;
|
|
10594
|
+
gap: 0.75rem;
|
|
10595
|
+
}
|
|
10596
|
+
.mld-formula-input__preview-formula {
|
|
10597
|
+
font-size: 1rem;
|
|
10598
|
+
line-height: 1.4;
|
|
10594
10599
|
}
|
|
10595
10600
|
/* Molecular weight display */
|
|
10596
10601
|
.mld-formula-input__mw {
|
|
10597
|
-
font-size: 0.
|
|
10602
|
+
font-size: 0.6875rem;
|
|
10598
10603
|
color: var(--text-muted);
|
|
10599
|
-
|
|
10604
|
+
white-space: nowrap;
|
|
10605
|
+
flex-shrink: 0;
|
|
10600
10606
|
}
|
|
10601
10607
|
.mld-formula-input__mw::before {
|
|
10602
10608
|
content: 'MW: ';
|
|
@@ -23504,22 +23510,28 @@ to {
|
|
|
23504
23510
|
background-color: var(--bg-tertiary);
|
|
23505
23511
|
}
|
|
23506
23512
|
|
|
23507
|
-
/* Preview */
|
|
23513
|
+
/* Preview row */
|
|
23508
23514
|
.mld-formula-input__preview {
|
|
23509
|
-
padding: 0.
|
|
23515
|
+
padding: 0.25rem 0.75rem;
|
|
23510
23516
|
background-color: var(--bg-tertiary);
|
|
23511
23517
|
border-top: 1px solid var(--border-color);
|
|
23512
|
-
|
|
23513
|
-
min-height: 1.75rem;
|
|
23518
|
+
min-height: 1.5rem;
|
|
23514
23519
|
display: flex;
|
|
23515
23520
|
align-items: center;
|
|
23521
|
+
justify-content: space-between;
|
|
23522
|
+
gap: 0.75rem;
|
|
23523
|
+
}
|
|
23524
|
+
.mld-formula-input__preview-formula {
|
|
23525
|
+
font-size: 1rem;
|
|
23526
|
+
line-height: 1.4;
|
|
23516
23527
|
}
|
|
23517
23528
|
|
|
23518
23529
|
/* Molecular weight display */
|
|
23519
23530
|
.mld-formula-input__mw {
|
|
23520
|
-
font-size: 0.
|
|
23531
|
+
font-size: 0.6875rem;
|
|
23521
23532
|
color: var(--text-muted);
|
|
23522
|
-
|
|
23533
|
+
white-space: nowrap;
|
|
23534
|
+
flex-shrink: 0;
|
|
23523
23535
|
}
|
|
23524
23536
|
.mld-formula-input__mw::before {
|
|
23525
23537
|
content: 'MW: ';
|
package/package.json
CHANGED
|
@@ -88,27 +88,28 @@ function handleInput(event: Event) {
|
|
|
88
88
|
/>
|
|
89
89
|
|
|
90
90
|
<div
|
|
91
|
-
v-if="showPreview && modelValue"
|
|
91
|
+
v-if="(showPreview && modelValue) || (showMW && modelValue && !getError(modelValue) && getMW(modelValue) !== null)"
|
|
92
92
|
class="mld-formula-input__preview"
|
|
93
93
|
>
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
<span v-if="showPreview && modelValue" class="mld-formula-input__preview-formula">
|
|
95
|
+
<template v-for="(part, i) in getParts(modelValue)" :key="i">
|
|
96
|
+
<span v-if="part.type === 'element'">{{ part.text }}</span>
|
|
97
|
+
<span v-else-if="part.type === 'subscript'" style="vertical-align: sub; font-size: 0.75em; line-height: 0;">{{ part.text }}</span>
|
|
98
|
+
<span v-else-if="part.type === 'superscript'" style="vertical-align: super; font-size: 0.75em; line-height: 0;">{{ part.text }}</span>
|
|
99
|
+
<span v-else-if="part.type === 'paren'" style="color: var(--text-secondary);">{{ part.text }}</span>
|
|
100
|
+
<span v-else-if="part.type === 'dot'" style="margin: 0 0.125em; color: var(--text-muted);">{{ part.text }}</span>
|
|
101
|
+
<span v-else-if="part.type === 'charge'" style="vertical-align: super; font-size: 0.75em; line-height: 0;">{{ part.text }}</span>
|
|
102
|
+
</template>
|
|
103
|
+
</span>
|
|
104
|
+
<span
|
|
105
|
+
v-if="showMW && modelValue && !getError(modelValue) && getMW(modelValue) !== null"
|
|
106
|
+
class="mld-formula-input__mw"
|
|
107
|
+
>
|
|
108
|
+
{{ formatMW(modelValue) }}
|
|
109
|
+
</span>
|
|
102
110
|
</div>
|
|
103
111
|
</div>
|
|
104
112
|
|
|
105
|
-
<div
|
|
106
|
-
v-if="showMW && modelValue && !getError(modelValue) && getMW(modelValue) !== null"
|
|
107
|
-
class="mld-formula-input__mw"
|
|
108
|
-
>
|
|
109
|
-
{{ formatMW(modelValue) }}
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
113
|
<div
|
|
113
114
|
v-if="modelValue && getError(modelValue)"
|
|
114
115
|
class="mld-formula-input__error-text"
|
|
@@ -140,39 +140,56 @@ function waitForJSME(): Promise<void> {
|
|
|
140
140
|
return win.__jsmeLoadPromise__
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
//
|
|
143
|
+
// Wait for the browser to complete a paint cycle
|
|
144
|
+
function waitForPaint(): Promise<void> {
|
|
145
|
+
return new Promise(resolve => {
|
|
146
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// JSME initialization — two phases:
|
|
151
|
+
// 1. Load the script (no DOM needed)
|
|
152
|
+
// 2. Reveal the editor div, wait for paint, then mount JSME with explicit px dimensions
|
|
144
153
|
async function initJSME() {
|
|
145
|
-
if (
|
|
154
|
+
if (props.readonly) return
|
|
146
155
|
|
|
147
156
|
try {
|
|
148
157
|
isLoading.value = true
|
|
149
158
|
loadError.value = null
|
|
150
159
|
|
|
151
|
-
//
|
|
160
|
+
// Phase 1: load the JSME script (independent of DOM)
|
|
152
161
|
await waitForJSME()
|
|
153
162
|
|
|
154
|
-
|
|
163
|
+
const win = getJSMEState()
|
|
164
|
+
if (!win.JSApplet?.JSME) {
|
|
165
|
+
throw new Error('JSME library not available after loading')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Phase 2: reveal the editor div by clearing the loading state
|
|
169
|
+
isLoading.value = false
|
|
155
170
|
await nextTick()
|
|
156
171
|
|
|
157
172
|
if (!containerRef.value) return
|
|
158
173
|
|
|
159
|
-
//
|
|
160
|
-
|
|
174
|
+
// Wait for a full paint cycle so GWT can measure the container
|
|
175
|
+
await waitForPaint()
|
|
161
176
|
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
if (!containerRef.value) return
|
|
178
|
+
|
|
179
|
+
// Use explicit pixel width — GWT cannot resolve percentage widths reliably
|
|
180
|
+
const rect = containerRef.value.getBoundingClientRect()
|
|
181
|
+
const width = Math.floor(rect.width) || 400
|
|
165
182
|
|
|
166
|
-
//
|
|
183
|
+
// Mount JSME into the now-painted container
|
|
167
184
|
const editorId = `jsme-${Date.now()}`
|
|
168
185
|
containerRef.value.id = editorId
|
|
169
186
|
|
|
170
187
|
const instance = new win.JSApplet.JSME(
|
|
171
188
|
editorId,
|
|
172
|
-
|
|
189
|
+
`${width}px`,
|
|
173
190
|
`${props.height}px`,
|
|
174
191
|
{
|
|
175
|
-
options: 'query,hydrogens,paste
|
|
192
|
+
options: 'query,hydrogens,paste',
|
|
176
193
|
}
|
|
177
194
|
)
|
|
178
195
|
|
|
@@ -185,8 +202,6 @@ async function initJSME() {
|
|
|
185
202
|
|
|
186
203
|
// Set up change callback
|
|
187
204
|
(instance as { setCallBack: (event: string, callback: () => void) => void }).setCallBack('AfterStructureModified', handleStructureChange)
|
|
188
|
-
|
|
189
|
-
isLoading.value = false
|
|
190
205
|
} catch (err) {
|
|
191
206
|
const message = err instanceof Error ? err.message : 'Failed to load molecule editor'
|
|
192
207
|
loadError.value = message
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
|
-
import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue'
|
|
2
|
+
import { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'
|
|
3
3
|
import { useAuthStore } from '../stores/auth'
|
|
4
4
|
import { useSettingsStore } from '../stores/settings'
|
|
5
5
|
import type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'
|
|
@@ -66,8 +66,6 @@ export function useAuth(): UseAuthReturn {
|
|
|
66
66
|
const authStore = useAuthStore()
|
|
67
67
|
const settingsStore = useSettingsStore()
|
|
68
68
|
|
|
69
|
-
// Track refresh timer (module-level to prevent timer multiplication)
|
|
70
|
-
const refreshTimerId = ref<number | null>(_refreshTimerId)
|
|
71
69
|
const isRefreshing = ref(false)
|
|
72
70
|
|
|
73
71
|
function getApiBaseUrl(): string {
|
|
@@ -281,7 +279,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
281
279
|
|
|
282
280
|
// Schedule refresh
|
|
283
281
|
const delay = refreshAt - now
|
|
284
|
-
|
|
282
|
+
_refreshTimerId = window.setTimeout(() => {
|
|
285
283
|
refreshToken()
|
|
286
284
|
}, delay)
|
|
287
285
|
}
|
|
@@ -290,9 +288,9 @@ export function useAuth(): UseAuthReturn {
|
|
|
290
288
|
* Stop automatic token refresh.
|
|
291
289
|
*/
|
|
292
290
|
function stopTokenRefresh(): void {
|
|
293
|
-
if (
|
|
294
|
-
window.clearTimeout(
|
|
295
|
-
|
|
291
|
+
if (_refreshTimerId !== null) {
|
|
292
|
+
window.clearTimeout(_refreshTimerId)
|
|
293
|
+
_refreshTimerId = null
|
|
296
294
|
}
|
|
297
295
|
}
|
|
298
296
|
|
|
@@ -382,34 +380,34 @@ export function useAuth(): UseAuthReturn {
|
|
|
382
380
|
}
|
|
383
381
|
}
|
|
384
382
|
|
|
385
|
-
// Set up periodic check as safety net
|
|
383
|
+
// Set up periodic check as safety net (only inside component setup)
|
|
386
384
|
let checkInterval: number | null = null
|
|
387
385
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
386
|
+
if (getCurrentInstance()) {
|
|
387
|
+
onMounted(() => {
|
|
388
|
+
checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)
|
|
389
|
+
})
|
|
392
390
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
checkInterval = null
|
|
399
|
-
}
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
// Watch for token changes to reschedule refresh
|
|
403
|
-
watch(
|
|
404
|
-
() => authStore.tokenExpires,
|
|
405
|
-
(newExpires) => {
|
|
406
|
-
if (newExpires) {
|
|
407
|
-
scheduleTokenRefresh()
|
|
408
|
-
} else {
|
|
409
|
-
stopTokenRefresh()
|
|
391
|
+
onUnmounted(() => {
|
|
392
|
+
stopTokenRefresh()
|
|
393
|
+
if (checkInterval !== null) {
|
|
394
|
+
window.clearInterval(checkInterval)
|
|
395
|
+
checkInterval = null
|
|
410
396
|
}
|
|
411
|
-
}
|
|
412
|
-
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Watch for token changes to reschedule refresh
|
|
400
|
+
watch(
|
|
401
|
+
() => authStore.tokenExpires,
|
|
402
|
+
(newExpires) => {
|
|
403
|
+
if (newExpires) {
|
|
404
|
+
scheduleTokenRefresh()
|
|
405
|
+
} else {
|
|
406
|
+
stopTokenRefresh()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
}
|
|
413
411
|
|
|
414
412
|
return {
|
|
415
413
|
// Core auth methods
|
|
@@ -331,24 +331,9 @@ export function useAutoGroup() {
|
|
|
331
331
|
}))
|
|
332
332
|
})
|
|
333
333
|
|
|
334
|
-
const
|
|
334
|
+
const _computedResult = computed(() => {
|
|
335
335
|
if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
|
|
336
|
-
return []
|
|
337
|
-
}
|
|
338
|
-
const result = computeGroups(
|
|
339
|
-
samples.value,
|
|
340
|
-
effectiveColumns.value,
|
|
341
|
-
enabledFields.value,
|
|
342
|
-
outlierActions.value,
|
|
343
|
-
delimiter.value,
|
|
344
|
-
minFieldCount.value,
|
|
345
|
-
)
|
|
346
|
-
return result.groups
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
const metadata = computed(() => {
|
|
350
|
-
if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
|
|
351
|
-
return []
|
|
336
|
+
return { groups: [] as SampleGroup[], metadata: [] as MetadataRow[], excludedSamples: [] as string[] }
|
|
352
337
|
}
|
|
353
338
|
return computeGroups(
|
|
354
339
|
samples.value,
|
|
@@ -357,25 +342,14 @@ export function useAutoGroup() {
|
|
|
357
342
|
outlierActions.value,
|
|
358
343
|
delimiter.value,
|
|
359
344
|
minFieldCount.value,
|
|
360
|
-
)
|
|
345
|
+
)
|
|
361
346
|
})
|
|
362
347
|
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
effectiveColumns.value,
|
|
367
|
-
enabledFields.value,
|
|
368
|
-
outlierActions.value,
|
|
369
|
-
delimiter.value,
|
|
370
|
-
minFieldCount.value,
|
|
371
|
-
).excludedSamples
|
|
372
|
-
})
|
|
348
|
+
const groups = computed(() => _computedResult.value.groups)
|
|
349
|
+
const metadata = computed(() => _computedResult.value.metadata)
|
|
350
|
+
const excludedSamples = computed(() => _computedResult.value.excludedSamples)
|
|
373
351
|
|
|
374
|
-
const result = computed<AutoGroupResult>(() =>
|
|
375
|
-
groups: groups.value,
|
|
376
|
-
metadata: metadata.value,
|
|
377
|
-
excludedSamples: excludedSamples.value,
|
|
378
|
-
}))
|
|
352
|
+
const result = computed<AutoGroupResult>(() => _computedResult.value)
|
|
379
353
|
|
|
380
354
|
function parseInput() {
|
|
381
355
|
if (inputMode.value === 'csv' && csvData.value) {
|
|
@@ -354,7 +354,7 @@ export function useForm<T extends Record<string, unknown>>(
|
|
|
354
354
|
function reset(values?: Partial<T>): void {
|
|
355
355
|
const resetValues = values ? { ..._initialValues, ...values } : _initialValues
|
|
356
356
|
for (const key of Object.keys(data)) {
|
|
357
|
-
;(data as Record<string, unknown>)[key] = resetValues[key as keyof T]
|
|
357
|
+
;(data as Record<string, unknown>)[key] = structuredClone(resetValues[key as keyof T])
|
|
358
358
|
errors[key] = null
|
|
359
359
|
touched[key] = false
|
|
360
360
|
dirty[key] = false
|
|
@@ -52,6 +52,7 @@ export interface UseWellPlateEditorReturn {
|
|
|
52
52
|
redo: () => void
|
|
53
53
|
exportData: (format: 'json' | 'csv') => string
|
|
54
54
|
importData: (data: string, format: 'json' | 'csv') => boolean
|
|
55
|
+
loadState: (state: Partial<PlateMapEditorState>) => void
|
|
55
56
|
reset: () => void
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -103,14 +104,10 @@ export function useWellPlateEditor(
|
|
|
103
104
|
const canUndo = computed(() => historyIndex.value >= 0)
|
|
104
105
|
const canRedo = computed(() => historyIndex.value < history.value.length - 1)
|
|
105
106
|
|
|
106
|
-
function deepClone<T>(obj: T): T {
|
|
107
|
-
return structuredClone(obj)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
107
|
function saveToHistory() {
|
|
111
108
|
const entry: HistoryEntry = {
|
|
112
|
-
plates:
|
|
113
|
-
samples:
|
|
109
|
+
plates: structuredClone(internalState.value.plates),
|
|
110
|
+
samples: structuredClone(internalState.value.samples),
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
if (historyIndex.value < history.value.length - 1) {
|
|
@@ -257,8 +254,8 @@ export function useWellPlateEditor(
|
|
|
257
254
|
const entry = history.value[historyIndex.value]
|
|
258
255
|
historyIndex.value--
|
|
259
256
|
|
|
260
|
-
internalState.value.plates =
|
|
261
|
-
internalState.value.samples =
|
|
257
|
+
internalState.value.plates = structuredClone(entry.plates)
|
|
258
|
+
internalState.value.samples = structuredClone(entry.samples)
|
|
262
259
|
|
|
263
260
|
const activePlateExists = internalState.value.plates.some(p => p.id === internalState.value.activePlateId)
|
|
264
261
|
if (!activePlateExists) {
|
|
@@ -273,8 +270,8 @@ export function useWellPlateEditor(
|
|
|
273
270
|
historyIndex.value++
|
|
274
271
|
const entry = history.value[historyIndex.value]
|
|
275
272
|
|
|
276
|
-
internalState.value.plates =
|
|
277
|
-
internalState.value.samples =
|
|
273
|
+
internalState.value.plates = structuredClone(entry.plates)
|
|
274
|
+
internalState.value.samples = structuredClone(entry.samples)
|
|
278
275
|
internalState.value.selectedWells = []
|
|
279
276
|
}
|
|
280
277
|
|
|
@@ -368,6 +365,20 @@ export function useWellPlateEditor(
|
|
|
368
365
|
}
|
|
369
366
|
}
|
|
370
367
|
|
|
368
|
+
function loadState(state: Partial<PlateMapEditorState>) {
|
|
369
|
+
saveToHistory()
|
|
370
|
+
if (state.plates && state.plates.length > 0) {
|
|
371
|
+
internalState.value.plates = structuredClone(state.plates)
|
|
372
|
+
internalState.value.activePlateId = state.activePlateId ?? state.plates[0].id
|
|
373
|
+
}
|
|
374
|
+
if (state.samples) {
|
|
375
|
+
internalState.value.samples = structuredClone(state.samples)
|
|
376
|
+
}
|
|
377
|
+
internalState.value.selectedWells = state.selectedWells ?? []
|
|
378
|
+
internalState.value.activeSampleId = state.activeSampleId
|
|
379
|
+
updateSampleCounts()
|
|
380
|
+
}
|
|
381
|
+
|
|
371
382
|
function reset() {
|
|
372
383
|
const plate = createEmptyPlate('Plate 1', defaultFormat)
|
|
373
384
|
internalState.value = {
|
|
@@ -403,6 +414,7 @@ export function useWellPlateEditor(
|
|
|
403
414
|
redo,
|
|
404
415
|
exportData,
|
|
405
416
|
importData,
|
|
417
|
+
loadState,
|
|
406
418
|
reset,
|
|
407
419
|
}
|
|
408
420
|
}
|
package/src/stores/settings.ts
CHANGED
|
@@ -33,8 +33,12 @@ function getDefaultServerHost(): string {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function getDefaultServerPort(): number {
|
|
36
|
-
if (typeof window !== 'undefined'
|
|
37
|
-
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
if (window.location.port) {
|
|
38
|
+
return parseInt(window.location.port, 10)
|
|
39
|
+
}
|
|
40
|
+
// Standard ports: 443 for HTTPS, 80 for HTTP (browser omits from location.port)
|
|
41
|
+
return window.location.protocol === 'https:' ? 443 : 80
|
|
38
42
|
}
|
|
39
43
|
return 8000
|
|
40
44
|
}
|
|
@@ -92,34 +96,26 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
92
96
|
// API prefix - can be configured via env variable VITE_API_PREFIX
|
|
93
97
|
const apiPrefix: string = (import.meta.env?.VITE_API_PREFIX as string | undefined) ?? '/api'
|
|
94
98
|
|
|
95
|
-
function
|
|
99
|
+
function _isSameOrigin(): { same: boolean; currentHost: string; currentPort: string } {
|
|
96
100
|
const currentHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
|
|
97
101
|
const currentPort = typeof window !== 'undefined' ? window.location.port : '8000'
|
|
102
|
+
const effectivePort = currentPort || (window.location.protocol === 'https:' ? '443' : '80')
|
|
103
|
+
const same =
|
|
104
|
+
(serverHost.value === currentHost && String(serverPort.value) === effectivePort) ||
|
|
105
|
+
(serverHost.value === 'localhost' && currentHost !== 'localhost')
|
|
106
|
+
return { same, currentHost, currentPort }
|
|
107
|
+
}
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (serverHost.value === 'localhost' && currentHost !== 'localhost') {
|
|
104
|
-
return apiPrefix
|
|
105
|
-
}
|
|
106
|
-
|
|
109
|
+
function getApiBaseUrl(): string {
|
|
110
|
+
const { same } = _isSameOrigin()
|
|
111
|
+
if (same) return apiPrefix
|
|
107
112
|
return `http://${serverHost.value}:${serverPort.value}${apiPrefix}`
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
function getWsBaseUrl(): string {
|
|
111
|
-
const
|
|
112
|
-
const currentPort = typeof window !== 'undefined' ? window.location.port : '8000'
|
|
116
|
+
const { same, currentHost, currentPort } = _isSameOrigin()
|
|
113
117
|
const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
114
|
-
|
|
115
|
-
if (serverHost.value === currentHost && String(serverPort.value) === currentPort) {
|
|
116
|
-
return `${protocol}//${currentHost}:${currentPort}${apiPrefix}`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (serverHost.value === 'localhost' && currentHost !== 'localhost') {
|
|
120
|
-
return `${protocol}//${currentHost}:${currentPort}${apiPrefix}`
|
|
121
|
-
}
|
|
122
|
-
|
|
118
|
+
if (same) return `${protocol}//${currentHost}${currentPort ? ':' + currentPort : ''}${apiPrefix}`
|
|
123
119
|
return `ws://${serverHost.value}:${serverPort.value}${apiPrefix}`
|
|
124
120
|
}
|
|
125
121
|
|
|
@@ -138,19 +134,10 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
function applyTheme() {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
} else {
|
|
146
|
-
isDark = theme.value === 'dark'
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (isDark) {
|
|
150
|
-
document.documentElement.classList.add('dark')
|
|
151
|
-
} else {
|
|
152
|
-
document.documentElement.classList.remove('dark')
|
|
153
|
-
}
|
|
137
|
+
const dark = theme.value === 'system'
|
|
138
|
+
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
139
|
+
: theme.value === 'dark'
|
|
140
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
154
141
|
}
|
|
155
142
|
|
|
156
143
|
watch(theme, () => {
|
|
@@ -206,7 +193,6 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
206
193
|
}
|
|
207
194
|
|
|
208
195
|
return {
|
|
209
|
-
// State
|
|
210
196
|
serverHost,
|
|
211
197
|
serverPort,
|
|
212
198
|
requestTimeout,
|
|
@@ -215,8 +201,6 @@ export const useSettingsStore = defineStore('mld-settings', () => {
|
|
|
215
201
|
theme,
|
|
216
202
|
colorPalette,
|
|
217
203
|
tableDensity,
|
|
218
|
-
|
|
219
|
-
// Actions
|
|
220
204
|
initialize,
|
|
221
205
|
applyTheme,
|
|
222
206
|
persistSettings,
|
|
@@ -65,22 +65,29 @@
|
|
|
65
65
|
background-color: var(--bg-tertiary);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/* Preview */
|
|
68
|
+
/* Preview row */
|
|
69
69
|
.mld-formula-input__preview {
|
|
70
|
-
padding: 0.
|
|
70
|
+
padding: 0.25rem 0.75rem;
|
|
71
71
|
background-color: var(--bg-tertiary);
|
|
72
72
|
border-top: 1px solid var(--border-color);
|
|
73
|
-
|
|
74
|
-
min-height: 1.75rem;
|
|
73
|
+
min-height: 1.5rem;
|
|
75
74
|
display: flex;
|
|
76
75
|
align-items: center;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
gap: 0.75rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mld-formula-input__preview-formula {
|
|
81
|
+
font-size: 1rem;
|
|
82
|
+
line-height: 1.4;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/* Molecular weight display */
|
|
80
86
|
.mld-formula-input__mw {
|
|
81
|
-
font-size: 0.
|
|
87
|
+
font-size: 0.6875rem;
|
|
82
88
|
color: var(--text-muted);
|
|
83
|
-
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
flex-shrink: 0;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
.mld-formula-input__mw::before {
|