@morscherlab/mint-sdk 1.4.0-beta.10 → 1.4.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/{AlertBox-B2Bgr3ei.js → AlertBox-BdoZqANv.js} +2 -2
  2. package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-BdoZqANv.js.map} +1 -1
  3. package/dist/{AppSidebar-mvm--GxN.js → AppSidebar-DGK5i-hJ.js} +7 -7
  4. package/dist/{AppSidebar-mvm--GxN.js.map → AppSidebar-DGK5i-hJ.js.map} +1 -1
  5. package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-CbD7BR_s.js} +2 -2
  6. package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-CbD7BR_s.js.map} +1 -1
  7. package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-BoofZZxF.js} +4 -4
  8. package/dist/{ExperimentSelectorModal-65peG4Up.js.map → ExperimentSelectorModal-BoofZZxF.js.map} +1 -1
  9. package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-BkwYOKpG.js} +2 -2
  10. package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-BkwYOKpG.js.map} +1 -1
  11. package/dist/{SettingsModal-DO3IZ7qz.js → SettingsModal-BXsjIVnm.js} +25 -24
  12. package/dist/SettingsModal-BXsjIVnm.js.map +1 -0
  13. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  14. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  15. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  16. package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-BF5g4wRC.js} +2 -2
  17. package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-BF5g4wRC.js.map} +1 -1
  18. package/dist/{data-CgILxDLW.js → data-ihsBd9bs.js} +10 -10
  19. package/dist/{data-CgILxDLW.js.map → data-ihsBd9bs.js.map} +1 -1
  20. package/dist/data.js +7 -7
  21. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  22. package/dist/experiment-DyO54FQB.js.map +1 -0
  23. package/dist/index.js +18 -18
  24. package/dist/{install-CsPFJSx3.js → install-CFgfRO64.js} +13 -13
  25. package/dist/{install-CsPFJSx3.js.map → install-CFgfRO64.js.map} +1 -1
  26. package/dist/install.js +1 -1
  27. package/dist/{lab-MHzp-_J8.js → lab-B-pLwc5U.js} +10 -10
  28. package/dist/{lab-MHzp-_J8.js.map → lab-B-pLwc5U.js.map} +1 -1
  29. package/dist/lab.js +4 -4
  30. package/dist/{records-B6gpM_3e.js → records-DnK_1Q_S.js} +2 -2
  31. package/dist/{records-B6gpM_3e.js.map → records-DnK_1Q_S.js.map} +1 -1
  32. package/dist/{runtime-8bs8EogX.js → runtime-pKfF_iql.js} +9 -9
  33. package/dist/{runtime-8bs8EogX.js.map → runtime-pKfF_iql.js.map} +1 -1
  34. package/dist/runtime.js +7 -7
  35. package/dist/{shell-CywENwBT.js → shell-BIH8Rc21.js} +4 -4
  36. package/dist/{shell-CywENwBT.js.map → shell-BIH8Rc21.js.map} +1 -1
  37. package/dist/shell.js +4 -4
  38. package/dist/stores/index.d.ts +1 -1
  39. package/dist/stores/index.js +2 -2
  40. package/dist/stores/settings.d.ts +4 -12
  41. package/dist/styles.css +165 -43
  42. package/dist/types/platform.d.ts +1 -1
  43. package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-BGN1dmcR.js} +4 -4
  44. package/dist/{useExperimentSamples-DHnxKFAF.js.map → useExperimentSamples-BGN1dmcR.js.map} +1 -1
  45. package/dist/{useListReorder-BitdUhxn.js → useListReorder-DPynGrCj.js} +3 -3
  46. package/dist/{useListReorder-BitdUhxn.js.map → useListReorder-DPynGrCj.js.map} +1 -1
  47. package/dist/{useRackEditor-DiwCt77M.js → useRackEditor-B2FrQA0p.js} +8 -8
  48. package/dist/{useRackEditor-DiwCt77M.js.map → useRackEditor-B2FrQA0p.js.map} +1 -1
  49. package/dist/{useRealtime-C3iy9QkU.js → useRealtime-yWjTTnc9.js} +2 -2
  50. package/dist/{useRealtime-C3iy9QkU.js.map → useRealtime-yWjTTnc9.js.map} +1 -1
  51. package/dist/{useRequest-D506eHi8.js → useRequest-Dqw12k2s.js} +3 -3
  52. package/dist/{useRequest-D506eHi8.js.map → useRequest-Dqw12k2s.js.map} +1 -1
  53. package/package.json +1 -1
  54. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  55. package/src/__tests__/stores/settings.test.ts +21 -21
  56. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  57. package/src/__tests__/styles/v2AuditRules.test.ts +96 -0
  58. package/src/components/GeneratedResultRenderer.vue +3 -4
  59. package/src/components/SettingsModal.vue +7 -12
  60. package/src/stores/index.ts +0 -2
  61. package/src/stores/settings.ts +20 -50
  62. package/src/styles/components/app-layout.css +3 -2
  63. package/src/styles/components/app-top-bar.css +3 -3
  64. package/src/styles/components/dataframe.css +1 -1
  65. package/src/styles/components/modal.css +2 -2
  66. package/src/styles/components/number-input.css +1 -1
  67. package/src/styles/components/progress-bar.css +3 -3
  68. package/src/styles/components/section-card.css +4 -2
  69. package/src/styles/components/settings-modal.css +7 -2
  70. package/src/styles/variables.css +131 -15
  71. package/src/types/platform.ts +1 -1
  72. package/dist/SettingsModal-DO3IZ7qz.js.map +0 -1
  73. package/dist/experiment-43OJtH7R.js.map +0 -1
@@ -5,6 +5,10 @@
5
5
 
6
6
  @import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap');
7
7
 
8
+ /* Tailwind v4's layer order, declared first so the SDK's base and utilities
9
+ layers sort the same way with or without Tailwind in the host. */
10
+ @layer theme, base, components, utilities;
11
+
8
12
  /* ==========================================================================
9
13
  CSS Variables - Light Mode
10
14
  ========================================================================== */
@@ -47,13 +51,8 @@
47
51
  where --text-secondary would drop to ~4.35:1. */
48
52
  --text-secondary-strong: #475569;
49
53
 
50
- /* Brand colors — one teal accent: teal-600 fills, teal-700 hover + ink,
51
- teal-800 pressed. White on #0D9488 is 3.74:1, a known AA exception. */
52
- --color-primary: #0D9488;
53
- --color-primary-hover: #0F766E;
54
- --color-primary-pressed: #115E59;
55
- --color-primary-soft: rgba(13, 148, 136, 0.10);
56
- --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). */
57
56
  /* Fixed teal scale, the same in both themes. */
58
57
  --color-primary-50: #F0FDFA;
59
58
  --color-primary-100: #CCFBF1;
@@ -135,6 +134,8 @@
135
134
  --form-height-md: 2rem;
136
135
  --form-height-lg: 2.5rem;
137
136
  --mint-gutter: 0.5rem;
137
+ /* Distance from the outermost cards to the window edge, in every density. */
138
+ --mint-page-inset: 1rem;
138
139
 
139
140
  /* Semantic colors */
140
141
  --mint-success: #10B981;
@@ -169,7 +170,7 @@
169
170
  /* Soft focus rings — 3px translucent. Used by all form primitives
170
171
  post-0.16 visual refresh. Solid variants above kept for legacy
171
172
  components that haven't migrated yet. */
172
- --focus-ring-soft: 0 0 0 3px rgba(13, 148, 136, 0.18);
173
+ --focus-ring-soft: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
173
174
  /* Red is perceptually brighter than teal at equal alpha; drop 2pp to keep visual weight matched. */
174
175
  --focus-ring-soft-error: 0 0 0 3px rgba(239, 68, 68, 0.18);
175
176
 
@@ -207,12 +208,7 @@ html.dark {
207
208
  --border-light: #172033;
208
209
  --border-strong: #334155;
209
210
 
210
- /* Brand colors - Dark mode (CTA, purple and entity aliases follow) */
211
- --color-primary: #2DD4BF;
212
- --color-primary-hover: #5EEAD4;
213
- --color-primary-pressed: #99F6E4;
214
- --color-primary-soft: rgba(45, 212, 191, 0.12);
215
- --color-primary-border: rgba(45, 212, 191, 0.35);
211
+ /* Brand colors - Dark mode: the accent comes from the palette blocks below. */
216
212
  /* Dark ink on the light accent fill (10.1:1 on #2DD4BF). */
217
213
  --text-on-accent: #0B1120;
218
214
  --color-experiment-hover: #E2E8F0;
@@ -248,7 +244,7 @@ html.dark {
248
244
 
249
245
  /* Focus ring - dark mode offset uses bg-primary instead of white */
250
246
  --focus-ring-offset: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--color-primary);
251
- --focus-ring-soft: 0 0 0 3px rgba(45, 212, 191, 0.22);
247
+ --focus-ring-soft: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent);
252
248
  --focus-ring-soft-error: 0 0 0 3px rgba(248, 113, 113, 0.26);
