@morscherlab/mint-sdk 1.4.0-beta.11 → 1.4.0-beta.13
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/{AlertBox-B2Bgr3ei.js → AlertBox-BdoZqANv.js} +2 -2
- package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-BdoZqANv.js.map} +1 -1
- package/dist/{AppSidebar-mvm--GxN.js → AppSidebar-CIXJBzDv.js} +237 -70
- package/dist/{AppSidebar-mvm--GxN.js.map → AppSidebar-CIXJBzDv.js.map} +1 -1
- package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-CbD7BR_s.js} +2 -2
- package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-CbD7BR_s.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-BbFtr1Fr.js} +112 -96
- package/dist/ExperimentSelectorModal-BbFtr1Fr.js.map +1 -0
- package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-BkwYOKpG.js} +2 -2
- package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-BkwYOKpG.js.map} +1 -1
- package/dist/{SettingsModal-DO3IZ7qz.js → SettingsModal-CdhFMi8j.js} +25 -24
- package/dist/SettingsModal-CdhFMi8j.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
- package/dist/components/AppTopBar.vue.d.ts +13 -5
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-BF5g4wRC.js} +2 -2
- package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-BF5g4wRC.js.map} +1 -1
- package/dist/{data-CgILxDLW.js → data-DUt0TIp5.js} +10 -10
- package/dist/{data-CgILxDLW.js.map → data-DUt0TIp5.js.map} +1 -1
- package/dist/data.js +7 -7
- package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
- package/dist/experiment-DyO54FQB.js.map +1 -0
- package/dist/index.js +18 -18
- package/dist/{install-mB8-IyvO.js → install-Czoe1rA8.js} +13 -13
- package/dist/{install-mB8-IyvO.js.map → install-Czoe1rA8.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-D5bIX426.js → lab-B-pLwc5U.js} +10 -10
- package/dist/{lab-D5bIX426.js.map → lab-B-pLwc5U.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B6gpM_3e.js → records-DnK_1Q_S.js} +2 -2
- package/dist/{records-B6gpM_3e.js.map → records-DnK_1Q_S.js.map} +1 -1
- package/dist/{runtime-D7vKpfbF.js → runtime-fPb_FEZr.js} +9 -9
- package/dist/{runtime-D7vKpfbF.js.map → runtime-fPb_FEZr.js.map} +1 -1
- package/dist/runtime.js +7 -7
- package/dist/{shell-CywENwBT.js → shell-lDf_EY-x.js} +4 -4
- package/dist/{shell-CywENwBT.js.map → shell-lDf_EY-x.js.map} +1 -1
- package/dist/shell.js +4 -4
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +2 -2
- package/dist/stores/settings.d.ts +4 -12
- package/dist/styles.css +409 -60
- package/dist/types/index.d.ts +1 -1
- package/dist/types/jobs.d.ts +9 -0
- package/dist/types/platform.d.ts +1 -1
- package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-BGN1dmcR.js} +4 -4
- package/dist/{useExperimentSamples-DHnxKFAF.js.map → useExperimentSamples-BGN1dmcR.js.map} +1 -1
- package/dist/{useListReorder-BitdUhxn.js → useListReorder-DPynGrCj.js} +3 -3
- package/dist/{useListReorder-BitdUhxn.js.map → useListReorder-DPynGrCj.js.map} +1 -1
- package/dist/{useRackEditor-Q8xO-cCy.js → useRackEditor-B2FrQA0p.js} +7 -7
- package/dist/{useRackEditor-Q8xO-cCy.js.map → useRackEditor-B2FrQA0p.js.map} +1 -1
- package/dist/{useRealtime-C3iy9QkU.js → useRealtime-yWjTTnc9.js} +2 -2
- package/dist/{useRealtime-C3iy9QkU.js.map → useRealtime-yWjTTnc9.js.map} +1 -1
- package/dist/{useRequest-D506eHi8.js → useRequest-Dqw12k2s.js} +3 -3
- package/dist/{useRequest-D506eHi8.js.map → useRequest-Dqw12k2s.js.map} +1 -1
- package/package.json +1 -1
- package/src/__stories__/experiment-helpers.ts +3 -3
- package/src/__tests__/components/AppTopBar.test.ts +175 -1
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/stores/settings.test.ts +21 -21
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +107 -8
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/SettingsModal.vue +7 -12
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/stores/index.ts +0 -2
- package/src/stores/settings.ts +20 -50
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +155 -0
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/styles/variables.css +121 -15
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
- package/dist/SettingsModal-DO3IZ7qz.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
package/src/styles/variables.css
CHANGED
|
@@ -51,13 +51,8 @@
|
|
|
51
51
|
where --text-secondary would drop to ~4.35:1. */
|
|
52
52
|
--text-secondary-strong: #475569;
|
|
53
53
|
|
|
54
|
-
/* Brand colors
|
|
55
|
-
|
|
56
|
-
--color-primary: #0D9488;
|
|
57
|
-
--color-primary-hover: #0F766E;
|
|
58
|
-
--color-primary-pressed: #115E59;
|
|
59
|
-
--color-primary-soft: rgba(13, 148, 136, 0.10);
|
|
60
|
-
--color-primary-border: #99F6E4;
|
|
54
|
+
/* Brand colors: the accent (--color-primary*) comes from the palette
|
|
55
|
+
blocks below the dark block (html[data-palette], default teal). */
|
|
61
56
|
/* Fixed teal scale, the same in both themes. */
|
|
62
57
|
--color-primary-50: #F0FDFA;
|
|
63
58
|
--color-primary-100: #CCFBF1;
|
|
@@ -175,7 +170,7 @@
|
|
|
175
170
|
/* Soft focus rings — 3px translucent. Used by all form primitives
|
|
176
171
|
post-0.16 visual refresh. Solid variants above kept for legacy
|
|
177
172
|
components that haven't migrated yet. */
|
|
178
|
-
--focus-ring-soft: 0 0 0 3px
|
|
173
|
+
--focus-ring-soft: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
|
|
179
174
|
/* Red is perceptually brighter than teal at equal alpha; drop 2pp to keep visual weight matched. */
|
|
180
175
|
--focus-ring-soft-error: 0 0 0 3px rgba(239, 68, 68, 0.18);
|
|
181
176
|
|
|
@@ -213,12 +208,7 @@ html.dark {
|
|
|
213
208
|
--border-light: #172033;
|
|
214
209
|
--border-strong: #334155;
|
|
215
210
|
|
|
216
|
-
/* Brand colors - Dark mode
|
|
217
|
-
--color-primary: #2DD4BF;
|
|
218
|
-
--color-primary-hover: #5EEAD4;
|
|
219
|
-
--color-primary-pressed: #99F6E4;
|
|
220
|
-
--color-primary-soft: rgba(45, 212, 191, 0.12);
|
|
221
|
-
--color-primary-border: rgba(45, 212, 191, 0.35);
|
|
211
|
+
/* Brand colors - Dark mode: the accent comes from the palette blocks below. */
|
|
222
212
|
/* Dark ink on the light accent fill (10.1:1 on #2DD4BF). */
|
|
223
213
|
--text-on-accent: #0B1120;
|
|
224
214
|
--color-experiment-hover: #E2E8F0;
|
|
@@ -254,7 +244,7 @@ html.dark {
|
|
|
254
244
|
|
|
255
245
|
/* Focus ring - dark mode offset uses bg-primary instead of white */
|
|
256
246
|
--focus-ring-offset: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--color-primary);
|
|
257
|
-
--focus-ring-soft: 0 0 0 3px
|
|
247
|
+
--focus-ring-soft: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent);
|
|
258
248
|
--focus-ring-soft-error: 0 0 0 3px rgba(248, 113, 113, 0.26);
|
|
259
249
|
|
|
260
250
|
/* Legacy aliases */
|
|
@@ -264,6 +254,122 @@ html.dark {
|
|
|
264
254
|
color-scheme: dark;
|
|
265
255
|
}
|
|
266
256
|
|
|
257
|
+
/* Accent palettes (Settings → Color palette). The settings store writes
|
|
258
|
+
html[data-palette]; the palette swatches set the same attribute on their
|
|
259
|
+
strip to preview a palette. Each palette is one accent in both themes: white
|
|
260
|
+
text on the light-mode fill, dark text (--text-on-accent) on the dark-mode
|
|
261
|
+
fill. Hues come from the biology plot-style palettes. */
|
|
262
|
+
|
|
263
|
+
/* Teal (default): teal-600 fill, teal-700 hover + ink, teal-800 pressed.
|
|
264
|
+
White on #0D9488 is 3.74:1, a known AA exception. */
|
|
265
|
+
:root,
|
|
266
|
+
[data-palette="default"] {
|
|
267
|
+
--color-primary: #0D9488;
|
|
268
|
+
--color-primary-hover: #0F766E;
|
|
269
|
+
--color-primary-pressed: #115E59;
|
|
270
|
+
--color-primary-soft: rgba(13, 148, 136, 0.10);
|
|
271
|
+
--color-primary-border: #99F6E4;
|
|
272
|
+
}
|
|
273
|
+
html.dark,
|
|
274
|
+
html.dark [data-palette="default"] {
|
|
275
|
+
--color-primary: #2DD4BF;
|
|
276
|
+
--color-primary-hover: #5EEAD4;
|
|
277
|
+
--color-primary-pressed: #99F6E4;
|
|
278
|
+
--color-primary-soft: rgba(45, 212, 191, 0.12);
|
|
279
|
+
--color-primary-border: rgba(45, 212, 191, 0.35);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Colour-blind safe: Okabe-Ito blue accent, and Okabe-Ito bluish green,
|
|
283
|
+
vermilion and orange for status, so success and error differ for every
|
|
284
|
+
colour-vision type. White on #0072B2 is 5.19:1. */
|
|
285
|
+
:root[data-palette="colorblind"],
|
|
286
|
+
[data-palette="colorblind"] {
|
|
287
|
+
--color-primary: #0072B2;
|
|
288
|
+
--color-primary-hover: #005F94;
|
|
289
|
+
--color-primary-pressed: #004C77;
|
|
290
|
+
--color-primary-soft: rgba(0, 114, 178, 0.10);
|
|
291
|
+
--color-primary-border: #B3D5EA;
|
|
292
|
+
--mint-success: #009E73;
|
|
293
|
+
--mint-success-hover: #00805D;
|
|
294
|
+
--mint-success-bg: rgba(0, 158, 115, 0.1);
|
|
295
|
+
--mint-success-border: rgba(0, 158, 115, 0.3);
|
|
296
|
+
--mint-error: #D55E00;
|
|
297
|
+
--mint-error-hover: #B34E00;
|
|
298
|
+
--mint-error-bg: rgba(213, 94, 0, 0.1);
|
|
299
|
+
--mint-error-border: rgba(213, 94, 0, 0.3);
|
|
300
|
+
--mint-warning: #E69F00;
|
|
301
|
+
--mint-warning-hover: #C78800;
|
|
302
|
+
--mint-warning-bg: rgba(230, 159, 0, 0.1);
|
|
303
|
+
--mint-warning-border: rgba(230, 159, 0, 0.3);
|
|
304
|
+
--mint-warning-text: #8F5F00;
|
|
305
|
+
}
|
|
306
|
+
html.dark[data-palette="colorblind"],
|
|
307
|
+
html.dark [data-palette="colorblind"] {
|
|
308
|
+
--color-primary: #56B4E9;
|
|
309
|
+
--color-primary-hover: #7CC5EE;
|
|
310
|
+
--color-primary-pressed: #A3D6F3;
|
|
311
|
+
--color-primary-soft: rgba(86, 180, 233, 0.14);
|
|
312
|
+
--color-primary-border: rgba(86, 180, 233, 0.38);
|
|
313
|
+
--mint-success-bg: rgba(0, 158, 115, 0.15);
|
|
314
|
+
--mint-error-bg: rgba(213, 94, 0, 0.15);
|
|
315
|
+
--mint-warning-bg: rgba(230, 159, 0, 0.15);
|
|
316
|
+
--mint-warning-text: #FCD34D;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Purple: plot-style purple #8781bd, deepened so white text is 5.15:1. */
|
|
320
|
+
:root[data-palette="purple"],
|
|
321
|
+
[data-palette="purple"] {
|
|
322
|
+
--color-primary: #6B64B0;
|
|
323
|
+
--color-primary-hover: #5A539E;
|
|
324
|
+
--color-primary-pressed: #4A448A;
|
|
325
|
+
--color-primary-soft: rgba(107, 100, 176, 0.10);
|
|
326
|
+
--color-primary-border: #D3D0EC;
|
|
327
|
+
}
|
|
328
|
+
html.dark[data-palette="purple"],
|
|
329
|
+
html.dark [data-palette="purple"] {
|
|
330
|
+
--color-primary: #A9A3DB;
|
|
331
|
+
--color-primary-hover: #C2BDE7;
|
|
332
|
+
--color-primary-pressed: #D8D4F1;
|
|
333
|
+
--color-primary-soft: rgba(169, 163, 219, 0.14);
|
|
334
|
+
--color-primary-border: rgba(169, 163, 219, 0.38);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Ocean: plot-style ocean ramp, #2171B5 fill (white 5.13:1), #6BAED6 dark. */
|
|
338
|
+
:root[data-palette="ocean"],
|
|
339
|
+
[data-palette="ocean"] {
|
|
340
|
+
--color-primary: #2171B5;
|
|
341
|
+
--color-primary-hover: #1B5F99;
|
|
342
|
+
--color-primary-pressed: #164E7E;
|
|
343
|
+
--color-primary-soft: rgba(33, 113, 181, 0.10);
|
|
344
|
+
--color-primary-border: #BBD7EE;
|
|
345
|
+
}
|
|
346
|
+
html.dark[data-palette="ocean"],
|
|
347
|
+
html.dark [data-palette="ocean"] {
|
|
348
|
+
--color-primary: #6BAED6;
|
|
349
|
+
--color-primary-hover: #8EC2E2;
|
|
350
|
+
--color-primary-pressed: #B0D5EC;
|
|
351
|
+
--color-primary-soft: rgba(107, 174, 214, 0.14);
|
|
352
|
+
--color-primary-border: rgba(107, 174, 214, 0.38);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* Forest: plot-style forest_clay, #2F6B4F fill (white 6.29:1), #7AA874 dark. */
|
|
356
|
+
:root[data-palette="forest"],
|
|
357
|
+
[data-palette="forest"] {
|
|
358
|
+
--color-primary: #2F6B4F;
|
|
359
|
+
--color-primary-hover: #275A42;
|
|
360
|
+
--color-primary-pressed: #1F4935;
|
|
361
|
+
--color-primary-soft: rgba(47, 107, 79, 0.10);
|
|
362
|
+
--color-primary-border: #BFD8C6;
|
|
363
|
+
}
|
|
364
|
+
html.dark[data-palette="forest"],
|
|
365
|
+
html.dark [data-palette="forest"] {
|
|
366
|
+
--color-primary: #7AA874;
|
|
367
|
+
--color-primary-hover: #95BC90;
|
|
368
|
+
--color-primary-pressed: #B1CFAD;
|
|
369
|
+
--color-primary-soft: rgba(122, 168, 116, 0.14);
|
|
370
|
+
--color-primary-border: rgba(122, 168, 116, 0.38);
|
|
371
|
+
}
|
|
372
|
+
|
|
267
373
|
/* Comfortable density (html[data-density], written by the settings store):
|
|
268
374
|
the pre-v2 control heights and a wider gutter. */
|
|
269
375
|
[data-density="comfortable"] {
|
package/src/types/index.ts
CHANGED
package/src/types/jobs.ts
CHANGED
|
@@ -68,3 +68,13 @@ export interface JobSnapshotPayload {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export type JobStreamData = JobStateInput | JobSnapshotPayload
|
|
71
|
+
|
|
72
|
+
/** What the top-bar run slot shows for the viewer's own job. The plugin picks the job. */
|
|
73
|
+
export interface TopBarJob {
|
|
74
|
+
/** Lifecycle of the shown job; sets the icon and the tone. */
|
|
75
|
+
status: JobStatus
|
|
76
|
+
/** Tooltip and accessible name, e.g. "Extracting", "Saved to EXP-0142". Not shown as text. */
|
|
77
|
+
label: string
|
|
78
|
+
/** 0-100; drawn as a small bar after the icon. */
|
|
79
|
+
percent?: number | null
|
|
80
|
+
}
|
package/src/types/platform.ts
CHANGED
|
@@ -66,5 +66,5 @@ export type PlatformContext = Partial<Omit<PlatformFrontendContext, 'plugin'>> &
|
|
|
66
66
|
|
|
67
67
|
// Settings state types
|
|
68
68
|
export type ThemeMode = 'light' | 'dark' | 'system'
|
|
69
|
-
export type ColorPalette = 'default' | 'colorblind' | '
|
|
69
|
+
export type ColorPalette = 'default' | 'colorblind' | 'purple' | 'ocean' | 'forest'
|
|
70
70
|
export type TableDensity = 'compact' | 'normal' | 'comfortable'
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExperimentSelectorModal-65peG4Up.js","names":[],"sources":["../src/composables/useExperimentSelector.ts","../src/components/ExperimentSelectorModal.vue","../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequest } from './useRequest'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */\n withStatusCounts?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n /** Experiment count per status under every filter except status itself. */\n statusCounts: Ref<Record<string, number>>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n /** Fetch `statusCounts` from `GET /experiments/stats`. */\n fetchStatusCounts: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const {\n limit = 100,\n immediate = false,\n experimentType,\n allowedExperimentTypes: optionAllowedExperimentTypes,\n apiBaseUrl,\n withStatusCounts = false,\n } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequest('Failed to fetch experiments')\n const isLoading = request.isLoading\n const error = request.error\n const lastLoadedAt = ref<Date | null>(null)\n const page = ref(0)\n let fetchGeneration = 0\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const statusCounts = ref<Record<string, number>>({})\n let statsGeneration = 0\n let countedScope: string | null = null\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n mine: undefined,\n })\n\n function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {\n if (values == null) return null\n return [...new Set(values.filter((value): value is string => typeof value === 'string' && value.length > 0))]\n }\n\n function currentAllowedExperimentTypes(): string[] | null {\n if (optionAllowedExperimentTypes !== undefined) {\n return normalizeExperimentTypes(optionAllowedExperimentTypes)\n }\n return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes)\n }\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n type ExperimentTypeScope =\n | { kind: 'all' }\n | { kind: 'blocked' }\n | { kind: 'single'; experimentType: string }\n | { kind: 'multi'; experimentTypes: string[] }\n\n function resolveExperimentTypeScope(): ExperimentTypeScope {\n const allowedTypes = currentAllowedExperimentTypes()\n const selectedType = experimentType ?? filters.experimentType ?? undefined\n\n if (selectedType) {\n if (allowedTypes && !allowedTypes.includes(selectedType)) {\n return { kind: 'blocked' }\n }\n return { kind: 'single', experimentType: selectedType }\n }\n\n if (allowedTypes) {\n if (allowedTypes.length === 0) return { kind: 'blocked' }\n if (allowedTypes.length === 1) return { kind: 'single', experimentType: allowedTypes[0] }\n return { kind: 'multi', experimentTypes: allowedTypes }\n }\n\n return { kind: 'all' }\n }\n\n /** Query params shared by the list and the stats route; status, sort and paging are list-only. */\n function scopeParams(typeFilter: string | undefined): URLSearchParams {\n const params = new URLSearchParams()\n if (typeFilter) params.set('experiment_type', typeFilter)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n if (filters.mine) params.set('mine', 'true')\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n return params\n }\n\n function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {\n const params = scopeParams(typeFilter)\n if (filters.status) params.set('status', filters.status)\n\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n params.set('limit', String(pageLimit))\n params.set('skip', String(skip))\n\n const query = params.toString()\n const base = platformBase ?? ''\n return `${base}/experiments${query ? `?${query}` : ''}`\n }\n\n function sortExperiments(items: ExperimentSummary[]): ExperimentSummary[] {\n const { sortBy, sortOrder } = parseSortKey()\n const direction = sortOrder === 'desc' ? -1 : 1\n return [...items].sort((a, b) => {\n const aValue = a[sortBy] ?? ''\n const bValue = b[sortBy] ?? ''\n if (aValue < bValue) return -1 * direction\n if (aValue > bValue) return 1 * direction\n return 0\n })\n }\n\n async function fetchExperimentList(typeFilter: string | undefined, skip: number, pageLimit: number) {\n return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))\n }\n\n async function fetchScopedExperimentPage(\n requestedPage: number,\n ): Promise<ExperimentListResponse> {\n const scope = resolveExperimentTypeScope()\n const pageSkip = requestedPage * limit\n\n if (scope.kind === 'blocked') {\n return { experiments: [], total: 0 }\n }\n\n if (scope.kind === 'multi') {\n const perTypeLimit = pageSkip + limit\n const responses = await Promise.all(\n scope.experimentTypes.map(type => fetchExperimentList(type, 0, perTypeLimit)),\n )\n const merged = sortExperiments(responses.flatMap(response => response.experiments))\n return {\n experiments: merged.slice(pageSkip, pageSkip + limit),\n total: responses.reduce((sum, response) => sum + response.total, 0),\n }\n }\n\n return await fetchExperimentList(\n scope.kind === 'single' ? scope.experimentType : undefined,\n pageSkip,\n limit,\n )\n }\n\n async function fetchExperiments(): Promise<void> {\n const generation = ++fetchGeneration\n const requestedPage = page.value\n isLoading.value = true\n request.clearError()\n try {\n const data = await fetchScopedExperimentPage(requestedPage)\n if (generation !== fetchGeneration) return\n if (requestedPage === 0) {\n experiments.value = data.experiments\n } else {\n experiments.value = [...experiments.value, ...data.experiments]\n }\n total.value = data.total\n lastLoadedAt.value = new Date()\n } catch (err) {\n if (generation !== fetchGeneration) return\n request.setError(err, 'Failed to fetch experiments')\n if (requestedPage === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n if (generation === fetchGeneration) {\n isLoading.value = false\n }\n }\n }\n\n function invalidateFetch(): void {\n fetchGeneration += 1\n isLoading.value = false\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n const allowedTypes = currentAllowedExperimentTypes()\n const allowedSet = allowedTypes ? new Set(allowedTypes) : null\n experimentTypes.value = typesRes.value\n .filter(t => !allowedSet || allowedSet.has(t.value))\n .map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) {\n filters.experimentType = undefined\n }\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n /** The filters the stats route reads; status, sort and paging do not change the counts. */\n function countScope(): string {\n return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])\n }\n\n function refreshStatusCountsOnScopeChange(): void {\n if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()\n }\n\n async function fetchStatusCounts(): Promise<void> {\n const generation = ++statsGeneration\n countedScope = countScope()\n const scope = resolveExperimentTypeScope()\n if (scope.kind === 'blocked') {\n statusCounts.value = {}\n return\n }\n const types = scope.kind === 'multi'\n ? scope.experimentTypes\n : [scope.kind === 'single' ? scope.experimentType : undefined]\n const base = platformBase ?? ''\n try {\n const responses = await Promise.all(\n types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(\n `${base}/experiments/stats?${scopeParams(type)}`,\n )),\n )\n if (generation !== statsGeneration) return\n const counts: Record<string, number> = {}\n for (const response of responses) {\n for (const [status, count] of Object.entries(response.by_status)) {\n counts[status] = (counts[status] ?? 0) + count\n }\n }\n statusCounts.value = counts\n } catch {\n // Counts only decorate the status menu; without them it shows bare labels.\n // Forget the scope so the next status or sort change retries the request.\n if (generation === statsGeneration) {\n statusCounts.value = {}\n countedScope = null\n }\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n invalidateFetch()\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n filters.mine = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n watch(\n () => [\n filters.search,\n filters.status,\n filters.project,\n filters.experimentType,\n filters.datePreset,\n filters.mine,\n sortKey.value,\n ],\n () => invalidateFetch(),\n { flush: 'sync' },\n )\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n refreshStatusCountsOnScopeChange()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n // Also checked on a status or sort change: the cancelled debounce may hold a new search.\n refreshStatusCountsOnScopeChange()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n fetchStatusCounts,\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsModal-DO3IZ7qz.js","names":[],"sources":["../src/components/internal/SettingsPaneHeadInternal.vue","../src/components/internal/SettingsPaneHeadInternal.vue","../src/components/SettingsModal.schema.ts","../src/components/SettingsModal.vue","../src/components/SettingsModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** SettingsModal pane header: title + description or mono count, and an optional \"Restore group defaults\" action. */\ninterface Props {\n title: string\n description?: string\n /** Mono count after the title (search results). */\n count?: string\n /** Modified fields in the group. Omit to hide the restore action; 0 renders it disabled. */\n modifiedCount?: number\n}\n\ndefineProps<Props>()\n\nconst emit = defineEmits<{\n restore: []\n}>()\n</script>\n\n<template>\n <div class=\"mint-settings-modal__head\">\n <div class=\"mint-settings-modal__head-text\">\n <h4 class=\"mint-settings-modal__head-title\">\n {{ title }}\n <span v-if=\"count\" class=\"mint-settings-modal__head-count\">{{ count }}</span>\n </h4>\n <p v-if=\"description\" class=\"mint-settings-modal__head-description\">{{ description }}</p>\n </div>\n <button\n v-if=\"modifiedCount !== undefined\"\n type=\"button\"\n class=\"mint-settings-modal__restore-group\"\n :disabled=\"modifiedCount === 0\"\n @click=\"emit('restore')\"\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3 8a5 5 0 1 0 1.5-3.5M3 3v2.5h2.5\" />\n </svg>\n Restore group defaults\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/** SettingsModal pane header: title + description or mono count, and an optional \"Restore group defaults\" action. */\ninterface Props {\n title: string\n description?: string\n /** Mono count after the title (search results). */\n count?: string\n /** Modified fields in the group. Omit to hide the restore action; 0 renders it disabled. */\n modifiedCount?: number\n}\n\ndefineProps<Props>()\n\nconst emit = defineEmits<{\n restore: []\n}>()\n</script>\n\n<template>\n <div class=\"mint-settings-modal__head\">\n <div class=\"mint-settings-modal__head-text\">\n <h4 class=\"mint-settings-modal__head-title\">\n {{ title }}\n <span v-if=\"count\" class=\"mint-settings-modal__head-count\">{{ count }}</span>\n </h4>\n <p v-if=\"description\" class=\"mint-settings-modal__head-description\">{{ description }}</p>\n </div>\n <button\n v-if=\"modifiedCount !== undefined\"\n type=\"button\"\n class=\"mint-settings-modal__restore-group\"\n :disabled=\"modifiedCount === 0\"\n @click=\"emit('restore')\"\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3 8a5 5 0 1 0 1.5-3.5M3 3v2.5h2.5\" />\n </svg>\n Restore group defaults\n </button>\n </div>\n</template>\n","import type {\n FormSchema,\n FormSectionSchema,\n SettingsGroup,\n SettingsModalSchema,\n SettingsTab,\n SettingsTabInput,\n} from '../types'\nimport type { AccessControlled } from '../permissions'\nimport { normalizeItemInput } from '../utils/items'\n\nexport const APPEARANCE_TAB_ID = 'appearance'\n\nexport const APPEARANCE_TAB: SettingsTab = {\n id: APPEARANCE_TAB_ID,\n label: 'Appearance',\n // Heroicons \"swatch\" — the built-in tab sits beside plugin tabs that all\n // carry icons, so it needs one of its own to keep the rail consistent.\n icon: '<svg fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z\"></path></svg>',\n description: 'Theme, color palette, and density',\n}\n\n// Map settings groups onto the form-builder's flat-section shape.\n// `title: ''` because the rail/pane header already shows the group name.\nexport function buildFlatSettingsSchema(schema: SettingsModalSchema): FormSchema {\n return {\n sections: schema.groups.map<FormSectionSchema>((group) => ({\n id: group.id,\n title: '',\n fields: group.fields,\n columns: group.columns,\n condition: group.condition,\n })),\n }\n}\n\nexport function filterSettingsSchemaByAccess(\n schema: SettingsModalSchema,\n canShow: (item: AccessControlled) => boolean\n): SettingsModalSchema {\n const groups = schema.groups.flatMap((group) => {\n if (!canShow(group)) return []\n\n const fields = group.fields.filter(canShow)\n if (fields.length === 0) return []\n\n return [{ ...group, fields }]\n })\n\n return { groups }\n}\n\nexport function settingsGroupToTab(group: SettingsGroup): SettingsTab {\n return {\n id: group.id,\n label: group.label,\n icon: group.icon,\n description: group.description,\n }\n}\n\nexport function normalizeVisibleSettingsTabs(\n tabs: SettingsTabInput[],\n canShow: (item: AccessControlled) => boolean\n): SettingsTab[] {\n return tabs.map(normalizeItemInput).filter(canShow)\n}\n\nexport function buildSettingsTabs(\n baseTabs: SettingsTab[],\n showAppearance: boolean,\n appearanceFirst = false\n): SettingsTab[] {\n if (!showAppearance) return baseTabs\n return appearanceFirst ? [APPEARANCE_TAB, ...baseTabs] : [...baseTabs, APPEARANCE_TAB]\n}\n","<script setup lang=\"ts\">\n/**\n * Tabbed settings modal with three usage modes:\n *\n * 1. Schema-driven (recommended) — pass a `defineControlModel()` result or a\n * `SettingsModalSchema` as `spec`, plus `v-model:values`. Each\n * group/section becomes a tab; fields auto-render via the SDK form field\n * registry (text, select, number, toggle, molecule, concentration, …).\n * Conditional visibility, validation, and dynamic options come for free.\n *\n * 2. Manual tabs + slots — pass `tabs` and a `<template #tab-{id}>` slot\n * per tab. Use this when you need bespoke widgets the form-builder\n * registry doesn't cover (or for legacy plugins).\n *\n * 3. Appearance only — `showAppearance` (default true) renders the built-in\n * theme / palette / table-density tab. Works alongside both modes above.\n *\n * Layout: omitted (default) renders a sidebar rail split into \"Plugin\" and\n * \"Personal\" (Appearance) sections when there are more than three groups, and\n * stacks every group on one page otherwise. `vertical` always uses the rail;\n * `horizontal` keeps the underline tab strip.\n *\n * Schema-driven settings also get a search across groups (label + hint), a\n * modified-from-default marker with a per-row reset for every field with a\n * non-null `defaultValue` (every control model field gets one), and a \"Restore group defaults\" action. Changes\n * apply immediately; the default footer says so and closes with \"Done\".\n *\n * @example Recommended\n * ```vue\n * <SettingsModal v-model:open=\"open\" v-model:values=\"values\" :spec=\"settingsModel\" />\n * ```\n */\nimport { ref, computed, watch } from 'vue'\nimport BaseModal from './BaseModal.vue'\nimport BaseButton from './BaseButton.vue'\nimport BaseInput from './BaseInput.vue'\nimport SegmentedControl from './SegmentedControl.vue'\nimport FormFieldRendererInternal from './internal/FormFieldRendererInternal.vue'\nimport SettingsPaneHeadInternal from './internal/SettingsPaneHeadInternal.vue'\nimport { useFormBuilder } from '../composables/useFormBuilder'\nimport {\n controlsToSettingsSchema,\n isControlModelBinding,\n type ControlModelBinding,\n} from '../composables/useControlSchema'\nimport { useSettingsStore, colorPalettes } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\nimport { usePlatform } from '../composables/usePlatform'\nimport { canAccessByPolicy } from '../permissions'\nimport {\n formSchemaFieldNames,\n pickExistingRecordKeys,\n pickRecordKeys,\n recordValuesEqualForKeys,\n valuesEqual,\n} from '../utils/formModelSync'\nimport type {\n ThemeMode,\n ColorPalette,\n TableDensity,\n ModalLayout,\n ModalTab,\n SettingsGroup,\n SettingsTab,\n SettingsTabInput,\n SettingsModalLayout,\n SettingsModalSchema,\n SettingsUserType,\n FormFieldSchema,\n} from '../types'\nimport type { AccessControlled, PermissionUser } from '../permissions'\nimport {\n APPEARANCE_TAB,\n APPEARANCE_TAB_ID,\n buildFlatSettingsSchema,\n buildSettingsTabs,\n filterSettingsSchemaByAccess,\n normalizeVisibleSettingsTabs,\n settingsGroupToTab,\n} from './SettingsModal.schema'\n\ninterface Props {\n open: boolean\n title?: string\n /** Manual tab descriptors. Ignored when `spec` is set (groups become tabs). */\n tabs?: SettingsTabInput[]\n showAppearance?: boolean\n /** List Appearance before the other tabs and open on it. */\n appearanceFirst?: boolean\n size?: 'md' | 'lg' | 'xl'\n layout?: SettingsModalLayout\n /** Control model built with defineControlModel(), or a SettingsModalSchema (e.g. the generated `settingsForm`). Fields auto-render via SDK form components. */\n spec?: ControlModelBinding | SettingsModalSchema\n /** Two-way bound values when `spec` is set. */\n values?: Record<string, unknown>\n /** Optional user type override for permission-filtered settings content. Defaults to SDK auth/platform context. */\n userType?: SettingsUserType\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n title: 'Settings',\n tabs: () => [],\n showAppearance: true,\n size: 'lg',\n layout: undefined,\n spec: undefined,\n})\n\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n 'update:values': [data: Record<string, unknown>]\n close: []\n}>()\n\nconst settings = useSettingsStore()\nconst auth = useAuthStore()\nconst { user: platformUser, integrated, isAdmin: platformIsAdmin } = usePlatform()\n// With sign-in turned off (`authRequired: false`) every caller of an integrated page is the\n// implicit administrator, and usePlatform reports a page with no user as admin.\nconst implicitPlatformAdmin = computed(() => integrated.value && !platformUser.value && platformIsAdmin.value)\n\nconst resolvedModel = computed<ControlModelBinding | undefined>(() =>\n isControlModelBinding(props.spec) ? props.spec : undefined,\n)\nconst sourceSettingsSchema = computed<SettingsModalSchema | undefined>(() =>\n resolvedModel.value\n ? controlsToSettingsSchema(resolvedModel.value.controls, resolvedModel.value.controlOptions)\n : props.spec as SettingsModalSchema | undefined,\n)\nconst currentAccessUser = computed<PermissionUser | null>(() => {\n if (props.userType) {\n return { role: props.userType === 'admin' ? 'admin' : 'user' }\n }\n return auth.userInfo ?? platformUser.value ?? (implicitPlatformAdmin.value ? { role: 'admin' } : null)\n})\nconst isAccessUserAuthenticated = computed(() =>\n props.userType !== undefined || auth.isAuthenticated || !!platformUser.value || implicitPlatformAdmin.value,\n)\nconst settingsSchema = computed<SettingsModalSchema | undefined>(() => {\n if (!sourceSettingsSchema.value) return undefined\n return filterSettingsSchemaByAccess(sourceSettingsSchema.value, canShowForCurrentUser)\n})\nconst isSchemaDriven = computed(() => !!settingsSchema.value)\nconst resolvedValues = computed<Record<string, unknown>>(() => ({\n ...(resolvedModel.value?.controlOptions.initialValues ?? {}),\n ...(props.values ?? {}),\n}))\n\n// Schema-driven mode keeps one form-builder instance and syncs schema changes\n// into it, so callers can swap groups/fields without remounting the modal.\nconst builder = useFormBuilder(\n settingsSchema.value ? buildFlatSettingsSchema(settingsSchema.value) : { sections: [] },\n resolvedValues.value,\n resolvedModel.value?.controlOptions.enhancements,\n)\n\nwatch(\n () => settingsSchema.value,\n (schema) => {\n if (!schema) {\n builder.updateSchema({ sections: [] }, {})\n return\n }\n\n const flatSchema = buildFlatSettingsSchema(schema)\n const fieldNames = formSchemaFieldNames(flatSchema)\n const sourceValues = props.values === undefined\n ? {\n ...resolvedValues.value,\n ...(builder.form.data as Record<string, unknown>),\n }\n : resolvedValues.value\n\n builder.updateSchema(flatSchema, pickExistingRecordKeys(sourceValues ?? {}, fieldNames))\n },\n { deep: true },\n)\n\nwatch(\n () => ({ ...resolvedValues.value }),\n (data) => {\n if (!settingsSchema.value) return\n const fieldNames = builderFieldNames()\n if (recordValuesEqualForKeys(data, builder.form.data as Record<string, unknown>, fieldNames)) return\n builder.reset(pickRecordKeys(data, fieldNames))\n },\n { deep: true },\n)\n\n// Changes apply immediately; the footer confirms it once the user changed something.\nconst applied = ref(false)\n\nwatch(\n () => ({ ...builder.form.data }),\n (data) => {\n // A reset to the parent's values (async load, schema swap) is not a user change.\n if (!recordValuesEqualForKeys(data, resolvedValues.value, builderFieldNames())) applied.value = true\n if (settingsSchema.value) {\n emit('update:values', {\n ...resolvedValues.value,\n ...data,\n })\n }\n },\n { deep: true },\n)\n\n// Schema groups whose `condition` evaluates false against the current data are\n// dropped from the rail entirely — same semantics as section visibility in\n// FormBuilder. Manual `tabs` have no condition mechanism, so they pass through.\nconst visibleSchemaGroups = computed(() =>\n settingsSchema.value\n ? settingsSchema.value.groups.filter((g) => builder.isSectionVisible(g.id))\n : [],\n)\n\nconst manualTabs = computed<SettingsTab[]>(() =>\n normalizeVisibleSettingsTabs(props.tabs, canShowForCurrentUser)\n)\n\nconst allTabs = computed<SettingsTab[]>(() => {\n const base: SettingsTab[] = settingsSchema.value\n ? visibleSchemaGroups.value.map(settingsGroupToTab)\n : manualTabs.value\n return buildSettingsTabs(base, props.showAppearance, props.appearanceFirst)\n})\n\nconst activeTab = ref(allTabs.value[0]?.id || APPEARANCE_TAB_ID)\n\n/** Above this many plugin groups the default layout switches from one stacked page to the rail. */\nconst STACKED_GROUP_LIMIT = 3\n\n// BaseModal owns the tab chrome: 'vertical' keeps its public name and maps to\n// the shell rail; horizontal maps to the shell strip (plain when single-tab).\nconst baseLayout = computed<ModalLayout>(() => {\n if (props.layout === 'vertical') return 'rail'\n if (props.layout === 'horizontal') return allTabs.value.length > 1 ? 'tabs' : 'plain'\n const pluginTabCount = settingsSchema.value ? visibleSchemaGroups.value.length : manualTabs.value.length\n return pluginTabCount > STACKED_GROUP_LIMIT ? 'rail' : 'plain'\n})\nconst isRail = computed(() => baseLayout.value === 'rail')\nconst isStacked = computed(() => baseLayout.value === 'plain')\n\nconst query = ref('')\nconst searchTerm = computed(() => query.value.trim().toLowerCase())\nconst isSearching = computed(() => isSchemaDriven.value && searchTerm.value !== '')\n\nconst modalTabs = computed<ModalTab[]>(() =>\n allTabs.value.map((tab) => {\n const group = visibleSchemaGroups.value.find((g) => g.id === tab.id)\n const modified = group ? modifiedCount(group) : 0\n return {\n id: tab.id,\n label: tab.label,\n icon: tab.icon,\n description: tab.description,\n meta: group ? (modified ? String(modified) : undefined) : tab.meta,\n // Sections only split plugin groups from Appearance; without it there is nothing to split.\n section: props.showAppearance ? (tab.id === APPEARANCE_TAB_ID ? 'Personal' : tab.section ?? 'Plugin') : undefined,\n }\n }),\n)\n\nfunction selectTab(id: string) {\n activeTab.value = id\n query.value = ''\n}\n\nfunction handleSearchKeydown(event: KeyboardEvent) {\n // First Escape clears the search; the next one closes the modal.\n if (event.key === 'Escape' && query.value) {\n event.stopPropagation()\n query.value = ''\n }\n}\n\n// If the active tab vanishes (group hidden by condition), drop back to the first available.\nwatch(allTabs, (tabs) => {\n if (!tabs.some((t) => t.id === activeTab.value)) {\n activeTab.value = tabs[0]?.id ?? APPEARANCE_TAB_ID\n }\n})\n\nconst activeGroup = computed(() =>\n visibleSchemaGroups.value.find((g) => g.id === activeTab.value),\n)\n\nfunction visibleFields(group: SettingsGroup): FormFieldSchema[] {\n return group.fields.filter((f) => builder.isFieldVisible(f.name))\n}\n\n/**\n * Modified = differs from the field's own `defaultValue`. A field without one, or with a `null`\n * default (a secret, or an optional setting), never is: restoring `null` would clear a stored value.\n */\nfunction isModified(field: FormFieldSchema): boolean {\n return field.defaultValue != null\n && !valuesEqual((builder.form.data as Record<string, unknown>)[field.name], field.defaultValue)\n}\n\nfunction modifiedCount(group: SettingsGroup): number {\n return visibleFields(group).filter(isModified).length\n}\n\nfunction resetField(field: FormFieldSchema) {\n builder.form.setFieldValue(field.name, field.defaultValue)\n}\n\nfunction restoreGroup(group: SettingsGroup | undefined) {\n if (group) visibleFields(group).filter(isModified).forEach(resetField)\n}\n\nfunction isBooleanField(field: FormFieldSchema): boolean {\n return field.type === 'checkbox' || field.type === 'toggle'\n}\n\n/** Booleans are always toggles inside settings rows; their hint is printed under the row, not in a tooltip. */\nfunction settingsField(field: FormFieldSchema): FormFieldSchema {\n return isBooleanField(field) ? { ...field, type: 'toggle', hint: undefined } : field\n}\n\ninterface SettingsRow {\n field: FormFieldSchema\n group: SettingsGroup\n}\n\nconst searchResults = computed<SettingsRow[]>(() => {\n if (!isSearching.value) return []\n return visibleSchemaGroups.value.flatMap((group) =>\n visibleFields(group)\n .filter((field) => [field.label, field.hint, group.label].join(' ').toLowerCase().includes(searchTerm.value))\n .map((field) => ({ field, group })),\n )\n})\n\nconst resultCount = computed(() =>\n `${searchResults.value.length} result${searchResults.value.length === 1 ? '' : 's'}`,\n)\n\n/** What the pane renders: one list of search hits, every group (stacked) or the active group. */\nconst paneSections = computed<{ key: string; group?: SettingsGroup; rows: SettingsRow[] }[]>(() => {\n if (isSearching.value) {\n return searchResults.value.length ? [{ key: 'search', rows: searchResults.value }] : []\n }\n const groups = isStacked.value\n ? visibleSchemaGroups.value\n : activeGroup.value ? [activeGroup.value] : []\n return groups.map((group) => ({\n key: group.id,\n group,\n rows: visibleFields(group).map((field) => ({ field, group })),\n }))\n})\n\nconst themeOptions: { value: ThemeMode; label: string; halves: ('light' | 'dark')[] }[] = [\n { value: 'light', label: 'Light', halves: ['light'] },\n { value: 'dark', label: 'Dark', halves: ['dark'] },\n { value: 'system', label: 'System', halves: ['light', 'dark'] },\n]\n\n/** Five stops along the palette's primary → accent hue arc (shortest way round), as in the board. */\nfunction paletteSwatches(key: ColorPalette): string[] {\n const [from, to] = colorPalettes[key].hues\n const span = ((to - from + 540) % 360) - 180\n const tone = key === 'pastel' ? '60% 72%' : '65% 48%'\n return Array.from({ length: 5 }, (_, i) => `hsl(${Math.round(from + (span * i) / 4)} ${tone})`)\n}\n\nconst densityPreviewRows = [\n ['S-0012', '2.41', '12.5'],\n ['S-0013', '7.90', '3.25'],\n ['S-0014', '10.60', '0.80'],\n]\n\nconst densityOptions: { value: TableDensity; label: string }[] = [\n { value: 'compact', label: 'Compact' },\n { value: 'normal', label: 'Normal' },\n { value: 'comfortable', label: 'Comfortable' },\n]\n\nwatch(\n () => [settings.theme, settings.colorPalette, settings.tableDensity],\n () => { applied.value = true },\n)\n\nfunction handleClose() {\n emit('update:open', false)\n emit('close')\n}\n\nfunction builderFieldNames(): string[] {\n return builder.fields.map(field => field.name)\n}\n\nfunction canShowForCurrentUser(item: AccessControlled): boolean {\n return canAccessByPolicy(\n currentAccessUser.value,\n item.access,\n isAccessUserAuthenticated.value,\n )\n}\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n :layout=\"baseLayout\"\n :tabs=\"modalTabs\"\n :active-tab=\"isSearching ? '' : activeTab\"\n @update:active-tab=\"selectTab\"\n @update:open=\"emit('update:open', $event)\"\n @close=\"handleClose\"\n >\n <template #header>\n <div class=\"mint-settings-modal__header\">\n <h3 class=\"mint-modal__title\">{{ title }}</h3>\n <div v-if=\"isSchemaDriven\" class=\"mint-settings-modal__search\">\n <svg class=\"mint-settings-modal__search-icon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"4.5\" /><path d=\"M10.5 10.5L14 14\" />\n </svg>\n <BaseInput\n :model-value=\"query\"\n type=\"search\"\n size=\"sm\"\n placeholder=\"Search settings\"\n aria-label=\"Search settings\"\n @update:model-value=\"query = String($event ?? '')\"\n @keydown=\"handleSearchKeydown\"\n />\n </div>\n </div>\n </template>\n\n <template v-if=\"isRail && isSchemaDriven\" #pane-header=\"{ tab }\">\n <SettingsPaneHeadInternal v-if=\"isSearching\" title=\"Search results\" :count=\"resultCount\" />\n <SettingsPaneHeadInternal\n v-else\n :title=\"tab?.label ?? ''\"\n :description=\"tab?.description\"\n :modified-count=\"activeGroup ? modifiedCount(activeGroup) : undefined\"\n @restore=\"restoreGroup(activeGroup)\"\n />\n </template>\n\n <!-- Body: the active pane only — tab strip / rail / footer are BaseModal's shell. -->\n <div :class=\"['mint-settings-modal', { 'mint-settings-modal--schema': isSchemaDriven }]\">\n <template v-if=\"isSchemaDriven\">\n <SettingsPaneHeadInternal v-if=\"isSearching && !isRail\" title=\"Search results\" :count=\"resultCount\" />\n <section v-for=\"section in paneSections\" :key=\"section.key\" class=\"mint-settings-modal__group\">\n <SettingsPaneHeadInternal\n v-if=\"section.group && !isRail\"\n :title=\"section.group.label\"\n :description=\"section.group.description\"\n :modified-count=\"modifiedCount(section.group)\"\n @restore=\"restoreGroup(section.group)\"\n />\n <div\n class=\"mint-settings-modal__group-grid\"\n :style=\"{ '--mint-settings-cols': section.group?.columns ?? 1 }\"\n >\n <div\n v-for=\"row in section.rows\"\n :key=\"row.field.name\"\n :class=\"['mint-settings-modal__row', { 'mint-settings-modal__row--modified': isModified(row.field) }]\"\n :style=\"row.field.colSpan ? { gridColumn: `span ${row.field.colSpan}` } : undefined\"\n >\n <span v-if=\"!section.group\" class=\"mint-settings-modal__row-group\">{{ row.group.label }}</span>\n <div class=\"mint-settings-modal__row-field\">\n <slot\n :name=\"`field:${row.field.name}`\"\n :field=\"row.field\"\n :form=\"builder.form\"\n :field-props=\"builder.form.getFieldProps(row.field.name)\"\n >\n <FormFieldRendererInternal\n :field=\"settingsField(row.field)\"\n :resolved-props=\"builder.getResolvedFieldProps(settingsField(row.field))\"\n :form=\"builder.form\"\n />\n </slot>\n <p v-if=\"isBooleanField(row.field) && row.field.hint\" class=\"mint-settings-modal__row-hint\">\n {{ row.field.hint }}\n </p>\n <button\n v-if=\"isModified(row.field)\"\n type=\"button\"\n class=\"mint-settings-modal__reset\"\n title=\"Restore default\"\n :aria-label=\"`Restore default for ${row.field.label}`\"\n @click=\"resetField(row.field)\"\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3 8a5 5 0 1 0 1.5-3.5M3 3v2.5h2.5\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n </section>\n <p v-if=\"isSearching && searchResults.length === 0\" class=\"mint-settings-modal__empty\">\n No settings match \"{{ query.trim() }}\"\n </p>\n </template>\n\n <template v-else>\n <section\n v-for=\"tab in manualTabs\"\n v-show=\"isStacked || activeTab === tab.id\"\n :key=\"tab.id\"\n class=\"mint-settings-modal__group\"\n >\n <SettingsPaneHeadInternal v-if=\"isStacked\" :title=\"tab.label\" :description=\"tab.description\" />\n <slot :name=\"`tab-${tab.id}`\" />\n </section>\n </template>\n\n <section\n v-if=\"showAppearance\"\n v-show=\"!isSearching && (isStacked || activeTab === APPEARANCE_TAB_ID)\"\n class=\"mint-settings-modal__group mint-settings-modal__appearance\"\n >\n <SettingsPaneHeadInternal\n v-if=\"isStacked\"\n :title=\"APPEARANCE_TAB.label\"\n :description=\"APPEARANCE_TAB.description\"\n />\n\n <div class=\"mint-settings-modal__setting\">\n <div class=\"mint-settings-modal__setting-text\">\n <span class=\"mint-settings-modal__setting-label\">Theme</span>\n <span class=\"mint-settings-modal__setting-hint\">Applies to this browser only</span>\n </div>\n <div class=\"mint-settings-modal__themes\">\n <button\n v-for=\"opt in themeOptions\"\n :key=\"opt.value\"\n type=\"button\"\n :aria-pressed=\"settings.theme === opt.value\"\n :class=\"['mint-settings-modal__theme', { 'mint-settings-modal__theme--active': settings.theme === opt.value }]\"\n @click=\"settings.theme = opt.value\"\n >\n <span class=\"mint-settings-modal__theme-preview\" aria-hidden=\"true\">\n <span\n v-for=\"half in opt.halves\"\n :key=\"half\"\n :class=\"['mint-settings-modal__theme-half', `mint-settings-modal__theme-half--${half}`]\"\n >\n <span class=\"mint-settings-modal__theme-line\" />\n <span class=\"mint-settings-modal__theme-card\" />\n </span>\n </span>\n <span class=\"mint-settings-modal__theme-label\">\n <span class=\"mint-settings-modal__theme-radio\" aria-hidden=\"true\" />\n {{ opt.label }}\n </span>\n </button>\n </div>\n </div>\n\n <div class=\"mint-settings-modal__setting\">\n <div class=\"mint-settings-modal__setting-text\">\n <span class=\"mint-settings-modal__setting-label\">Color palette</span>\n <span class=\"mint-settings-modal__setting-hint\">Primary and accent hues across the interface</span>\n </div>\n <div class=\"mint-settings-modal__palettes\">\n <button\n v-for=\"(palette, key) in colorPalettes\"\n :key=\"key\"\n type=\"button\"\n :aria-pressed=\"settings.colorPalette === key\"\n :class=\"['mint-settings-modal__palette', { 'mint-settings-modal__palette--active': settings.colorPalette === key }]\"\n @click=\"settings.colorPalette = key as ColorPalette\"\n >\n <span class=\"mint-settings-modal__swatches\" aria-hidden=\"true\">\n <span\n v-for=\"color in paletteSwatches(key as ColorPalette)\"\n :key=\"color\"\n class=\"mint-settings-modal__swatch\"\n :style=\"{ backgroundColor: color }\"\n />\n </span>\n <span class=\"mint-settings-modal__palette-name\">{{ palette.name }}</span>\n <svg class=\"mint-settings-modal__palette-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3.5 8.5l3 3 6-7\" />\n </svg>\n </button>\n </div>\n </div>\n\n <div class=\"mint-settings-modal__setting mint-settings-modal__setting--inline\">\n <div class=\"mint-settings-modal__setting-text\">\n <span class=\"mint-settings-modal__setting-label\">Density</span>\n <span class=\"mint-settings-modal__setting-hint\">Adjusts control and table row height</span>\n </div>\n <SegmentedControl\n :model-value=\"settings.tableDensity\"\n :options=\"densityOptions\"\n size=\"sm\"\n @update:model-value=\"settings.tableDensity = $event as TableDensity\"\n />\n <div\n :class=\"['mint-settings-modal__density-preview', `mint-settings-modal__density-preview--${settings.tableDensity}`]\"\n aria-hidden=\"true\"\n >\n <div class=\"mint-settings-modal__density-row mint-settings-modal__density-row--head\">\n <span>Sample</span><span>RT</span><span>IC50</span>\n </div>\n <div v-for=\"cells in densityPreviewRows\" :key=\"cells[0]\" class=\"mint-settings-modal__density-row\">\n <span>{{ cells[0] }}</span><span>{{ cells[1] }}</span><span>{{ cells[2] }}</span>\n </div>\n </div>\n </div>\n\n <slot name=\"appearance\" />\n </section>\n </div>\n\n <template #footer>\n <slot\n name=\"footer\"\n :values=\"builder.form.data\"\n :close=\"handleClose\"\n >\n <span class=\"mint-settings-modal__status\" role=\"status\">\n <svg v-if=\"applied\" class=\"mint-settings-modal__status-icon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3.5 8.5l3 3 6-7\" />\n </svg>\n {{ applied ? 'Applied · changes take effect immediately' : 'Changes take effect immediately' }}\n </span>\n <BaseButton class=\"mint-settings-modal__done\" variant=\"primary\" @click=\"handleClose\">Done</BaseButton>\n </slot>\n </template>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Tabbed settings modal with three usage modes:\n *\n * 1. Schema-driven (recommended) — pass a `defineControlModel()` result or a\n * `SettingsModalSchema` as `spec`, plus `v-model:values`. Each\n * group/section becomes a tab; fields auto-render via the SDK form field\n * registry (text, select, number, toggle, molecule, concentration, …).\n * Conditional visibility, validation, and dynamic options come for free.\n *\n * 2. Manual tabs + slots — pass `tabs` and a `<template #tab-{id}>` slot\n * per tab. Use this when you need bespoke widgets the form-builder\n * registry doesn't cover (or for legacy plugins).\n *\n * 3. Appearance only — `showAppearance` (default true) renders the built-in\n * theme / palette / table-density tab. Works alongside both modes above.\n *\n * Layout: omitted (default) renders a sidebar rail split into \"Plugin\" and\n * \"Personal\" (Appearance) sections when there are more than three groups, and\n * stacks every group on one page otherwise. `vertical` always uses the rail;\n * `horizontal` keeps the underline tab strip.\n *\n * Schema-driven settings also get a search across groups (label + hint), a\n * modified-from-default marker with a per-row reset for every field with a\n * non-null `defaultValue` (every control model field gets one), and a \"Restore group defaults\" action. Changes\n * apply immediately; the default footer says so and closes with \"Done\".\n *\n * @example Recommended\n * ```vue\n * <SettingsModal v-model:open=\"open\" v-model:values=\"values\" :spec=\"settingsModel\" />\n * ```\n */\nimport { ref, computed, watch } from 'vue'\nimport BaseModal from './BaseModal.vue'\nimport BaseButton from './BaseButton.vue'\nimport BaseInput from './BaseInput.vue'\nimport SegmentedControl from './SegmentedControl.vue'\nimport FormFieldRendererInternal from './internal/FormFieldRendererInternal.vue'\nimport SettingsPaneHeadInternal from './internal/SettingsPaneHeadInternal.vue'\nimport { useFormBuilder } from '../composables/useFormBuilder'\nimport {\n controlsToSettingsSchema,\n isControlModelBinding,\n type ControlModelBinding,\n} from '../composables/useControlSchema'\nimport { useSettingsStore, colorPalettes } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\nimport { usePlatform } from '../composables/usePlatform'\nimport { canAccessByPolicy } from '../permissions'\nimport {\n formSchemaFieldNames,\n pickExistingRecordKeys,\n pickRecordKeys,\n recordValuesEqualForKeys,\n valuesEqual,\n} from '../utils/formModelSync'\nimport type {\n ThemeMode,\n ColorPalette,\n TableDensity,\n ModalLayout,\n ModalTab,\n SettingsGroup,\n SettingsTab,\n SettingsTabInput,\n SettingsModalLayout,\n SettingsModalSchema,\n SettingsUserType,\n FormFieldSchema,\n} from '../types'\nimport type { AccessControlled, PermissionUser } from '../permissions'\nimport {\n APPEARANCE_TAB,\n APPEARANCE_TAB_ID,\n buildFlatSettingsSchema,\n buildSettingsTabs,\n filterSettingsSchemaByAccess,\n normalizeVisibleSettingsTabs,\n settingsGroupToTab,\n} from './SettingsModal.schema'\n\ninterface Props {\n open: boolean\n title?: string\n /** Manual tab descriptors. Ignored when `spec` is set (groups become tabs). */\n tabs?: SettingsTabInput[]\n showAppearance?: boolean\n /** List Appearance before the other tabs and open on it. */\n appearanceFirst?: boolean\n size?: 'md' | 'lg' | 'xl'\n layout?: SettingsModalLayout\n /** Control model built with defineControlModel(), or a SettingsModalSchema (e.g. the generated `settingsForm`). Fields auto-render via SDK form components. */\n spec?: ControlModelBinding | SettingsModalSchema\n /** Two-way bound values when `spec` is set. */\n values?: Record<string, unknown>\n /** Optional user type override for permission-filtered settings content. Defaults to SDK auth/platform context. */\n userType?: SettingsUserType\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n title: 'Settings',\n tabs: () => [],\n showAppearance: true,\n size: 'lg',\n layout: undefined,\n spec: undefined,\n})\n\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n 'update:values': [data: Record<string, unknown>]\n close: []\n}>()\n\nconst settings = useSettingsStore()\nconst auth = useAuthStore()\nconst { user: platformUser, integrated, isAdmin: platformIsAdmin } = usePlatform()\n// With sign-in turned off (`authRequired: false`) every caller of an integrated page is the\n// implicit administrator, and usePlatform reports a page with no user as admin.\nconst implicitPlatformAdmin = computed(() => integrated.value && !platformUser.value && platformIsAdmin.value)\n\nconst resolvedModel = computed<ControlModelBinding | undefined>(() =>\n isControlModelBinding(props.spec) ? props.spec : undefined,\n)\nconst sourceSettingsSchema = computed<SettingsModalSchema | undefined>(() =>\n resolvedModel.value\n ? controlsToSettingsSchema(resolvedModel.value.controls, resolvedModel.value.controlOptions)\n : props.spec as SettingsModalSchema | undefined,\n)\nconst currentAccessUser = computed<PermissionUser | null>(() => {\n if (props.userType) {\n return { role: props.userType === 'admin' ? 'admin' : 'user' }\n }\n return auth.userInfo ?? platformUser.value ?? (implicitPlatformAdmin.value ? { role: 'admin' } : null)\n})\nconst isAccessUserAuthenticated = computed(() =>\n props.userType !== undefined || auth.isAuthenticated || !!platformUser.value || implicitPlatformAdmin.value,\n)\nconst settingsSchema = computed<SettingsModalSchema | undefined>(() => {\n if (!sourceSettingsSchema.value) return undefined\n return filterSettingsSchemaByAccess(sourceSettingsSchema.value, canShowForCurrentUser)\n})\nconst isSchemaDriven = computed(() => !!settingsSchema.value)\nconst resolvedValues = computed<Record<string, unknown>>(() => ({\n ...(resolvedModel.value?.controlOptions.initialValues ?? {}),\n ...(props.values ?? {}),\n}))\n\n// Schema-driven mode keeps one form-builder instance and syncs schema changes\n// into it, so callers can swap groups/fields without remounting the modal.\nconst builder = useFormBuilder(\n settingsSchema.value ? buildFlatSettingsSchema(settingsSchema.value) : { sections: [] },\n resolvedValues.value,\n resolvedModel.value?.controlOptions.enhancements,\n)\n\nwatch(\n () => settingsSchema.value,\n (schema) => {\n if (!schema) {\n builder.updateSchema({ sections: [] }, {})\n return\n }\n\n const flatSchema = buildFlatSettingsSchema(schema)\n const fieldNames = formSchemaFieldNames(flatSchema)\n const sourceValues = props.values === undefined\n ? {\n ...resolvedValues.value,\n ...(builder.form.data as Record<string, unknown>),\n }\n : resolvedValues.value\n\n builder.updateSchema(flatSchema, pickExistingRecordKeys(sourceValues ?? {}, fieldNames))\n },\n { deep: true },\n)\n\nwatch(\n () => ({ ...resolvedValues.value }),\n (data) => {\n if (!settingsSchema.value) return\n const fieldNames = builderFieldNames()\n if (recordValuesEqualForKeys(data, builder.form.data as Record<string, unknown>, fieldNames)) return\n builder.reset(pickRecordKeys(data, fieldNames))\n },\n { deep: true },\n)\n\n// Changes apply immediately; the footer confirms it once the user changed something.\nconst applied = ref(false)\n\nwatch(\n () => ({ ...builder.form.data }),\n (data) => {\n // A reset to the parent's values (async load, schema swap) is not a user change.\n if (!recordValuesEqualForKeys(data, resolvedValues.value, builderFieldNames())) applied.value = true\n if (settingsSchema.value) {\n emit('update:values', {\n ...resolvedValues.value,\n ...data,\n })\n }\n },\n { deep: true },\n)\n\n// Schema groups whose `condition` evaluates false against the current data are\n// dropped from the rail entirely — same semantics as section visibility in\n// FormBuilder. Manual `tabs` have no condition mechanism, so they pass through.\nconst visibleSchemaGroups = computed(() =>\n settingsSchema.value\n ? settingsSchema.value.groups.filter((g) => builder.isSectionVisible(g.id))\n : [],\n)\n\nconst manualTabs = computed<SettingsTab[]>(() =>\n normalizeVisibleSettingsTabs(props.tabs, canShowForCurrentUser)\n)\n\nconst allTabs = computed<SettingsTab[]>(() => {\n const base: SettingsTab[] = settingsSchema.value\n ? visibleSchemaGroups.value.map(settingsGroupToTab)\n : manualTabs.value\n return buildSettingsTabs(base, props.showAppearance, props.appearanceFirst)\n})\n\nconst activeTab = ref(allTabs.value[0]?.id || APPEARANCE_TAB_ID)\n\n/** Above this many plugin groups the default layout switches from one stacked page to the rail. */\nconst STACKED_GROUP_LIMIT = 3\n\n// BaseModal owns the tab chrome: 'vertical' keeps its public name and maps to\n// the shell rail; horizontal maps to the shell strip (plain when single-tab).\nconst baseLayout = computed<ModalLayout>(() => {\n if (props.layout === 'vertical') return 'rail'\n if (props.layout === 'horizontal') return allTabs.value.length > 1 ? 'tabs' : 'plain'\n const pluginTabCount = settingsSchema.value ? visibleSchemaGroups.value.length : manualTabs.value.length\n return pluginTabCount > STACKED_GROUP_LIMIT ? 'rail' : 'plain'\n})\nconst isRail = computed(() => baseLayout.value === 'rail')\nconst isStacked = computed(() => baseLayout.value === 'plain')\n\nconst query = ref('')\nconst searchTerm = computed(() => query.value.trim().toLowerCase())\nconst isSearching = computed(() => isSchemaDriven.value && searchTerm.value !== '')\n\nconst modalTabs = computed<ModalTab[]>(() =>\n allTabs.value.map((tab) => {\n const group = visibleSchemaGroups.value.find((g) => g.id === tab.id)\n const modified = group ? modifiedCount(group) : 0\n return {\n id: tab.id,\n label: tab.label,\n icon: tab.icon,\n description: tab.description,\n meta: group ? (modified ? String(modified) : undefined) : tab.meta,\n // Sections only split plugin groups from Appearance; without it there is nothing to split.\n section: props.showAppearance ? (tab.id === APPEARANCE_TAB_ID ? 'Personal' : tab.section ?? 'Plugin') : undefined,\n }\n }),\n)\n\nfunction selectTab(id: string) {\n activeTab.value = id\n query.value = ''\n}\n\nfunction handleSearchKeydown(event: KeyboardEvent) {\n // First Escape clears the search; the next one closes the modal.\n if (event.key === 'Escape' && query.value) {\n event.stopPropagation()\n query.value = ''\n }\n}\n\n// If the active tab vanishes (group hidden by condition), drop back to the first available.\nwatch(allTabs, (tabs) => {\n if (!tabs.some((t) => t.id === activeTab.value)) {\n activeTab.value = tabs[0]?.id ?? APPEARANCE_TAB_ID\n }\n})\n\nconst activeGroup = computed(() =>\n visibleSchemaGroups.value.find((g) => g.id === activeTab.value),\n)\n\nfunction visibleFields(group: SettingsGroup): FormFieldSchema[] {\n return group.fields.filter((f) => builder.isFieldVisible(f.name))\n}\n\n/**\n * Modified = differs from the field's own `defaultValue`. A field without one, or with a `null`\n * default (a secret, or an optional setting), never is: restoring `null` would clear a stored value.\n */\nfunction isModified(field: FormFieldSchema): boolean {\n return field.defaultValue != null\n && !valuesEqual((builder.form.data as Record<string, unknown>)[field.name], field.defaultValue)\n}\n\nfunction modifiedCount(group: SettingsGroup): number {\n return visibleFields(group).filter(isModified).length\n}\n\nfunction resetField(field: FormFieldSchema) {\n builder.form.setFieldValue(field.name, field.defaultValue)\n}\n\nfunction restoreGroup(group: SettingsGroup | undefined) {\n if (group) visibleFields(group).filter(isModified).forEach(resetField)\n}\n\nfunction isBooleanField(field: FormFieldSchema): boolean {\n return field.type === 'checkbox' || field.type === 'toggle'\n}\n\n/** Booleans are always toggles inside settings rows; their hint is printed under the row, not in a tooltip. */\nfunction settingsField(field: FormFieldSchema): FormFieldSchema {\n return isBooleanField(field) ? { ...field, type: 'toggle', hint: undefined } : field\n}\n\ninterface SettingsRow {\n field: FormFieldSchema\n group: SettingsGroup\n}\n\nconst searchResults = computed<SettingsRow[]>(() => {\n if (!isSearching.value) return []\n return visibleSchemaGroups.value.flatMap((group) =>\n visibleFields(group)\n .filter((field) => [field.label, field.hint, group.label].join(' ').toLowerCase().includes(searchTerm.value))\n .map((field) => ({ field, group })),\n )\n})\n\nconst resultCount = computed(() =>\n `${searchResults.value.length} result${searchResults.value.length === 1 ? '' : 's'}`,\n)\n\n/** What the pane renders: one list of search hits, every group (stacked) or the active group. */\nconst paneSections = computed<{ key: string; group?: SettingsGroup; rows: SettingsRow[] }[]>(() => {\n if (isSearching.value) {\n return searchResults.value.length ? [{ key: 'search', rows: searchResults.value }] : []\n }\n const groups = isStacked.value\n ? visibleSchemaGroups.value\n : activeGroup.value ? [activeGroup.value] : []\n return groups.map((group) => ({\n key: group.id,\n group,\n rows: visibleFields(group).map((field) => ({ field, group })),\n }))\n})\n\nconst themeOptions: { value: ThemeMode; label: string; halves: ('light' | 'dark')[] }[] = [\n { value: 'light', label: 'Light', halves: ['light'] },\n { value: 'dark', label: 'Dark', halves: ['dark'] },\n { value: 'system', label: 'System', halves: ['light', 'dark'] },\n]\n\n/** Five stops along the palette's primary → accent hue arc (shortest way round), as in the board. */\nfunction paletteSwatches(key: ColorPalette): string[] {\n const [from, to] = colorPalettes[key].hues\n const span = ((to - from + 540) % 360) - 180\n const tone = key === 'pastel' ? '60% 72%' : '65% 48%'\n return Array.from({ length: 5 }, (_, i) => `hsl(${Math.round(from + (span * i) / 4)} ${tone})`)\n}\n\nconst densityPreviewRows = [\n ['S-0012', '2.41', '12.5'],\n ['S-0013', '7.90', '3.25'],\n ['S-0014', '10.60', '0.80'],\n]\n\nconst densityOptions: { value: TableDensity; label: string }[] = [\n { value: 'compact', label: 'Compact' },\n { value: 'normal', label: 'Normal' },\n { value: 'comfortable', label: 'Comfortable' },\n]\n\nwatch(\n () => [settings.theme, settings.colorPalette, settings.tableDensity],\n () => { applied.value = true },\n)\n\nfunction handleClose() {\n emit('update:open', false)\n emit('close')\n}\n\nfunction builderFieldNames(): string[] {\n return builder.fields.map(field => field.name)\n}\n\nfunction canShowForCurrentUser(item: AccessControlled): boolean {\n return canAccessByPolicy(\n currentAccessUser.value,\n item.access,\n isAccessUserAuthenticated.value,\n )\n}\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n :layout=\"baseLayout\"\n :tabs=\"modalTabs\"\n :active-tab=\"isSearching ? '' : activeTab\"\n @update:active-tab=\"selectTab\"\n @update:open=\"emit('update:open', $event)\"\n @close=\"handleClose\"\n >\n <template #header>\n <div class=\"mint-settings-modal__header\">\n <h3 class=\"mint-modal__title\">{{ title }}</h3>\n <div v-if=\"isSchemaDriven\" class=\"mint-settings-modal__search\">\n <svg class=\"mint-settings-modal__search-icon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"4.5\" /><path d=\"M10.5 10.5L14 14\" />\n </svg>\n <BaseInput\n :model-value=\"query\"\n type=\"search\"\n size=\"sm\"\n placeholder=\"Search settings\"\n aria-label=\"Search settings\"\n @update:model-value=\"query = String($event ?? '')\"\n @keydown=\"handleSearchKeydown\"\n />\n </div>\n </div>\n </template>\n\n <template v-if=\"isRail && isSchemaDriven\" #pane-header=\"{ tab }\">\n <SettingsPaneHeadInternal v-if=\"isSearching\" title=\"Search results\" :count=\"resultCount\" />\n <SettingsPaneHeadInternal\n v-else\n :title=\"tab?.label ?? ''\"\n :description=\"tab?.description\"\n :modified-count=\"activeGroup ? modifiedCount(activeGroup) : undefined\"\n @restore=\"restoreGroup(activeGroup)\"\n />\n </template>\n\n <!-- Body: the active pane only — tab strip / rail / footer are BaseModal's shell. -->\n <div :class=\"['mint-settings-modal', { 'mint-settings-modal--schema': isSchemaDriven }]\">\n <template v-if=\"isSchemaDriven\">\n <SettingsPaneHeadInternal v-if=\"isSearching && !isRail\" title=\"Search results\" :count=\"resultCount\" />\n <section v-for=\"section in paneSections\" :key=\"section.key\" class=\"mint-settings-modal__group\">\n <SettingsPaneHeadInternal\n v-if=\"section.group && !isRail\"\n :title=\"section.group.label\"\n :description=\"section.group.description\"\n :modified-count=\"modifiedCount(section.group)\"\n @restore=\"restoreGroup(section.group)\"\n />\n <div\n class=\"mint-settings-modal__group-grid\"\n :style=\"{ '--mint-settings-cols': section.group?.columns ?? 1 }\"\n >\n <div\n v-for=\"row in section.rows\"\n :key=\"row.field.name\"\n :class=\"['mint-settings-modal__row', { 'mint-settings-modal__row--modified': isModified(row.field) }]\"\n :style=\"row.field.colSpan ? { gridColumn: `span ${row.field.colSpan}` } : undefined\"\n >\n <span v-if=\"!section.group\" class=\"mint-settings-modal__row-group\">{{ row.group.label }}</span>\n <div class=\"mint-settings-modal__row-field\">\n <slot\n :name=\"`field:${row.field.name}`\"\n :field=\"row.field\"\n :form=\"builder.form\"\n :field-props=\"builder.form.getFieldProps(row.field.name)\"\n >\n <FormFieldRendererInternal\n :field=\"settingsField(row.field)\"\n :resolved-props=\"builder.getResolvedFieldProps(settingsField(row.field))\"\n :form=\"builder.form\"\n />\n </slot>\n <p v-if=\"isBooleanField(row.field) && row.field.hint\" class=\"mint-settings-modal__row-hint\">\n {{ row.field.hint }}\n </p>\n <button\n v-if=\"isModified(row.field)\"\n type=\"button\"\n class=\"mint-settings-modal__reset\"\n title=\"Restore default\"\n :aria-label=\"`Restore default for ${row.field.label}`\"\n @click=\"resetField(row.field)\"\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3 8a5 5 0 1 0 1.5-3.5M3 3v2.5h2.5\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n </section>\n <p v-if=\"isSearching && searchResults.length === 0\" class=\"mint-settings-modal__empty\">\n No settings match \"{{ query.trim() }}\"\n </p>\n </template>\n\n <template v-else>\n <section\n v-for=\"tab in manualTabs\"\n v-show=\"isStacked || activeTab === tab.id\"\n :key=\"tab.id\"\n class=\"mint-settings-modal__group\"\n >\n <SettingsPaneHeadInternal v-if=\"isStacked\" :title=\"tab.label\" :description=\"tab.description\" />\n <slot :name=\"`tab-${tab.id}`\" />\n </section>\n </template>\n\n <section\n v-if=\"showAppearance\"\n v-show=\"!isSearching && (isStacked || activeTab === APPEARANCE_TAB_ID)\"\n class=\"mint-settings-modal__group mint-settings-modal__appearance\"\n >\n <SettingsPaneHeadInternal\n v-if=\"isStacked\"\n :title=\"APPEARANCE_TAB.label\"\n :description=\"APPEARANCE_TAB.description\"\n />\n\n <div class=\"mint-settings-modal__setting\">\n <div class=\"mint-settings-modal__setting-text\">\n <span class=\"mint-settings-modal__setting-label\">Theme</span>\n <span class=\"mint-settings-modal__setting-hint\">Applies to this browser only</span>\n </div>\n <div class=\"mint-settings-modal__themes\">\n <button\n v-for=\"opt in themeOptions\"\n :key=\"opt.value\"\n type=\"button\"\n :aria-pressed=\"settings.theme === opt.value\"\n :class=\"['mint-settings-modal__theme', { 'mint-settings-modal__theme--active': settings.theme === opt.value }]\"\n @click=\"settings.theme = opt.value\"\n >\n <span class=\"mint-settings-modal__theme-preview\" aria-hidden=\"true\">\n <span\n v-for=\"half in opt.halves\"\n :key=\"half\"\n :class=\"['mint-settings-modal__theme-half', `mint-settings-modal__theme-half--${half}`]\"\n >\n <span class=\"mint-settings-modal__theme-line\" />\n <span class=\"mint-settings-modal__theme-card\" />\n </span>\n </span>\n <span class=\"mint-settings-modal__theme-label\">\n <span class=\"mint-settings-modal__theme-radio\" aria-hidden=\"true\" />\n {{ opt.label }}\n </span>\n </button>\n </div>\n </div>\n\n <div class=\"mint-settings-modal__setting\">\n <div class=\"mint-settings-modal__setting-text\">\n <span class=\"mint-settings-modal__setting-label\">Color palette</span>\n <span class=\"mint-settings-modal__setting-hint\">Primary and accent hues across the interface</span>\n </div>\n <div class=\"mint-settings-modal__palettes\">\n <button\n v-for=\"(palette, key) in colorPalettes\"\n :key=\"key\"\n type=\"button\"\n :aria-pressed=\"settings.colorPalette === key\"\n :class=\"['mint-settings-modal__palette', { 'mint-settings-modal__palette--active': settings.colorPalette === key }]\"\n @click=\"settings.colorPalette = key as ColorPalette\"\n >\n <span class=\"mint-settings-modal__swatches\" aria-hidden=\"true\">\n <span\n v-for=\"color in paletteSwatches(key as ColorPalette)\"\n :key=\"color\"\n class=\"mint-settings-modal__swatch\"\n :style=\"{ backgroundColor: color }\"\n />\n </span>\n <span class=\"mint-settings-modal__palette-name\">{{ palette.name }}</span>\n <svg class=\"mint-settings-modal__palette-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3.5 8.5l3 3 6-7\" />\n </svg>\n </button>\n </div>\n </div>\n\n <div class=\"mint-settings-modal__setting mint-settings-modal__setting--inline\">\n <div class=\"mint-settings-modal__setting-text\">\n <span class=\"mint-settings-modal__setting-label\">Density</span>\n <span class=\"mint-settings-modal__setting-hint\">Adjusts control and table row height</span>\n </div>\n <SegmentedControl\n :model-value=\"settings.tableDensity\"\n :options=\"densityOptions\"\n size=\"sm\"\n @update:model-value=\"settings.tableDensity = $event as TableDensity\"\n />\n <div\n :class=\"['mint-settings-modal__density-preview', `mint-settings-modal__density-preview--${settings.tableDensity}`]\"\n aria-hidden=\"true\"\n >\n <div class=\"mint-settings-modal__density-row mint-settings-modal__density-row--head\">\n <span>Sample</span><span>RT</span><span>IC50</span>\n </div>\n <div v-for=\"cells in densityPreviewRows\" :key=\"cells[0]\" class=\"mint-settings-modal__density-row\">\n <span>{{ cells[0] }}</span><span>{{ cells[1] }}</span><span>{{ cells[2] }}</span>\n </div>\n </div>\n </div>\n\n <slot name=\"appearance\" />\n </section>\n </div>\n\n <template #footer>\n <slot\n name=\"footer\"\n :values=\"builder.form.data\"\n :close=\"handleClose\"\n >\n <span class=\"mint-settings-modal__status\" role=\"status\">\n <svg v-if=\"applied\" class=\"mint-settings-modal__status-icon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M3.5 8.5l3 3 6-7\" />\n </svg>\n {{ applied ? 'Applied · changes take effect immediately' : 'Changes take effect immediately' }}\n </span>\n <BaseButton class=\"mint-settings-modal__done\" variant=\"primary\" @click=\"handleClose\">Done</BaseButton>\n </slot>\n </template>\n </BaseModal>\n</template>\n"],"mappings":"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"}
|