@morscherlab/mint-sdk 1.4.0-beta.15 → 1.4.0-beta.17

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 (36) hide show
  1. package/dist/{AppSidebar-Bi8g9mRt.js → AppSidebar-CB0-RQ4m.js} +876 -114
  2. package/dist/AppSidebar-CB0-RQ4m.js.map +1 -0
  3. package/dist/{SettingsModal-jra_2u0o.js → SettingsModal-CoZK3EDN.js} +2 -2
  4. package/dist/{SettingsModal-jra_2u0o.js.map → SettingsModal-CoZK3EDN.js.map} +1 -1
  5. package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
  6. package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
  7. package/dist/components/AppTopBar.vue.d.ts +4 -4
  8. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  9. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  10. package/dist/composables/useOnboarding.d.ts +119 -0
  11. package/dist/index.js +5 -5
  12. package/dist/{install-Bx_6KwJA.js → install-BKgb5qZw.js} +5 -5
  13. package/dist/{install-Bx_6KwJA.js.map → install-BKgb5qZw.js.map} +1 -1
  14. package/dist/install.js +1 -1
  15. package/dist/{runtime-BJvGms6h.js → runtime-Cz1GJa_O.js} +2 -2
  16. package/dist/{runtime-BJvGms6h.js.map → runtime-Cz1GJa_O.js.map} +1 -1
  17. package/dist/runtime.js +2 -2
  18. package/dist/{shell-Czyrzk7l.js → shell-kq9p4VsX.js} +7 -2
  19. package/dist/{shell-Czyrzk7l.js.map → shell-kq9p4VsX.js.map} +1 -1
  20. package/dist/shell.d.ts +1 -0
  21. package/dist/shell.js +3 -3
  22. package/dist/styles.css +385 -0
  23. package/package.json +1 -1
  24. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  25. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  26. package/src/__tests__/styles/v2AuditRules.test.ts +8 -0
  27. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  28. package/src/components/AppTopBar.vue +11 -2
  29. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  30. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  31. package/src/composables/useOnboarding.ts +465 -0
  32. package/src/shell.ts +17 -0
  33. package/src/styles/components/onboarding.css +368 -0
  34. package/src/styles/index.css +1 -0
  35. package/src/styles/variables.css +16 -0
  36. package/dist/AppSidebar-Bi8g9mRt.js.map +0 -1
@@ -3,7 +3,7 @@ import { s as colorPalettes, u as useSettingsStore } from "./experiment-DyO54FQB
3
3
  import { G as BaseInput_default } from "./records-B4yl2oIu.js";
4
4
  import { o as usePlatform } from "./useListReorder-C_8RYeVP.js";
5
5
  import { t as SegmentedControl_default } from "./SegmentedControl-hOgBVdju.js";
6
- import { a as pickExistingRecordKeys, c as valuesEqual, h as normalizeItemInput, i as formSchemaFieldNames, k as controlsToSettingsSchema, l as useFormBuilder, o as pickRecordKeys, r as FormFieldRendererInternal_default, s as recordValuesEqualForKeys } from "./AppSidebar-Bi8g9mRt.js";
6
+ import { P as controlsToSettingsSchema, a as pickExistingRecordKeys, b as normalizeItemInput, c as valuesEqual, i as formSchemaFieldNames, l as useFormBuilder, o as pickRecordKeys, r as FormFieldRendererInternal_default, s as recordValuesEqualForKeys } from "./AppSidebar-CB0-RQ4m.js";
7
7
  import { r as useAuthStore } from "./auth-SliLnFre.js";
8
8
  import { n as isControlModelBinding } from "./controlSchemaModel-CIH5Hqpw.js";
9
9
  import { canAccessByPolicy } from "./permissions.js";
@@ -664,4 +664,4 @@ var SettingsModal_default = SettingsModal_vue_vue_type_script_setup_true_lang_de
664
664
  //#endregion
