@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/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.375rem 0.75rem;
10587
+ padding: 0.25rem 0.75rem;
10588
10588
  background-color: var(--bg-tertiary);
10589
10589
  border-top: 1px solid var(--border-color);
10590
- font-size: 1.125rem;
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.75rem;
10602
+ font-size: 0.6875rem;
10598
10603
  color: var(--text-muted);
10599
- padding-left: 0.125rem;
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.375rem 0.75rem;
23515
+ padding: 0.25rem 0.75rem;
23510
23516
  background-color: var(--bg-tertiary);
23511
23517
  border-top: 1px solid var(--border-color);
23512
- font-size: 1.125rem;
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.75rem;
23531
+ font-size: 0.6875rem;
23521
23532
  color: var(--text-muted);
23522
- padding-left: 0.125rem;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mld-sdk",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "MLD Platform SDK - Vue 3 components, composables, and types for plugin development",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- <template v-for="(part, i) in getParts(modelValue)" :key="i">
95
- <span v-if="part.type === 'element'">{{ part.text }}</span>
96
- <span v-else-if="part.type === 'subscript'" style="vertical-align: sub; font-size: 0.75em; line-height: 0;">{{ part.text }}</span>
97
- <span v-else-if="part.type === 'superscript'" style="vertical-align: super; font-size: 0.75em; line-height: 0;">{{ part.text }}</span>
98
- <span v-else-if="part.type === 'paren'" style="color: var(--text-secondary);">{{ part.text }}</span>
99
- <span v-else-if="part.type === 'dot'" style="margin: 0 0.125em; color: var(--text-muted);">{{ part.text }}</span>
100
- <span v-else-if="part.type === 'charge'" style="vertical-align: super; font-size: 0.75em; line-height: 0;">{{ part.text }}</span>
101
- </template>
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
- // JSME initialization
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 (!containerRef.value || props.readonly) return
154
+ if (props.readonly) return
146
155
 
147
156
  try {
148
157
  isLoading.value = true
149
158
  loadError.value = null
150
159
 
151
- // Wait for JSME to be ready
160
+ // Phase 1: load the JSME script (independent of DOM)
152
161
  await waitForJSME()
153
162
 
154
- // Wait for DOM to be ready
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
- // Get JSME constructor from window
160
- const win = getJSMEState()
174
+ // Wait for a full paint cycle so GWT can measure the container
175
+ await waitForPaint()
161
176
 
162
- if (!win.JSApplet?.JSME) {
163
- throw new Error('JSME library not available after loading')
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
- // Create JSME instance
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
- '100%',
189
+ `${width}px`,
173
190
  `${props.height}px`,
174
191
  {
175
- options: 'query,hydrogens,paste,depict',
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
@@ -112,7 +112,7 @@ watch(
112
112
  watch(
113
113
  () => props.modelValue,
114
114
  (newValue) => {
115
- if (newValue) editor.reset()
115
+ if (newValue) editor.loadState(newValue)
116
116
  }
117
117
  )
118
118
 
@@ -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
- refreshTimerId.value = window.setTimeout(() => {
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 (refreshTimerId.value !== null) {
294
- window.clearTimeout(refreshTimerId.value)
295
- refreshTimerId.value = null
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
- onMounted(() => {
389
- // Start periodic check
390
- checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)
391
- })
386
+ if (getCurrentInstance()) {
387
+ onMounted(() => {
388
+ checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)
389
+ })
392
390
 
393
- onUnmounted(() => {
394
- // Clean up on unmount
395
- stopTokenRefresh()
396
- if (checkInterval !== null) {
397
- window.clearInterval(checkInterval)
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 groups = computed(() => {
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
- ).metadata
345
+ )
361
346
  })
362
347
 
363
- const excludedSamples = computed(() => {
364
- return computeGroups(
365
- samples.value,
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: deepClone(internalState.value.plates),
113
- samples: deepClone(internalState.value.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 = deepClone(entry.plates)
261
- internalState.value.samples = deepClone(entry.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 = deepClone(entry.plates)
277
- internalState.value.samples = deepClone(entry.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
  }
@@ -33,8 +33,12 @@ function getDefaultServerHost(): string {
33
33
  }
34
34
 
35
35
  function getDefaultServerPort(): number {
36
- if (typeof window !== 'undefined' && window.location.port) {
37
- return parseInt(window.location.port, 10)
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 getApiBaseUrl(): string {
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
- if (serverHost.value === currentHost && String(serverPort.value) === currentPort) {
100
- return apiPrefix
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 currentHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
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
- let isDark = false
142
-
143
- if (theme.value === 'system') {
144
- isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
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.375rem 0.75rem;
70
+ padding: 0.25rem 0.75rem;
71
71
  background-color: var(--bg-tertiary);
72
72
  border-top: 1px solid var(--border-color);
73
- font-size: 1.125rem;
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.75rem;
87
+ font-size: 0.6875rem;
82
88
  color: var(--text-muted);
83
- padding-left: 0.125rem;
89
+ white-space: nowrap;
90
+ flex-shrink: 0;
84
91
  }
85
92
 
86
93
  .mld-formula-input__mw::before {