253
249
 
254
250
  /* Legacy aliases */
@@ -258,6 +254,122 @@ html.dark {
258
254
  color-scheme: dark;
259
255
  }
260
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
+
261
373
  /* Comfortable density (html[data-density], written by the settings store):
262
374
  the pre-v2 control heights and a wider gutter. */
263
375
  [data-density="comfortable"] {
@@ -344,6 +456,9 @@ button, input, select, textarea {
344
456
  /* ==========================================================================
345
457
  MINT Utility Classes (for Tailwind v4 compatibility)
346
458
  ========================================================================== */
459
+ /* In Tailwind's utilities layer: unlayered, these beat every layered rule, so a
460
+ host's responsive variant (`lg:flex-row`) lost to the SDK's `.flex-col`. */
461
+ @layer utilities {
347
462
  .bg-bg-hover {
348
463
  background-color: var(--bg-tertiary);
349
464
  }
@@ -719,6 +834,7 @@ button, input, select, textarea {
719
834
  .max-w-sm {
720
835
  max-width: 24rem;
721
836
  }
837
+ } /* @layer utilities */
722
838
 
723
839
  /* ==========================================================================
724
840
  Reduced Motion
@@ -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' | 'viridis' | 'pastel'
69
+ export type ColorPalette = 'default' | 'colorblind' | 'purple' | 'ocean' | 'forest'
70
70
  export type TableDensity = 'compact' | 'normal' | 'comfortable'
@@ -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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"experiment-43OJtH7R.js","names":[],"sources":["../src/apiErrors.ts","../src/stores/settings.ts","../src/composables/useApi.ts","../src/utils/errors.ts","../src/utils/integrationHost.ts","../src/stores/experiment.ts"],"sourcesContent":["export interface MintApiErrorEnvelope {\n code: string\n message: string\n status: number\n request_id: string | null\n details: unknown\n detail?: unknown\n}\n\nconst STATUS_MESSAGES: Readonly<Record<number, string>> = {\n 400: 'Bad request',\n 401: 'Authentication required',\n 403: 'Permission denied',\n 404: 'Resource not found',\n 405: 'Method not allowed',\n 409: 'Conflict',\n 413: 'Payload too large',\n 415: 'Unsupported media type',\n 422: 'Validation error',\n 429: 'Rate limit exceeded',\n 500: 'Internal server error',\n 502: 'Bad gateway',\n 503: 'Service unavailable',\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n return value as Record<string, unknown>\n}\n\n/** Public error thrown by typed MINT SDK requests. */\nexport class MintApiError extends Error {\n override readonly name: string = 'MintApiError'\n\n constructor(\n message: string,\n readonly code: string,\n readonly status: number,\n readonly requestId: string | null,\n readonly details: unknown,\n readonly detail: unknown,\n readonly body: unknown,\n readonly cause?: unknown,\n ) {\n super(message)\n }\n}\n\nexport interface MintApiErrorOptions {\n requestId?: string | null\n cause?: unknown\n}\n\n/** Normalize current typed envelopes and the previous FastAPI detail shape. */\nexport function mintApiErrorFromResponse(\n status: number,\n payload: unknown,\n options: MintApiErrorOptions = {},\n): MintApiError {\n const body = asRecord(payload)\n const legacyDetail = body?.detail\n const inner = asRecord(legacyDetail) ?? body\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value ? value : undefined\n // Complete envelopes own the public code; older wrappers defer to the plugin payload.\n const canonical = typeof body?.status === 'number' && text(body.code) && text(body.message)\n const code = (canonical ? text(body.code) : text(inner?.error) ?? text(inner?.code))\n ?? text(body?.code) ?? 'unknown'\n const message = (canonical ? text(body.message) : text(inner?.message))\n ?? text(body?.message) ?? text(legacyDetail)\n ?? STATUS_MESSAGES[status] ?? `HTTP ${status}`\n const details = body?.details ?? inner?.details ?? null\n const requestId = text(body?.request_id) ?? text(inner?.request_id) ?? options.requestId ?? null\n\n return new MintApiError(\n message,\n code,\n status,\n requestId,\n details,\n legacyDetail,\n payload,\n options.cause,\n )\n}\n","import { defineStore, getActivePinia } from 'pinia'\nimport { ref, watch } from 'vue'\nimport type { ThemeMode, ColorPalette, TableDensity } from '../types'\n\ndeclare global {\n interface ImportMetaEnv {\n readonly VITE_API_PREFIX?: string\n }\n\n interface ImportMeta {\n readonly env: ImportMetaEnv\n }\n}\n\nexport interface SettingsState {\n serverHost: string\n serverPort: number\n requestTimeout: number\n wsAutoReconnect: boolean\n wsReconnectInterval: number\n theme: ThemeMode\n colorPalette: ColorPalette\n tableDensity: TableDensity\n}\n\nconst STORAGE_KEY = 'mint-settings'\nconst LEGACY_STORAGE_KEY = 'mld-settings'\n\nfunction getDefaultServerHost(): string {\n if (typeof window !== 'undefined' && window.location.hostname !== 'localhost') {\n return window.location.hostname\n }\n return 'localhost'\n}\n\nfunction getDefaultServerPort(): number {\n if (typeof window !== 'undefined') {\n if (window.location.port) {\n return parseInt(window.location.port, 10)\n }\n // Standard ports: 443 for HTTPS, 80 for HTTP (browser omits from location.port)\n return window.location.protocol === 'https:' ? 443 : 80\n }\n return 8000\n}\n\nconst defaultSettings: SettingsState = {\n serverHost: getDefaultServerHost(),\n serverPort: getDefaultServerPort(),\n requestTimeout: 120000,\n wsAutoReconnect: true,\n wsReconnectInterval: 5000,\n theme: 'system',\n colorPalette: 'default',\n tableDensity: 'normal',\n}\n\nexport const colorPalettes: Record<ColorPalette, { name: string; hues: [number, number] }> = {\n default: { name: 'Default (Cyan-Pink)', hues: [180, 320] },\n colorblind: { name: 'Colorblind-friendly', hues: [45, 260] },\n viridis: { name: 'Viridis', hues: [280, 80] },\n pastel: { name: 'Pastel', hues: [200, 340] },\n}\n\n/**\n * Custom properties a non-default palette overrides on `<html>`.\n *\n * The saturation/lightness pairs are lifted from the original indigo/orange\n * tokens in `styles/variables.css` so only the hue moves; the `default`\n * palette writes nothing at all and the stylesheet values stand.\n */\nexport const PALETTE_CSS_VARIABLES = [\n '--color-primary',\n '--color-primary-hover',\n '--color-primary-soft',\n '--color-cta',\n '--color-cta-hover',\n] as const\n\n/** Build the `<html>` custom-property overrides for a palette. */\nexport function paletteCssVariables(palette: ColorPalette): Record<string, string> {\n if (palette === 'default') return {}\n const [primaryHue, accentHue] = colorPalettes[palette].hues\n return {\n '--color-primary': `hsl(${primaryHue} 84% 67%)`,\n '--color-primary-hover': `hsl(${primaryHue} 75% 59%)`,\n '--color-primary-soft': `hsl(${primaryHue} 84% 67% / 0.12)`,\n '--color-cta': `hsl(${accentHue} 95% 53%)`,\n '--color-cta-hover': `hsl(${accentHue} 90% 48%)`,\n }\n}\n\n/** `DataFrame`-style size that each table density maps onto. */\nconst DENSITY_TABLE_SIZE: Record<TableDensity, 'sm' | 'md' | 'lg'> = {\n compact: 'sm',\n normal: 'md',\n comfortable: 'lg',\n}\n\n/** Map a table density onto the `size` prop shared by the SDK table components. */\nexport function tableDensityToSize(density: TableDensity): 'sm' | 'md' | 'lg' {\n return DENSITY_TABLE_SIZE[density]\n}\n\nfunction loadSettings(): SettingsState {\n try {\n const stored = localStorage.getItem(STORAGE_KEY)\n if (stored) {\n const parsed = JSON.parse(stored)\n return { ...defaultSettings, ...parsed }\n }\n\n const legacyStored = localStorage.getItem(LEGACY_STORAGE_KEY)\n if (legacyStored) {\n const parsed = JSON.parse(legacyStored)\n const migrated = { ...defaultSettings, ...parsed }\n localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated))\n localStorage.removeItem(LEGACY_STORAGE_KEY)\n return migrated\n }\n } catch (e) {\n console.warn('Failed to load settings from localStorage:', e)\n }\n return { ...defaultSettings }\n}\n\nfunction saveSettings(settings: SettingsState): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))\n } catch (e) {\n console.warn('Failed to save settings to localStorage:', e)\n }\n}\n\nfunction getSystemPrefersDark(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n}\n\nexport const useSettingsStore = defineStore('mint-settings', () => {\n // State\n const serverHost = ref(defaultSettings.serverHost)\n const serverPort = ref(defaultSettings.serverPort)\n const requestTimeout = ref(defaultSettings.requestTimeout)\n const wsAutoReconnect = ref(defaultSettings.wsAutoReconnect)\n const wsReconnectInterval = ref(defaultSettings.wsReconnectInterval)\n const theme = ref<ThemeMode>(defaultSettings.theme)\n const systemPrefersDark = ref(getSystemPrefersDark())\n const colorPalette = ref<ColorPalette>(defaultSettings.colorPalette)\n const tableDensity = ref<TableDensity>(defaultSettings.tableDensity)\n\n // API prefix - can be configured via env variable VITE_API_PREFIX\n const apiPrefix: string = (import.meta.env?.VITE_API_PREFIX as string | undefined) ?? '/api'\n\n function _isSameOrigin(): { same: boolean; currentHost: string; currentPort: string } {\n const currentHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost'\n const currentPort = typeof window !== 'undefined' ? window.location.port : '8000'\n const effectivePort = currentPort || (window.location.protocol === 'https:' ? '443' : '80')\n const same =\n (serverHost.value === currentHost && String(serverPort.value) === effectivePort) ||\n (serverHost.value === 'localhost' && currentHost !== 'localhost')\n return { same, currentHost, currentPort }\n }\n\n function getApiBaseUrl(): string {\n const { same } = _isSameOrigin()\n if (same) return apiPrefix\n return `http://${serverHost.value}:${serverPort.value}${apiPrefix}`\n }\n\n function getWsBaseUrl(): string {\n const { same, currentHost, currentPort } = _isSameOrigin()\n const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'\n if (same) return `${protocol}//${currentHost}${currentPort ? ':' + currentPort : ''}${apiPrefix}`\n return `ws://${serverHost.value}:${serverPort.value}${apiPrefix}`\n }\n\n let _initialized = false\n\n function initialize() {\n if (_initialized) return\n _initialized = true\n\n const loaded = loadSettings()\n serverHost.value = loaded.serverHost\n serverPort.value = loaded.serverPort\n requestTimeout.value = loaded.requestTimeout\n wsAutoReconnect.value = loaded.wsAutoReconnect\n wsReconnectInterval.value = loaded.wsReconnectInterval\n theme.value = loaded.theme\n colorPalette.value = loaded.colorPalette\n tableDensity.value = loaded.tableDensity\n\n applyTheme()\n applyPalette()\n applyDensity()\n }\n\n function applyTheme() {\n if (typeof document === 'undefined') return\n const dark = theme.value === 'system' ? systemPrefersDark.value : theme.value === 'dark'\n document.documentElement.classList.toggle('dark', dark)\n }\n\n /**\n * Write the selected palette's hues onto `<html>` as custom-property\n * overrides. The `default` palette clears them so `styles/variables.css`\n * keeps ownership of the brand colors.\n */\n function applyPalette() {\n if (typeof document === 'undefined') return\n const style = document.documentElement.style\n const overrides = paletteCssVariables(colorPalette.value)\n for (const name of PALETTE_CSS_VARIABLES) {\n const value = overrides[name]\n if (value) style.setProperty(name, value)\n else style.removeProperty(name)\n }\n }\n\n /** Mirror the table density onto `<html data-density>` for CSS consumers. */\n function applyDensity() {\n if (typeof document === 'undefined') return\n document.documentElement.dataset.density = tableDensity.value\n }\n\n watch(theme, () => {\n applyTheme()\n persistSettings()\n })\n\n watch(colorPalette, applyPalette)\n watch(tableDensity, applyDensity)\n\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {\n systemPrefersDark.value = event.matches\n if (theme.value === 'system') {\n applyTheme()\n }\n })\n }\n\n function persistSettings() {\n saveSettings({\n serverHost: serverHost.value,\n serverPort: serverPort.value,\n requestTimeout: requestTimeout.value,\n wsAutoReconnect: wsAutoReconnect.value,\n wsReconnectInterval: wsReconnectInterval.value,\n theme: theme.value,\n colorPalette: colorPalette.value,\n tableDensity: tableDensity.value,\n })\n }\n\n watch([serverHost, serverPort, requestTimeout, wsAutoReconnect, wsReconnectInterval, colorPalette, tableDensity], () => {\n persistSettings()\n })\n\n function resetToDefaults() {\n serverHost.value = defaultSettings.serverHost\n serverPort.value = defaultSettings.serverPort\n requestTimeout.value = defaultSettings.requestTimeout\n wsAutoReconnect.value = defaultSettings.wsAutoReconnect\n wsReconnectInterval.value = defaultSettings.wsReconnectInterval\n theme.value = defaultSettings.theme\n colorPalette.value = defaultSettings.colorPalette\n tableDensity.value = defaultSettings.tableDensity\n }\n\n function getPaletteHues(): [number, number] {\n return colorPalettes[colorPalette.value].hues\n }\n\n /** The `size` the SDK table components default to for the current density. */\n function getTableSize(): 'sm' | 'md' | 'lg' {\n return tableDensityToSize(tableDensity.value)\n }\n\n function isDark(): boolean {\n return theme.value === 'system' ? systemPrefersDark.value : theme.value === 'dark'\n }\n\n return {\n serverHost,\n serverPort,\n requestTimeout,\n wsAutoReconnect,\n wsReconnectInterval,\n theme,\n systemPrefersDark,\n colorPalette,\n tableDensity,\n initialize,\n applyTheme,\n applyPalette,\n applyDensity,\n persistSettings,\n resetToDefaults,\n getPaletteHues,\n getTableSize,\n isDark,\n getApiBaseUrl,\n getWsBaseUrl,\n }\n})\n\n/**\n * Read the settings store without requiring Pinia to be installed.\n *\n * SDK components are mounted standalone (unit tests, Histoire stories, plugin\n * previews) where no Pinia instance exists. They use this to pick up a user's\n * preferences when the platform is running and fall back to their own defaults\n * otherwise, instead of throwing.\n */\nexport function tryUseSettingsStore(): ReturnType<typeof useSettingsStore> | null {\n if (!getActivePinia()) return null\n try {\n return useSettingsStore()\n } catch {\n return null\n }\n}\n","import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'\nimport { useSettingsStore } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\nimport { MintApiError, mintApiErrorFromResponse } from '../apiErrors'\n\nlet apiClientInstance: AxiosInstance | null = null\nlet interceptorAttached = false\n\ninterface MintAxiosRequestConfig extends AxiosRequestConfig {\n _mintSkipAuth?: boolean\n _mintAuthToken?: string\n}\n\ninterface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {\n _mintSkipAuth?: boolean\n _mintAuthToken?: string\n}\n\ntype MutableHeaders = Record<string, unknown> & {\n has?: (header: string) => boolean\n set?: (header: string, value: string) => void\n delete?: (header: string) => void\n}\n\nfunction joinUrlPath(baseUrl: string, path: string): string {\n if (/^https?:\\/\\//i.test(path)) return path\n if (!path) return baseUrl\n if (path.startsWith('?') || path.startsWith('#')) return `${baseUrl.replace(/\\/+$/, '')}${path}`\n const normalizedBase = baseUrl.replace(/\\/+$/, '')\n const normalizedPath = path.replace(/^\\/+/, '/')\n return `${normalizedBase}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`\n}\n\nfunction getBasePath(baseUrl: string): string {\n if (!baseUrl) return '/'\n\n try {\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n return new URL(baseUrl, origin).pathname.replace(/\\/+$/, '') || '/'\n } catch {\n const path = baseUrl.replace(/^https?:\\/\\/[^/]+/i, '')\n return path.replace(/\\/+$/, '') || '/'\n }\n}\n\nfunction normalizeRequestUrl(baseUrl: string, url: string): string {\n if (!url || /^https?:\\/\\//.test(url)) return url\n\n const basePath = getBasePath(baseUrl)\n if (basePath === '/') return url\n\n const normalizedUrl = url.startsWith('/') ? url : `/${url}`\n if (\n normalizedUrl === basePath\n || normalizedUrl.startsWith(`${basePath}?`)\n || normalizedUrl.startsWith(`${basePath}#`)\n ) {\n return normalizedUrl.slice(basePath.length)\n }\n if (normalizedUrl.startsWith(`${basePath}/`)) {\n return normalizedUrl.slice(basePath.length) || ''\n }\n return url\n}\n\nfunction asMutableHeaders(headers: AxiosRequestConfig['headers']): MutableHeaders | null {\n return headers ? (headers as MutableHeaders) : null\n}\n\nfunction hasAuthorizationHeader(headers: AxiosRequestConfig['headers']): boolean {\n const bag = asMutableHeaders(headers)\n if (!bag) return false\n if (typeof bag.has === 'function') return bag.has('Authorization')\n return Object.keys(bag).some((key) => key.toLowerCase() === 'authorization')\n}\n\nfunction setAuthorizationHeader(headers: AxiosRequestConfig['headers'], value: string): void {\n const bag = asMutableHeaders(headers)\n if (!bag) return\n if (typeof bag.set === 'function') {\n bag.set('Authorization', value)\n return\n }\n bag.Authorization = value\n}\n\nfunction deleteAuthorizationHeader(headers: AxiosRequestConfig['headers']): void {\n const bag = asMutableHeaders(headers)\n if (!bag) return\n if (typeof bag.delete === 'function') {\n bag.delete('Authorization')\n return\n }\n for (const key of Object.keys(bag)) {\n if (key.toLowerCase() === 'authorization') delete bag[key]\n }\n}\n\nfunction getAuthorizationHeader(headers: AxiosRequestConfig['headers']): unknown {\n const bag = asMutableHeaders(headers) as (MutableHeaders & { get?: (header: string) => unknown }) | null\n if (!bag) return undefined\n if (typeof bag.get === 'function') return bag.get('Authorization')\n const key = Object.keys(bag).find((candidate) => candidate.toLowerCase() === 'authorization')\n return key ? bag[key] : undefined\n}\n\n/**\n * Raised when an authenticated SDK request is rejected because the login is no\n * longer valid. It is also a `MintApiError` (status 401), so typed-error callers\n * and `instanceof AuthenticationRequiredError` callers both recognise it.\n */\nexport class AuthenticationRequiredError extends MintApiError {\n override readonly name: string = 'AuthenticationRequiredError'\n declare readonly code: 'AUTHENTICATION_REQUIRED'\n declare readonly status: 401\n\n constructor(cause?: unknown, envelope?: MintApiError) {\n super(\n 'Authentication required. Please sign in again.',\n 'AUTHENTICATION_REQUIRED',\n 401,\n envelope?.requestId ?? null,\n envelope?.details ?? null,\n envelope?.detail,\n envelope?.body ?? null,\n cause,\n )\n }\n}\n\n/** @internal Convert a rejected authenticated request into the SDK's public auth error. */\nexport function authenticationRequiredError(\n cause: unknown,\n rejectedToken?: string,\n envelope?: MintApiError,\n): AuthenticationRequiredError {\n const currentAuthStore = useAuthStore()\n if (rejectedToken && currentAuthStore.token === rejectedToken) {\n currentAuthStore.clearToken()\n }\n return new AuthenticationRequiredError(cause, envelope)\n}\n\nfunction getApiClient(): AxiosInstance {\n if (!apiClientInstance) {\n apiClientInstance = axios.create({\n headers: {\n 'Content-Type': 'application/json',\n },\n })\n }\n return apiClientInstance\n}\n\nexport interface ApiClientOptions {\n baseUrl?: string\n timeout?: number\n withAuth?: boolean\n}\n\nexport interface UseApiReturn {\n client: AxiosInstance\n get: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>\n post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n patch: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n delete: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>\n upload: <T>(url: string, file: File, fieldName?: string, additionalData?: Record<string, unknown>) => Promise<T>\n download: (url: string, filename?: string) => Promise<string>\n buildUrl: (path: string) => string\n buildWsUrl: (path: string) => string\n}\n\n/** Axios-based API client that injects the plugin base URL and JWT auth header on every request. A failed response rejects with a `MintApiError`. */\nexport function useApi(options: ApiClientOptions = {}): UseApiReturn {\n const settingsStore = useSettingsStore()\n const authStore = useAuthStore()\n const apiClient = getApiClient()\n\n // Ensure auth store is initialized (reads token from localStorage)\n if (!authStore.isInitialized) {\n authStore.initialize()\n }\n\n // Attach auth interceptor only once (reads token dynamically, not from closure)\n if (!interceptorAttached) {\n apiClient.interceptors.request.use((config) => {\n const request = config as MintInternalAxiosRequestConfig\n if (request._mintSkipAuth) {\n deleteAuthorizationHeader(request.headers)\n return request\n }\n\n const currentAuthStore = useAuthStore()\n if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) {\n setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`)\n }\n if (getAuthorizationHeader(config.headers) === `Bearer ${currentAuthStore.token}`) {\n request._mintAuthToken = currentAuthStore.token ?? undefined\n }\n return config\n })\n apiClient.interceptors.response.use(\n (response) => response,\n async (error: unknown) => {\n if (axios.isAxiosError(error) && error.response) {\n const request = error.config as MintInternalAxiosRequestConfig | undefined\n let payload: unknown = error.response.data\n if (payload instanceof Blob) {\n try {\n payload = JSON.parse(await payload.text())\n } catch {\n // Keep non-JSON download errors available as the original body.\n }\n }\n const typed = mintApiErrorFromResponse(\n error.response.status,\n payload,\n {\n requestId: String(error.response.headers?.['x-request-id'] ?? '') || null,\n cause: error,\n },\n )\n if (error.response.status === 401 && request?._mintSkipAuth !== true) {\n return Promise.reject(authenticationRequiredError(error, request?._mintAuthToken, typed))\n }\n return Promise.reject(typed)\n }\n return Promise.reject(error)\n },\n )\n interceptorAttached = true\n }\n\n function getBaseUrl(): string {\n return options.baseUrl ?? settingsStore.getApiBaseUrl()\n }\n\n function normalizeUrl(url: string): string {\n return normalizeRequestUrl(getBaseUrl(), url)\n }\n\n // Build per-request config that applies this caller's options\n function requestConfig(config?: AxiosRequestConfig): MintAxiosRequestConfig {\n const base: MintAxiosRequestConfig = {\n baseURL: getBaseUrl(),\n timeout: options.timeout ?? settingsStore.requestTimeout,\n ...config,\n }\n // Strip auth header if explicitly disabled\n if (options.withAuth === false) {\n base._mintSkipAuth = true\n deleteAuthorizationHeader(base.headers)\n }\n return base\n }\n\n // Generic request methods\n async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.get<T>(normalizeUrl(url), requestConfig(config))\n return response.data\n }\n\n async function post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.post<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.put<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.patch<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.delete<T>(normalizeUrl(url), requestConfig(config))\n return response.data\n }\n\n // File upload helper\n async function upload<T>(url: string, file: File, fieldName = 'file', additionalData?: Record<string, unknown>): Promise<T> {\n const formData = new FormData()\n formData.append(fieldName, file)\n\n if (additionalData) {\n Object.entries(additionalData).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n formData.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value))\n }\n })\n }\n\n const response = await apiClient.post<T>(normalizeUrl(url), formData, requestConfig({\n // Let Axios set Content-Type with the correct multipart boundary\n headers: { 'Content-Type': undefined },\n }))\n return response.data\n }\n\n // Download helper - returns blob URL\n async function download(url: string, filename?: string): Promise<string> {\n const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: 'blob' }))\n const blob = new Blob([response.data])\n const blobUrl = URL.createObjectURL(blob)\n\n // Optionally trigger download\n if (filename) {\n const link = document.createElement('a')\n link.href = blobUrl\n link.download = filename\n document.body.appendChild(link)\n link.click()\n document.body.removeChild(link)\n // Revoke after a short delay to allow download to start;\n // return empty string since the URL will be invalidated\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100)\n return ''\n }\n\n // Caller is responsible for revoking when no filename is provided\n return blobUrl\n }\n\n // Build full URL for external use (e.g., <a href=\"...\">)\n function buildUrl(path: string): string {\n const baseUrl = getBaseUrl()\n return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path))\n }\n\n // WebSocket URL builder\n function buildWsUrl(path: string): string {\n return joinUrlPath(settingsStore.getWsBaseUrl(), path)\n }\n\n return {\n client: apiClient,\n get,\n post,\n put,\n patch,\n delete: del,\n upload,\n download,\n buildUrl,\n buildWsUrl,\n }\n}\n","/**\n * The one place a thrown value is turned into a developer-facing message.\n *\n * Five copies of this logic drifted apart across the composables and stores\n * (some checked a bare `{ message }` object, some did not); this export is the\n * union of what they all accepted, so every caller now reads the same message\n * from the same value.\n */\nexport function readErrorMessage(value: unknown, fallback = 'Request failed.'): string {\n if (value instanceof Error && value.message) {\n return value.message\n }\n\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n\n if (\n typeof value === 'object'\n && value !== null\n && 'message' in value\n && typeof (value as { message?: unknown }).message === 'string'\n && (value as { message: string }).message.trim()\n ) {\n return (value as { message: string }).message\n }\n\n return fallback\n}\n","// The platform integration script (D7-B) sets `window.__MINT_INTEGRATION__` in\n// every plugin page the platform serves. SDK code calls into it and never\n// imports platform code; standalone plugins have no host.\n\n/** What the SDK experiment store gives the platform integration script. */\nexport interface ExperimentStoreBinding {\n /** The experiment id the store holds, or `null`. */\n currentId(): number | null\n /** Resolve the experiment from the platform and make it the current one. */\n selectById(id: number): Promise<unknown>\n /** Call `listener` with the new id each time the current experiment changes. */\n onChange(listener: (id: number | null) => void): void\n}\n\n/** The platform side of the plugin page integration (`window.__MINT_INTEGRATION__`). */\nexport interface MintIntegrationHost {\n /** Connect the experiment store: the host writes the experiment in the URL into it and writes its changes back to the URL. */\n bindExperimentStore(binding: ExperimentStoreBinding): void\n /** Render the platform page selector into `element`. Returns the function that removes it. */\n mountPageSelector(element: HTMLElement): () => void\n}\n\n/** The integration host of this page, or `undefined` outside a platform-served plugin page. */\nexport function integrationHost(): MintIntegrationHost | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as Window & { __MINT_INTEGRATION__?: MintIntegrationHost }).__MINT_INTEGRATION__\n}\n","import { defineStore } from 'pinia'\nimport { ref, watch } from 'vue'\nimport { useApi } from '../composables/useApi'\nimport { readErrorMessage } from '../utils/errors'\nimport { integrationHost } from '../utils/integrationHost'\nimport type { ExperimentSummary, PlatformContext } from '../types'\n\nfunction getPlatformApiUrl(): string | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n ?.platformApiUrl\n}\n\n/**\n * The single source of truth for the experiment a plugin is currently working on.\n *\n * Both entry points land here: `ExperimentSelectorModal` calls `select()` with a\n * record the platform already returned, and deep links call `selectById()` with\n * nothing but an id. Because a name can only ever arrive from the platform,\n * there is no code path in which a caller can invent one.\n *\n * In a platform-served plugin page the platform integration script keeps the\n * store and the `experiment_id` URL parameter in step.\n */\nexport const useExperimentStore = defineStore('mint-experiment', () => {\n const current = ref<ExperimentSummary | null>(null)\n const currentId = ref<number | null>(null)\n const isResolving = ref(false)\n const error = ref<string | null>(null)\n\n // In-flight requests only. Resolved records are deliberately not memoised, so\n // a rename is picked up on the next resolve instead of persisting for the\n // lifetime of the page.\n const inFlight = new Map<number, Promise<ExperimentSummary>>()\n\n // Monotonic token guarding every write to the shared selection. Responses can\n // land in any order, and a late one must never overwrite a newer choice —\n // that would display a name belonging to a different experiment, which is the\n // failure this store exists to prevent.\n let selectionToken = 0\n\n function fetchExperiment(id: number): Promise<ExperimentSummary> {\n const pending = inFlight.get(id)\n if (pending) return pending\n\n const base = getPlatformApiUrl() ?? ''\n const request = useApi()\n .get<ExperimentSummary>(`${base}/experiments/${id}`)\n .finally(() => {\n inFlight.delete(id)\n })\n\n inFlight.set(id, request)\n return request\n }\n\n /** Adopt a record the platform already returned, without a further request. */\n function select(experiment: ExperimentSummary): void {\n selectionToken += 1\n current.value = experiment\n currentId.value = experiment.id\n error.value = null\n isResolving.value = false\n }\n\n /**\n * Bind to an experiment known only by id, resolving its record from the\n * platform. On failure the binding is remembered but `current` stays null, so\n * callers can distinguish \"not resolved yet\" from \"nothing selected\".\n */\n async function selectById(id: number): Promise<ExperimentSummary | null> {\n const joinsCurrentResolution = isResolving.value && currentId.value === id\n const token = joinsCurrentResolution ? selectionToken : (selectionToken += 1)\n if (!joinsCurrentResolution) {\n current.value = null\n currentId.value = id\n error.value = null\n isResolving.value = true\n }\n try {\n const experiment = await fetchExperiment(id)\n // A newer selection superseded this one while the request was in flight.\n // Returning null tells the caller its result is no longer the selection,\n // so it cannot be applied downstream either.\n if (token !== selectionToken) return null\n current.value = experiment\n return experiment\n } catch (err) {\n if (token !== selectionToken) return null\n current.value = null\n error.value = readErrorMessage(err, 'Failed to load experiment')\n return null\n } finally {\n // Only the newest request owns the loading flag; otherwise a superseded\n // response would clear it while the current one is still pending.\n if (token === selectionToken) isResolving.value = false\n }\n }\n\n /** Read the selected experiment again. The selection and its record stay in place. */\n async function refresh(): Promise<void> {\n const id = currentId.value\n if (id === null) return\n const token = selectionToken\n try {\n const experiment = await fetchExperiment(id)\n if (token !== selectionToken) return\n current.value = experiment\n error.value = null\n } catch (err) {\n if (token === selectionToken) error.value = readErrorMessage(err, 'Failed to load experiment')\n }\n }\n\n function clear(): void {\n // Supersede any in-flight resolve so a detached experiment cannot be\n // repopulated by a response that was already on its way.\n selectionToken += 1\n current.value = null\n currentId.value = null\n error.value = null\n isResolving.value = false\n }\n\n integrationHost()?.bindExperimentStore({\n currentId: () => currentId.value,\n selectById,\n onChange: (listener) => { watch(currentId, listener) },\n })\n\n return {\n current,\n currentId,\n isResolving,\n error,\n select,\n selectById,\n refresh,\n clear,\n }\n})\n"],"mappings":";;;;;AASA,IAAM,kBAAoD;CACxD,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;AAEA,SAAS,SAAS,OAAgD;CAChE,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO;CACxE,OAAO;AACT;;AAGA,IAAa,eAAb,cAAkC,MAAM;CAK3B;CACA;CACA;CACA;CACA;CACA;CACA;CAVX,OAAiC;CAEjC,YACE,SACA,MACA,QACA,WACA,SACA,QACA,MACA,OACA;EACA,MAAM,OAAO;EARJ,KAAA,OAAA;EACA,KAAA,SAAA;EACA,KAAA,YAAA;EACA,KAAA,UAAA;EACA,KAAA,SAAA;EACA,KAAA,OAAA;EACA,KAAA,QAAA;CAGX;AACF;;AAQA,SAAgB,yBACd,QACA,SACA,UAA+B,CAAC,GAClB;CACd,MAAM,OAAO,SAAS,OAAO;CAC7B,MAAM,eAAe,MAAM;CAC3B,MAAM,QAAQ,SAAS,YAAY,KAAK;CACxC,MAAM,QAAQ,UACZ,OAAO,UAAU,YAAY,QAAQ,QAAQ,KAAA;CAE/C,MAAM,YAAY,OAAO,MAAM,WAAW,YAAY,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,OAAO;CAC1F,MAAM,QAAQ,YAAY,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,KAAK,OAAO,IAAI,MAC7E,KAAK,MAAM,IAAI,KAAK;CACzB,MAAM,WAAW,YAAY,KAAK,KAAK,OAAO,IAAI,KAAK,OAAO,OAAO,MAChE,KAAK,MAAM,OAAO,KAAK,KAAK,YAAY,KACxC,gBAAgB,WAAW,QAAQ;CACxC,MAAM,UAAU,MAAM,WAAW,OAAO,WAAW;CAGnD,OAAO,IAAI,aACT,SACA,MACA,QALgB,KAAK,MAAM,UAAU,KAAK,KAAK,OAAO,UAAU,KAAK,QAAQ,aAAa,MAO1F,SACA,cACA,SACA,QAAQ,KACV;AACF;;;AC3DA,IAAM,cAAc;AACpB,IAAM,qBAAqB;AAE3B,SAAS,uBAA+B;CACtC,IAAI,OAAO,WAAW,eAAe,OAAO,SAAS,aAAa,aAChE,OAAO,OAAO,SAAS;CAEzB,OAAO;AACT;AAEA,SAAS,uBAA+B;CACtC,IAAI,OAAO,WAAW,aAAa;EACjC,IAAI,OAAO,SAAS,MAClB,OAAO,SAAS,OAAO,SAAS,MAAM,EAAE;EAG1C,OAAO,OAAO,SAAS,aAAa,WAAW,MAAM;CACvD;CACA,OAAO;AACT;AAEA,IAAM,kBAAiC;CACrC,YAAY,qBAAqB;CACjC,YAAY,qBAAqB;CACjC,gBAAgB;CAChB,iBAAiB;CACjB,qBAAqB;CACrB,OAAO;CACP,cAAc;CACd,cAAc;AAChB;AAEA,IAAa,gBAAgF;CAC3F,SAAS;EAAE,MAAM;EAAuB,MAAM,CAAC,KAAK,GAAG;CAAE;CACzD,YAAY;EAAE,MAAM;EAAuB,MAAM,CAAC,IAAI,GAAG;CAAE;CAC3D,SAAS;EAAE,MAAM;EAAW,MAAM,CAAC,KAAK,EAAE;CAAE;CAC5C,QAAQ;EAAE,MAAM;EAAU,MAAM,CAAC,KAAK,GAAG;CAAE;AAC7C;;;;;;;;AASA,IAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;AACF;;AAGA,SAAgB,oBAAoB,SAA+C;CACjF,IAAI,YAAY,WAAW,OAAO,CAAC;CACnC,MAAM,CAAC,YAAY,aAAa,cAAc,QAAQ,CAAC;CACvD,OAAO;EACL,mBAAmB,OAAO,WAAW;EACrC,yBAAyB,OAAO,WAAW;EAC3C,wBAAwB,OAAO,WAAW;EAC1C,eAAe,OAAO,UAAU;EAChC,qBAAqB,OAAO,UAAU;CACxC;AACF;;AAGA,IAAM,qBAA+D;CACnE,SAAS;CACT,QAAQ;CACR,aAAa;AACf;;AAGA,SAAgB,mBAAmB,SAA2C;CAC5E,OAAO,mBAAmB;AAC5B;AAEA,SAAS,eAA8B;CACrC,IAAI;EACF,MAAM,SAAS,aAAa,QAAQ,WAAW;EAC/C,IAAI,QAAQ;GACV,MAAM,SAAS,KAAK,MAAM,MAAM;GAChC,OAAO;IAAE,GAAG;IAAiB,GAAG;GAAO;EACzC;EAEA,MAAM,eAAe,aAAa,QAAQ,kBAAkB;EAC5D,IAAI,cAAc;GAChB,MAAM,SAAS,KAAK,MAAM,YAAY;GACtC,MAAM,WAAW;IAAE,GAAG;IAAiB,GAAG;GAAO;GACjD,aAAa,QAAQ,aAAa,KAAK,UAAU,QAAQ,CAAC;GAC1D,aAAa,WAAW,kBAAkB;GAC1C,OAAO;EACT;CACF,SAAS,GAAG;EACV,QAAQ,KAAK,8CAA8C,CAAC;CAC9D;CACA,OAAO,EAAE,GAAG,gBAAgB;AAC9B;AAEA,SAAS,aAAa,UAA+B;CACnD,IAAI;EACF,aAAa,QAAQ,aAAa,KAAK,UAAU,QAAQ,CAAC;CAC5D,SAAS,GAAG;EACV,QAAQ,KAAK,4CAA4C,CAAC;CAC5D;AACF;AAEA,SAAS,uBAAgC;CACvC,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY,OAAO;CACrF,OAAO,OAAO,WAAW,8BAA8B,CAAC,CAAC;AAC3D;AAEA,IAAa,mBAAmB,YAAY,uBAAuB;CAEjE,MAAM,aAAa,IAAI,gBAAgB,UAAU;CACjD,MAAM,aAAa,IAAI,gBAAgB,UAAU;CACjD,MAAM,iBAAiB,IAAI,gBAAgB,cAAc;CACzD,MAAM,kBAAkB,IAAI,gBAAgB,eAAe;CAC3D,MAAM,sBAAsB,IAAI,gBAAgB,mBAAmB;CACnE,MAAM,QAAQ,IAAe,gBAAgB,KAAK;CAClD,MAAM,oBAAoB,IAAI,qBAAqB,CAAC;CACpD,MAAM,eAAe,IAAkB,gBAAgB,YAAY;CACnE,MAAM,eAAe,IAAkB,gBAAgB,YAAY;CAGnE,MAAM,YAAgF;CAEtF,SAAS,gBAA6E;EACpF,MAAM,cAAc,OAAO,WAAW,cAAc,OAAO,SAAS,WAAW;EAC/E,MAAM,cAAc,OAAO,WAAW,cAAc,OAAO,SAAS,OAAO;EAC3E,MAAM,gBAAgB,gBAAgB,OAAO,SAAS,aAAa,WAAW,QAAQ;EAItF,OAAO;GAAE,MAFN,WAAW,UAAU,eAAe,OAAO,WAAW,KAAK,MAAM,iBACjE,WAAW,UAAU,eAAe,gBAAgB;GACxC;GAAa;EAAY;CAC1C;CAEA,SAAS,gBAAwB;EAC/B,MAAM,EAAE,SAAS,cAAc;EAC/B,IAAI,MAAM,OAAO;EACjB,OAAO,UAAU,WAAW,MAAM,GAAG,WAAW,QAAQ;CAC1D;CAEA,SAAS,eAAuB;EAC9B,MAAM,EAAE,MAAM,aAAa,gBAAgB,cAAc;EACzD,MAAM,WAAW,OAAO,WAAW,eAAe,OAAO,SAAS,aAAa,WAAW,SAAS;EACnG,IAAI,MAAM,OAAO,GAAG,SAAS,IAAI,cAAc,cAAc,MAAM,cAAc,KAAK;EACtF,OAAO,QAAQ,WAAW,MAAM,GAAG,WAAW,QAAQ;CACxD;CAEA,IAAI,eAAe;CAEnB,SAAS,aAAa;EACpB,IAAI,cAAc;EAClB,eAAe;EAEf,MAAM,SAAS,aAAa;EAC5B,WAAW,QAAQ,OAAO;EAC1B,WAAW,QAAQ,OAAO;EAC1B,eAAe,QAAQ,OAAO;EAC9B,gBAAgB,QAAQ,OAAO;EAC/B,oBAAoB,QAAQ,OAAO;EACnC,MAAM,QAAQ,OAAO;EACrB,aAAa,QAAQ,OAAO;EAC5B,aAAa,QAAQ,OAAO;EAE5B,WAAW;EACX,aAAa;EACb,aAAa;CACf;CAEA,SAAS,aAAa;EACpB,IAAI,OAAO,aAAa,aAAa;EACrC,MAAM,OAAO,MAAM,UAAU,WAAW,kBAAkB,QAAQ,MAAM,UAAU;EAClF,SAAS,gBAAgB,UAAU,OAAO,QAAQ,IAAI;CACxD;;;;;;CAOA,SAAS,eAAe;EACtB,IAAI,OAAO,aAAa,aAAa;EACrC,MAAM,QAAQ,SAAS,gBAAgB;EACvC,MAAM,YAAY,oBAAoB,aAAa,KAAK;EACxD,KAAK,MAAM,QAAQ,uBAAuB;GACxC,MAAM,QAAQ,UAAU;GACxB,IAAI,OAAO,MAAM,YAAY,MAAM,KAAK;QACnC,MAAM,eAAe,IAAI;EAChC;CACF;;CAGA,SAAS,eAAe;EACtB,IAAI,OAAO,aAAa,aAAa;EACrC,SAAS,gBAAgB,QAAQ,UAAU,aAAa;CAC1D;CAEA,MAAM,aAAa;EACjB,WAAW;EACX,gBAAgB;CAClB,CAAC;CAED,MAAM,cAAc,YAAY;CAChC,MAAM,cAAc,YAAY;CAEhC,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAChE,OAAO,WAAW,8BAA8B,CAAC,CAAC,iBAAiB,WAAW,UAAU;EACtF,kBAAkB,QAAQ,MAAM;EAChC,IAAI,MAAM,UAAU,UAClB,WAAW;CAEf,CAAC;CAGH,SAAS,kBAAkB;EACzB,aAAa;GACX,YAAY,WAAW;GACvB,YAAY,WAAW;GACvB,gBAAgB,eAAe;GAC/B,iBAAiB,gBAAgB;GACjC,qBAAqB,oBAAoB;GACzC,OAAO,MAAM;GACb,cAAc,aAAa;GAC3B,cAAc,aAAa;EAC7B,CAAC;CACH;CAEA,MAAM;EAAC;EAAY;EAAY;EAAgB;EAAiB;EAAqB;EAAc;CAAY,SAAS;EACtH,gBAAgB;CAClB,CAAC;CAED,SAAS,kBAAkB;EACzB,WAAW,QAAQ,gBAAgB;EACnC,WAAW,QAAQ,gBAAgB;EACnC,eAAe,QAAQ,gBAAgB;EACvC,gBAAgB,QAAQ,gBAAgB;EACxC,oBAAoB,QAAQ,gBAAgB;EAC5C,MAAM,QAAQ,gBAAgB;EAC9B,aAAa,QAAQ,gBAAgB;EACrC,aAAa,QAAQ,gBAAgB;CACvC;CAEA,SAAS,iBAAmC;EAC1C,OAAO,cAAc,aAAa,MAAM,CAAC;CAC3C;;CAGA,SAAS,eAAmC;EAC1C,OAAO,mBAAmB,aAAa,KAAK;CAC9C;CAEA,SAAS,SAAkB;EACzB,OAAO,MAAM,UAAU,WAAW,kBAAkB,QAAQ,MAAM,UAAU;CAC9E;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF,CAAC;;;;;;;;;AAUD,SAAgB,sBAAkE;CAChF,IAAI,CAAC,eAAe,GAAG,OAAO;CAC9B,IAAI;EACF,OAAO,iBAAiB;CAC1B,QAAQ;EACN,OAAO;CACT;AACF;;;AC9TA,IAAI,oBAA0C;AAC9C,IAAI,sBAAsB;AAkB1B,SAAS,YAAY,SAAiB,MAAsB;CAC1D,IAAI,gBAAgB,KAAK,IAAI,GAAG,OAAO;CACvC,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG,GAAG,OAAO,GAAG,QAAQ,QAAQ,QAAQ,EAAE,IAAI;CAC1F,MAAM,iBAAiB,QAAQ,QAAQ,QAAQ,EAAE;CACjD,MAAM,iBAAiB,KAAK,QAAQ,QAAQ,GAAG;CAC/C,OAAO,GAAG,iBAAiB,eAAe,WAAW,GAAG,IAAI,iBAAiB,IAAI;AACnF;AAEA,SAAS,YAAY,SAAyB;CAC5C,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI;EACF,MAAM,SAAS,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;EACxE,OAAO,IAAI,IAAI,SAAS,MAAM,CAAC,CAAC,SAAS,QAAQ,QAAQ,EAAE,KAAK;CAClE,QAAQ;EAEN,OADa,QAAQ,QAAQ,sBAAsB,EAC5C,CAAA,CAAK,QAAQ,QAAQ,EAAE,KAAK;CACrC;AACF;AAEA,SAAS,oBAAoB,SAAiB,KAAqB;CACjE,IAAI,CAAC,OAAO,eAAe,KAAK,GAAG,GAAG,OAAO;CAE7C,MAAM,WAAW,YAAY,OAAO;CACpC,IAAI,aAAa,KAAK,OAAO;CAE7B,MAAM,gBAAgB,IAAI,WAAW,GAAG,IAAI,MAAM,IAAI;CACtD,IACE,kBAAkB,YACf,cAAc,WAAW,GAAG,SAAS,EAAE,KACvC,cAAc,WAAW,GAAG,SAAS,EAAE,GAE1C,OAAO,cAAc,MAAM,SAAS,MAAM;CAE5C,IAAI,cAAc,WAAW,GAAG,SAAS,EAAE,GACzC,OAAO,cAAc,MAAM,SAAS,MAAM,KAAK;CAEjD,OAAO;AACT;AAEA,SAAS,iBAAiB,SAA+D;CACvF,OAAO,UAAW,UAA6B;AACjD;AAEA,SAAS,uBAAuB,SAAiD;CAC/E,MAAM,MAAM,iBAAiB,OAAO;CACpC,IAAI,CAAC,KAAK,OAAO;CACjB,IAAI,OAAO,IAAI,QAAQ,YAAY,OAAO,IAAI,IAAI,eAAe;CACjE,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,MAAM,QAAQ,IAAI,YAAY,MAAM,eAAe;AAC7E;AAEA,SAAS,uBAAuB,SAAwC,OAAqB;CAC3F,MAAM,MAAM,iBAAiB,OAAO;CACpC,IAAI,CAAC,KAAK;CACV,IAAI,OAAO,IAAI,QAAQ,YAAY;EACjC,IAAI,IAAI,iBAAiB,KAAK;EAC9B;CACF;CACA,IAAI,gBAAgB;AACtB;AAEA,SAAS,0BAA0B,SAA8C;CAC/E,MAAM,MAAM,iBAAiB,OAAO;CACpC,IAAI,CAAC,KAAK;CACV,IAAI,OAAO,IAAI,WAAW,YAAY;EACpC,IAAI,OAAO,eAAe;EAC1B;CACF;CACA,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAC/B,IAAI,IAAI,YAAY,MAAM,iBAAiB,OAAO,IAAI;AAE1D;AAEA,SAAS,uBAAuB,SAAiD;CAC/E,MAAM,MAAM,iBAAiB,OAAO;CACpC,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,IAAI,OAAO,IAAI,QAAQ,YAAY,OAAO,IAAI,IAAI,eAAe;CACjE,MAAM,MAAM,OAAO,KAAK,GAAG,CAAC,CAAC,MAAM,cAAc,UAAU,YAAY,MAAM,eAAe;CAC5F,OAAO,MAAM,IAAI,OAAO,KAAA;AAC1B;;;;;;AAOA,IAAa,8BAAb,cAAiD,aAAa;CAC5D,OAAiC;CAIjC,YAAY,OAAiB,UAAyB;EACpD,MACE,kDACA,2BACA,KACA,UAAU,aAAa,MACvB,UAAU,WAAW,MACrB,UAAU,QACV,UAAU,QAAQ,MAClB,KACF;CACF;AACF;;AAGA,SAAgB,4BACd,OACA,eACA,UAC6B;CAC7B,MAAM,mBAAmB,aAAa;CACtC,IAAI,iBAAiB,iBAAiB,UAAU,eAC9C,iBAAiB,WAAW;CAE9B,OAAO,IAAI,4BAA4B,OAAO,QAAQ;AACxD;AAEA,SAAS,eAA8B;CACrC,IAAI,CAAC,mBACH,oBAAoB,MAAM,OAAO,EAC/B,SAAS,EACP,gBAAgB,mBAClB,EACF,CAAC;CAEH,OAAO;AACT;;AAsBA,SAAgB,OAAO,UAA4B,CAAC,GAAiB;CACnE,MAAM,gBAAgB,iBAAiB;CACvC,MAAM,YAAY,aAAa;CAC/B,MAAM,YAAY,aAAa;CAG/B,IAAI,CAAC,UAAU,eACb,UAAU,WAAW;CAIvB,IAAI,CAAC,qBAAqB;EACxB,UAAU,aAAa,QAAQ,KAAK,WAAW;GAC7C,MAAM,UAAU;GAChB,IAAI,QAAQ,eAAe;IACzB,0BAA0B,QAAQ,OAAO;IACzC,OAAO;GACT;GAEA,MAAM,mBAAmB,aAAa;GACtC,IAAI,iBAAiB,SAAS,OAAO,WAAW,CAAC,uBAAuB,OAAO,OAAO,GACpF,uBAAuB,OAAO,SAAS,UAAU,iBAAiB,OAAO;GAE3E,IAAI,uBAAuB,OAAO,OAAO,MAAM,UAAU,iBAAiB,SACxE,QAAQ,iBAAiB,iBAAiB,SAAS,KAAA;GAErD,OAAO;EACT,CAAC;EACD,UAAU,aAAa,SAAS,KAC7B,aAAa,UACd,OAAO,UAAmB;GACxB,IAAI,MAAM,aAAa,KAAK,KAAK,MAAM,UAAU;IAC/C,MAAM,UAAU,MAAM;IACtB,IAAI,UAAmB,MAAM,SAAS;IACtC,IAAI,mBAAmB,MACrB,IAAI;KACF,UAAU,KAAK,MAAM,MAAM,QAAQ,KAAK,CAAC;IAC3C,QAAQ,CAER;IAEF,MAAM,QAAQ,yBACZ,MAAM,SAAS,QACf,SACA;KACE,WAAW,OAAO,MAAM,SAAS,UAAU,mBAAmB,EAAE,KAAK;KACrE,OAAO;IACT,CACF;IACA,IAAI,MAAM,SAAS,WAAW,OAAO,SAAS,kBAAkB,MAC9D,OAAO,QAAQ,OAAO,4BAA4B,OAAO,SAAS,gBAAgB,KAAK,CAAC;IAE1F,OAAO,QAAQ,OAAO,KAAK;GAC7B;GACA,OAAO,QAAQ,OAAO,KAAK;EAC7B,CACF;EACA,sBAAsB;CACxB;CAEA,SAAS,aAAqB;EAC5B,OAAO,QAAQ,WAAW,cAAc,cAAc;CACxD;CAEA,SAAS,aAAa,KAAqB;EACzC,OAAO,oBAAoB,WAAW,GAAG,GAAG;CAC9C;CAGA,SAAS,cAAc,QAAqD;EAC1E,MAAM,OAA+B;GACnC,SAAS,WAAW;GACpB,SAAS,QAAQ,WAAW,cAAc;GAC1C,GAAG;EACL;EAEA,IAAI,QAAQ,aAAa,OAAO;GAC9B,KAAK,gBAAgB;GACrB,0BAA0B,KAAK,OAAO;EACxC;EACA,OAAO;CACT;CAGA,eAAe,IAAO,KAAa,QAAyC;EAE1E,QAAO,MADgB,UAAU,IAAO,aAAa,GAAG,GAAG,cAAc,MAAM,CAAC,EAAA,CAChE;CAClB;CAEA,eAAe,KAAQ,KAAa,MAAgB,QAAyC;EAE3F,QAAO,MADgB,UAAU,KAAQ,aAAa,GAAG,GAAG,MAAM,cAAc,MAAM,CAAC,EAAA,CACvE;CAClB;CAEA,eAAe,IAAO,KAAa,MAAgB,QAAyC;EAE1F,QAAO,MADgB,UAAU,IAAO,aAAa,GAAG,GAAG,MAAM,cAAc,MAAM,CAAC,EAAA,CACtE;CAClB;CAEA,eAAe,MAAS,KAAa,MAAgB,QAAyC;EAE5F,QAAO,MADgB,UAAU,MAAS,aAAa,GAAG,GAAG,MAAM,cAAc,MAAM,CAAC,EAAA,CACxE;CAClB;CAEA,eAAe,IAAO,KAAa,QAAyC;EAE1E,QAAO,MADgB,UAAU,OAAU,aAAa,GAAG,GAAG,cAAc,MAAM,CAAC,EAAA,CACnE;CAClB;CAGA,eAAe,OAAU,KAAa,MAAY,YAAY,QAAQ,gBAAsD;EAC1H,MAAM,WAAW,IAAI,SAAS;EAC9B,SAAS,OAAO,WAAW,IAAI;EAE/B,IAAI,gBACF,OAAO,QAAQ,cAAc,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW;GACvD,IAAI,UAAU,KAAA,KAAa,UAAU,MACnC,SAAS,OAAO,KAAK,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK,CAAC;EAE1F,CAAC;EAOH,QAAO,MAJgB,UAAU,KAAQ,aAAa,GAAG,GAAG,UAAU,cAAc,EAElF,SAAS,EAAE,gBAAgB,KAAA,EAAU,EACvC,CAAC,CAAC,EAAA,CACc;CAClB;CAGA,eAAe,SAAS,KAAa,UAAoC;EACvE,MAAM,WAAW,MAAM,UAAU,IAAI,aAAa,GAAG,GAAG,cAAc,EAAE,cAAc,OAAO,CAAC,CAAC;EAC/F,MAAM,OAAO,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC;EACrC,MAAM,UAAU,IAAI,gBAAgB,IAAI;EAGxC,IAAI,UAAU;GACZ,MAAM,OAAO,SAAS,cAAc,GAAG;GACvC,KAAK,OAAO;GACZ,KAAK,WAAW;GAChB,SAAS,KAAK,YAAY,IAAI;GAC9B,KAAK,MAAM;GACX,SAAS,KAAK,YAAY,IAAI;GAG9B,iBAAiB,IAAI,gBAAgB,OAAO,GAAG,GAAG;GAClD,OAAO;EACT;EAGA,OAAO;CACT;CAGA,SAAS,SAAS,MAAsB;EACtC,MAAM,UAAU,WAAW;EAC3B,OAAO,YAAY,SAAS,oBAAoB,SAAS,IAAI,CAAC;CAChE;CAGA,SAAS,WAAW,MAAsB;EACxC,OAAO,YAAY,cAAc,aAAa,GAAG,IAAI;CACvD;CAEA,OAAO;EACL,QAAQ;EACR;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;CACF;AACF;;;;;;;;;;;ACtVA,SAAgB,iBAAiB,OAAgB,WAAW,mBAA2B;CACrF,IAAI,iBAAiB,SAAS,MAAM,SAClC,OAAO,MAAM;CAGf,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,GAC1C,OAAO;CAGT,IACE,OAAO,UAAU,YACd,UAAU,QACV,aAAa,SACb,OAAQ,MAAgC,YAAY,YACnD,MAA8B,QAAQ,KAAK,GAE/C,OAAQ,MAA8B;CAGxC,OAAO;AACT;;;;ACLA,SAAgB,kBAAmD;CACjE,IAAI,OAAO,WAAW,aAAa,OAAO,KAAA;CAC1C,OAAQ,OAAmE;AAC7E;;;ACnBA,SAAS,oBAAwC;CAC/C,IAAI,OAAO,WAAW,aAAa,OAAO,KAAA;CAC1C,OAAQ,OAA8D,mBAClE;AACN;;;;;;;;;;;;AAaA,IAAa,qBAAqB,YAAY,yBAAyB;CACrE,MAAM,UAAU,IAA8B,IAAI;CAClD,MAAM,YAAY,IAAmB,IAAI;CACzC,MAAM,cAAc,IAAI,KAAK;CAC7B,MAAM,QAAQ,IAAmB,IAAI;CAKrC,MAAM,2BAAW,IAAI,IAAwC;CAM7D,IAAI,iBAAiB;CAErB,SAAS,gBAAgB,IAAwC;EAC/D,MAAM,UAAU,SAAS,IAAI,EAAE;EAC/B,IAAI,SAAS,OAAO;EAEpB,MAAM,OAAO,kBAAkB,KAAK;EACpC,MAAM,UAAU,OAAO,CAAC,CACrB,IAAuB,GAAG,KAAK,eAAe,IAAI,CAAC,CACnD,cAAc;GACb,SAAS,OAAO,EAAE;EACpB,CAAC;EAEH,SAAS,IAAI,IAAI,OAAO;EACxB,OAAO;CACT;;CAGA,SAAS,OAAO,YAAqC;EACnD,kBAAkB;EAClB,QAAQ,QAAQ;EAChB,UAAU,QAAQ,WAAW;EAC7B,MAAM,QAAQ;EACd,YAAY,QAAQ;CACtB;;;;;;CAOA,eAAe,WAAW,IAA+C;EACvE,MAAM,yBAAyB,YAAY,SAAS,UAAU,UAAU;EACxE,MAAM,QAAQ,yBAAyB,iBAAkB,kBAAkB;EAC3E,IAAI,CAAC,wBAAwB;GAC3B,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,MAAM,QAAQ;GACd,YAAY,QAAQ;EACtB;EACA,IAAI;GACF,MAAM,aAAa,MAAM,gBAAgB,EAAE;GAI3C,IAAI,UAAU,gBAAgB,OAAO;GACrC,QAAQ,QAAQ;GAChB,OAAO;EACT,SAAS,KAAK;GACZ,IAAI,UAAU,gBAAgB,OAAO;GACrC,QAAQ,QAAQ;GAChB,MAAM,QAAQ,iBAAiB,KAAK,2BAA2B;GAC/D,OAAO;EACT,UAAU;GAGR,IAAI,UAAU,gBAAgB,YAAY,QAAQ;EACpD;CACF;;CAGA,eAAe,UAAyB;EACtC,MAAM,KAAK,UAAU;EACrB,IAAI,OAAO,MAAM;EACjB,MAAM,QAAQ;EACd,IAAI;GACF,MAAM,aAAa,MAAM,gBAAgB,EAAE;GAC3C,IAAI,UAAU,gBAAgB;GAC9B,QAAQ,QAAQ;GAChB,MAAM,QAAQ;EAChB,SAAS,KAAK;GACZ,IAAI,UAAU,gBAAgB,MAAM,QAAQ,iBAAiB,KAAK,2BAA2B;EAC/F;CACF;CAEA,SAAS,QAAc;EAGrB,kBAAkB;EAClB,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,YAAY,QAAQ;CACtB;CAEA,gBAAgB,CAAC,EAAE,oBAAoB;EACrC,iBAAiB,UAAU;EAC3B;EACA,WAAW,aAAa;GAAE,MAAM,WAAW,QAAQ;EAAE;CACvD,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF,CAAC"}