665
665
  export { SettingsModal_exports as n, SettingsModal_default as t };
666
666
 
667
- //# sourceMappingURL=SettingsModal-jra_2u0o.js.map
667
+ //# sourceMappingURL=SettingsModal-CoZK3EDN.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsModal-jra_2u0o.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/** Accent tokens a palette swatch shows; `data-palette` on the strip resolves them per palette. */\nconst paletteSwatchTokens = ['--color-primary', '--color-primary-hover', '--color-primary-soft', '--color-primary-border']\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\">Accent colour 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\" :data-palette=\"key\" aria-hidden=\"true\">\n <span\n v-for=\"token in paletteSwatchTokens\"\n :key=\"token\"\n class=\"mint-settings-modal__swatch\"\n :style=\"{ backgroundColor: `var(${token})` }\"\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/** Accent tokens a palette swatch shows; `data-palette` on the strip resolves them per palette. */\nconst paletteSwatchTokens = ['--color-primary', '--color-primary-hover', '--color-primary-soft', '--color-primary-border']\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\">Accent colour 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\" :data-palette=\"key\" aria-hidden=\"true\">\n <span\n v-for=\"token in paletteSwatchTokens\"\n :key=\"token\"\n class=\"mint-settings-modal__swatch\"\n :style=\"{ backgroundColor: `var(${token})` }\"\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
+ {"version":3,"file":"SettingsModal-CoZK3EDN.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/** Accent tokens a palette swatch shows; `data-palette` on the strip resolves them per palette. */\nconst paletteSwatchTokens = ['--color-primary', '--color-primary-hover', '--color-primary-soft', '--color-primary-border']\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\">Accent colour 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\" :data-palette=\"key\" aria-hidden=\"true\">\n <span\n v-for=\"token in paletteSwatchTokens\"\n :key=\"token\"\n class=\"mint-settings-modal__swatch\"\n :style=\"{ backgroundColor: `var(${token})` }\"\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/** Accent tokens a palette swatch shows; `data-palette` on the strip resolves them per palette. */\nconst paletteSwatchTokens = ['--color-primary', '--color-primary-hover', '--color-primary-soft', '--color-primary-border']\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\">Accent colour 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\" :data-palette=\"key\" aria-hidden=\"true\">\n <span\n v-for=\"token in paletteSwatchTokens\"\n :key=\"token\"\n class=\"mint-settings-modal__swatch\"\n :style=\"{ backgroundColor: `var(${token})` }\"\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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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -23,9 +23,9 @@ declare var __VLS_6: {}, __VLS_13: {}, __VLS_26: {}, __VLS_33: {}, __VLS_40: {},
23
23
  page: PageSelectorItem;
24
24
  }, __VLS_61: {
25
25
  page: PageSelectorItem;
26
- }, __VLS_63: {}, __VLS_65: {}, __VLS_103: {}, __VLS_110: {}, __VLS_122: `settings-tab-${string}`, __VLS_123: {}, __VLS_126: {};
26
+ }, __VLS_63: {}, __VLS_65: {}, __VLS_103: {}, __VLS_115: {}, __VLS_127: `settings-tab-${string}`, __VLS_128: {}, __VLS_131: {};
27
27
  type __VLS_Slots = {} & {
28
- [K in NonNullable<typeof __VLS_122>]?: (props: typeof __VLS_123) => any;
28
+ [K in NonNullable<typeof __VLS_127>]?: (props: typeof __VLS_128) => any;
29
29
  } & {
30
30
  icon?: (props: typeof __VLS_6) => any;
31
31
  } & {
@@ -49,9 +49,9 @@ type __VLS_Slots = {} & {
49
49
  } & {
50
50
  actions?: (props: typeof __VLS_103) => any;
51
51
  } & {
52
- account?: (props: typeof __VLS_110) => any;
52
+ account?: (props: typeof __VLS_115) => any;
53
53
  } & {
54
- 'settings-appearance'?: (props: typeof __VLS_126) => any;
54
+ 'settings-appearance'?: (props: typeof __VLS_131) => any;
55
55
  };
56
56
  declare const __VLS_base: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
57
57
  "update:pill": (pillId: string) => any;
@@ -0,0 +1,7 @@
1
+ import { OnboardingState } from '../../composables/useOnboarding';
2
+ type __VLS_Props = {
3
+ state: OnboardingState;
4
+ };
5
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { OnboardingState } from '../../composables/useOnboarding';
2
+ type __VLS_Props = {
3
+ state: OnboardingState;
4
+ };
5
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,119 @@
1
+ import { ComputedRef, InjectionKey, MaybeRefOrGetter, ShallowRef } from 'vue';
2
+ import { Placement } from '@floating-ui/dom';
3
+ /** One card of a tour. It points at the element with `data-onboarding="<anchor>"`. */
4
+ export interface OnboardingStep {
5
+ /** Stable id. A returning user sees only steps whose id is new. */
6
+ id: string;
7
+ /** `data-onboarding` value of the element the step points at. */
8
+ anchor: string;
9
+ title: string;
10
+ body: string;
11
+ /** Side of the anchor for the card. Default `bottom`. */
12
+ placement?: Placement;
13
+ /** `'click'`: the highlighted element takes clicks, and a click moves the tour to the next step. */
14
+ advanceOn?: 'click';
15
+ /** Leave the step out while this returns false, e.g. without the permission its control needs. */
16
+ available?: () => boolean;
17
+ }
18
+ export interface OnboardingTour {
19
+ id: string;
20
+ /** Name in the Help menu. */
21
+ title: string;
22
+ /** Start only while the app's route is this value. Omit it for elements on every page, such as the top bar. */
23
+ route?: string;
24
+ steps: OnboardingStep[];
25
+ }
26
+ export interface OnboardingTask {
27
+ id: string;
28
+ title: string;
29
+ /** Leave the task out while this returns false, e.g. without the permission it needs. */
30
+ available?: () => boolean;
31
+ /** The task is done when this returns true. The coordinator checks it at start, on a route change and when the Help menu opens. */
32
+ done?: () => boolean;
33
+ /** The link at the end of the task row. */
34
+ action?: {
35
+ label: string;
36
+ run: () => void;
37
+ };
38
+ }
39
+ /** A new feature. A dot marks its element until the user opens it. */
40
+ export interface OnboardingNews {
41
+ id: string;
42
+ anchor: string;
43
+ title: string;
44
+ body: string;
45
+ placement?: Placement;
46
+ }
47
+ export interface OnboardingDefinition {
48
+ tours?: OnboardingTour[];
49
+ /** The "Getting started" checklist. */
50
+ checklist?: OnboardingTask[];
51
+ whatsNew?: OnboardingNews[];
52
+ }
53
+ export interface ProvideOnboardingOptions {
54
+ /** Storage namespace, e.g. `platform` or the plugin name. */
55
+ appId: string;
56
+ /** The signed-in user. `null` turns onboarding off (no user yet). Default: the platform user; `local` standalone or with auth off. */
57
+ userId?: MaybeRefOrGetter<string | number | null | undefined>;
58
+ /** The current route, matched against `OnboardingTour.route`. */
59
+ route?: MaybeRefOrGetter<string | null | undefined>;
60
+ /** Start unseen tours on their own. Default `true`. */
61
+ autoStart?: boolean;
62
+ }
63
+ /** The step on screen and the element it points at. */
64
+ export interface OnboardingActiveStep {
65
+ step: OnboardingStep;
66
+ element: HTMLElement;
67
+ /** Position among the steps of this run, and their count. */
68
+ index: number;
69
+ count: number;
70
+ /** A what's-new card, not a tour step. */
71
+ news: boolean;
72
+ }
73
+ export interface OnboardingState {
74
+ definition: OnboardingDefinition;
75
+ /** False until a user is known. */
76
+ enabled: ComputedRef<boolean>;
77
+ active: ShallowRef<OnboardingActiveStep | null>;
78
+ /** Tours that can run on the current route. */
79
+ tours: ComputedRef<OnboardingTour[]>;
80
+ /** The available tasks. */
81
+ tasks: ComputedRef<Array<Omit<OnboardingTask, 'done'> & {
82
+ done: boolean;
83
+ }>>;
84
+ tasksDone: ComputedRef<number>;
85
+ /** The checklist chip shows while a task is open and the user did not hide it. */
86
+ checklistVisible: ComputedRef<boolean>;
87
+ checklistHidden: ComputedRef<boolean>;
88
+ news: ComputedRef<Array<OnboardingNews & {
89
+ unseen: boolean;
90
+ }>>;
91
+ unseenNews: ComputedRef<number>;
92
+ startTour: (id: string) => void;
93
+ showNews: (id: string) => void;
94
+ next: () => void;
95
+ back: () => void;
96
+ /** End the run (× or Esc). Every step of the tour counts as seen. */
97
+ dismiss: () => void;
98
+ complete: (taskId: string) => void;
99
+ setChecklistHidden: (hidden: boolean) => void;
100
+ /** Run the task predicates again. */
101
+ refresh: () => void;
102
+ }
103
+ export declare const ONBOARDING_KEY: InjectionKey<OnboardingState>;
104
+ /** Wait this long for the missing anchor of a run's first step, or of the step after a click step; then skip it. */
105
+ export declare const ANCHOR_WAIT_MS = 2000;
106
+ /** Types a definition, so it can live in its own file. */
107
+ export declare function defineOnboarding(definition: OnboardingDefinition): OnboardingDefinition;
108
+ /**
109
+ * Creates the app's onboarding coordinator and provides it to `AppTopBar`, which then
110
+ * shows the Help button, the checklist chip and the tour layer. Call it once, in the
111
+ * root component. It shows one item at a time and waits for open modals.
112
+ *
113
+ * ```ts
114
+ * provideOnboarding(onboarding, { appId: 'leaf' })
115
+ * ```
116
+ */
117
+ export declare function provideOnboarding(definition: OnboardingDefinition, options: ProvideOnboardingOptions): OnboardingState;
118
+ /** The app's onboarding coordinator, e.g. to call `complete(taskId)`. Null when the app provides none. */
119
+ export declare function useOnboarding(): OnboardingState | null;
package/dist/index.js CHANGED
@@ -4,21 +4,21 @@ import { B as Tooltip_default, C as formatExperimentDate, G as BaseInput_default
4
4
  import { B as compareSortValues, C as useSequenceUtils, H as BaseSlider_default, I as defineDataFrame, N as NumberInput_default, P as DatePicker_default, R as definePluginWorkspaceView, U as BaseRadioGroup_default, V as useSortedItems, _ as getFieldRegistryEntry, b as useConcentrationUnits, o as usePlatform, v as getTypeDefault } from "./useListReorder-C_8RYeVP.js";
5
5
  import { D as BaseTabs_default, E as BasePill_default, a as useReagentSeries, b as ProgressBar_default, h as useProtocolTemplates, i as generateDilutionSeries, n as DEFAULT_PRESETS, r as DEFAULT_UNITS, v as useSampleGroups, y as SectionCard_default } from "./lab-ZDKlx1Oo.js";
6
6
  import { t as SegmentedControl_default } from "./SegmentedControl-hOgBVdju.js";
7
- import { a as AppContainer_default, i as Divider_default, n as FormBuilder_default, o as Calendar_default, r as Breadcrumb_default, t as MINTSdk } from "./install-Bx_6KwJA.js";
7
+ import { a as AppContainer_default, i as Divider_default, n as FormBuilder_default, o as Calendar_default, r as Breadcrumb_default, t as MINTSdk } from "./install-BKgb5qZw.js";
8
8
  import { r as LoadingSpinner_default, t as AlertBox_default } from "./AlertBox-SnsbVow-.js";
9
9
  import { C as hexToHsl, D as DataFrame_default, E as sampleTypeMarker, S as deriveShade, _ as mintPlotlyTemplate, r as useScheduleDrag, s as downloadBlob, t as useRackEditor, w as hslToHex } from "./useRackEditor-BhYlXKFM.js";
10
- import { c as generatedJobFormSchema, d as PluginWorkspaceView_default, f as useControlWorkspace, l as normalizeGeneratedJobInput, m as useToast, p as AppToastContainer_default, s as generatedJobDefaults, u as Avatar_default } from "./runtime-BJvGms6h.js";
10
+ import { c as generatedJobFormSchema, d as PluginWorkspaceView_default, f as useControlWorkspace, l as normalizeGeneratedJobInput, m as useToast, p as AppToastContainer_default, s as generatedJobDefaults, u as Avatar_default } from "./runtime-Cz1GJa_O.js";
11
11
  import { n as ActionMenu_default, t as useRequest } from "./useRequest-C9uwM79q.js";
12
- import { A as getDefaultControlView, M as ThemeToggle_default, N as useTheme, S as useControlSchema, b as defineControls, d as useForm, j as CollapsibleCard_default, l as useFormBuilder, m as PluginIcon_default, p as providePluginIdentity, u as evaluateCondition, v as usePluginWorkspace, x as getControlDefaults, y as defineControlComponentBindings } from "./AppSidebar-Bi8g9mRt.js";
12
+ import { C as usePluginWorkspace, D as useControlSchema, E as getControlDefaults, F as getDefaultControlView, I as CollapsibleCard_default, L as ThemeToggle_default, R as useTheme, T as defineControls, d as useForm, l as useFormBuilder, p as providePluginIdentity, u as evaluateCondition, w as defineControlComponentBindings, y as PluginIcon_default } from "./AppSidebar-CB0-RQ4m.js";
13
13
  import { r as useAuthStore } from "./auth-SliLnFre.js";
14
14
  import { t as defineControlModel } from "./controlSchemaModel-CIH5Hqpw.js";
15
15
  import { t as ConfirmDialog_default } from "./ConfirmDialog-CSls2Nuv.js";
16
- import { t as SettingsModal_default } from "./SettingsModal-jra_2u0o.js";
16
+ import { t as SettingsModal_default } from "./SettingsModal-CoZK3EDN.js";
17
17
  import { r as useExperimentSelector } from "./ExperimentSelectorModal-BqifjNaN.js";
18
18
  import { i as useFileBrowser, n as buildAnalysisArtifactTargetQuery, r as parseAnalysisArtifactTarget } from "./data-OZ94DhVC.js";
19
19
  import { a as useFileImport, c as extractSampleNamesFromDesignData, i as readFileAsText, l as extractSampleOptionsFromDesignData, n as detectDelimiter, o as validateImportFile, t as useExperimentSamples, u as unwrapExperimentDesignData } from "./useExperimentSamples-BLx29CkM.js";
20
20
  import { t as useExpansionSet } from "./useExpansionSet-CvoNMZJi.js";
21
- import { a as useManualLayoutResize, i as resizedVerticalSplitPercent, n as resizedLeadingPanelWidth, r as resizedTrailingPanelWidth } from "./shell-Czyrzk7l.js";
21
+ import { a as useManualLayoutResize, i as resizedVerticalSplitPercent, n as resizedLeadingPanelWidth, r as resizedTrailingPanelWidth } from "./shell-kq9p4VsX.js";
22
22
  import "./stores/index.js";
23
23
  import { computed } from "vue";
24
24
  //#region src/composables/useExperiment.ts