@morscherlab/mint-sdk 1.4.0-beta.16 → 1.4.0-beta.18

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 (78) hide show
  1. package/README.md +14 -3
  2. package/dist/{AlertBox-SnsbVow-.js → AlertBox-CzLg0UaL.js} +2 -2
  3. package/dist/{AlertBox-SnsbVow-.js.map → AlertBox-CzLg0UaL.js.map} +1 -1
  4. package/dist/{AppSidebar-CB0-RQ4m.js → AppSidebar-6oTT-gPO.js} +43 -13
  5. package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
  6. package/dist/{ExperimentPopover-ChKCrklH.js → ExperimentPopover-9l4QZlvQ.js} +2 -2
  7. package/dist/{ExperimentPopover-ChKCrklH.js.map → ExperimentPopover-9l4QZlvQ.js.map} +1 -1
  8. package/dist/{ExperimentSelectorModal-BqifjNaN.js → ExperimentSelectorModal-pR5fK3pA.js} +3 -3
  9. package/dist/{ExperimentSelectorModal-BqifjNaN.js.map → ExperimentSelectorModal-pR5fK3pA.js.map} +1 -1
  10. package/dist/{SegmentedControl-hOgBVdju.js → SegmentedControl-CJ7EAmOq.js} +2 -2
  11. package/dist/{SegmentedControl-hOgBVdju.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
  12. package/dist/{SettingsModal-CoZK3EDN.js → SettingsModal-Dyitgzfp.js} +6 -6
  13. package/dist/{SettingsModal-CoZK3EDN.js.map → SettingsModal-Dyitgzfp.js.map} +1 -1
  14. package/dist/components/AppTopBar.vue.d.ts +7 -0
  15. package/dist/components/JobsStatusTray.vue.d.ts +11 -4
  16. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  17. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  18. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  19. package/dist/composables/useJobCenter.d.ts +11 -3
  20. package/dist/{controlSchemaModel-CIH5Hqpw.js → controlSchemaModel-DCUOx8ll.js} +2 -2
  21. package/dist/{controlSchemaModel-CIH5Hqpw.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
  22. package/dist/{data-OZ94DhVC.js → data-BIiV4kKw.js} +9 -9
  23. package/dist/{data-OZ94DhVC.js.map → data-BIiV4kKw.js.map} +1 -1
  24. package/dist/data.js +7 -7
  25. package/dist/index.js +16 -16
  26. package/dist/{install-BKgb5qZw.js → install-Dg-Z6ze5.js} +13 -13
  27. package/dist/{install-BKgb5qZw.js.map → install-Dg-Z6ze5.js.map} +1 -1
  28. package/dist/install.js +1 -1
  29. package/dist/{lab-ZDKlx1Oo.js → lab-BHVT1qN2.js} +9 -9
  30. package/dist/{lab-ZDKlx1Oo.js.map → lab-BHVT1qN2.js.map} +1 -1
  31. package/dist/lab.js +4 -4
  32. package/dist/{records-B4yl2oIu.js → records-CR_pYzIP.js} +62 -62
  33. package/dist/{records-B4yl2oIu.js.map → records-CR_pYzIP.js.map} +1 -1
  34. package/dist/{runtime-Cz1GJa_O.js → runtime-cHv1phcc.js} +379 -31
  35. package/dist/runtime-cHv1phcc.js.map +1 -0
  36. package/dist/runtime.d.ts +1 -1
  37. package/dist/runtime.js +6 -6
  38. package/dist/{shell-kq9p4VsX.js → shell-BDuZf2zb.js} +4 -4
  39. package/dist/{shell-kq9p4VsX.js.map → shell-BDuZf2zb.js.map} +1 -1
  40. package/dist/shell.js +4 -4
  41. package/dist/styles.css +163 -307
  42. package/dist/types/components.d.ts +2 -0
  43. package/dist/{useExperimentSamples-BLx29CkM.js → useExperimentSamples-Dno1fFMq.js} +3 -3
  44. package/dist/{useExperimentSamples-BLx29CkM.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
  45. package/dist/{useListReorder-C_8RYeVP.js → useListReorder-CQ9OCScc.js} +76 -76
  46. package/dist/useListReorder-CQ9OCScc.js.map +1 -0
  47. package/dist/{useRackEditor-BhYlXKFM.js → useRackEditor-wL3kPrRT.js} +6 -6
  48. package/dist/{useRackEditor-BhYlXKFM.js.map → useRackEditor-wL3kPrRT.js.map} +1 -1
  49. package/dist/useRealtime-BW02xy4Z.js +398 -0
  50. package/dist/useRealtime-BW02xy4Z.js.map +1 -0
  51. package/dist/{useRequest-C9uwM79q.js → useRequest-CDkrXNU7.js} +2 -2
  52. package/dist/{useRequest-C9uwM79q.js.map → useRequest-CDkrXNU7.js.map} +1 -1
  53. package/package.json +1 -1
  54. package/src/__tests__/components/AppTopBar.test.ts +33 -2
  55. package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
  56. package/src/__tests__/components/JobsStatusTray.test.ts +137 -71
  57. package/src/__tests__/components/PluginWorkspaceView.test.ts +56 -1
  58. package/src/__tests__/composables/useJobCenter.test.ts +223 -3
  59. package/src/__tests__/docs/frontendDocsCatalog.test.ts +3 -3
  60. package/src/__tests__/styles/v2AuditRules.test.ts +8 -0
  61. package/src/components/AppTopBar.vue +14 -0
  62. package/src/components/GeneratedPluginView.vue +4 -9
  63. package/src/components/JobsStatusTray.story.vue +78 -43
  64. package/src/components/JobsStatusTray.vue +171 -279
  65. package/src/components/PluginWorkspaceView.props.ts +3 -0
  66. package/src/components/PluginWorkspaceView.vue +17 -0
  67. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  68. package/src/composables/useJobCenter.ts +127 -6
  69. package/src/runtime.ts +0 -1
  70. package/src/styles/components/app-top-bar.css +7 -0
  71. package/src/styles/components/jobs-status-tray.css +129 -296
  72. package/src/styles/variables.css +16 -0
  73. package/src/types/components.ts +2 -0
  74. package/dist/AppSidebar-CB0-RQ4m.js.map +0 -1
  75. package/dist/runtime-Cz1GJa_O.js.map +0 -1
  76. package/dist/useListReorder-C_8RYeVP.js.map +0 -1
  77. package/dist/useRealtime-CUMbW_ln.js +0 -699
  78. package/dist/useRealtime-CUMbW_ln.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { o as __exportAll, t as useDropdownState } from "./useDropdownState-Bb2N4m40.js";
2
- import { A as formatRelativeTime } from "./records-B4yl2oIu.js";
2
+ import { A as formatRelativeTime } from "./records-CR_pYzIP.js";
3
3
  import { t as ConfirmDialog_default } from "./ConfirmDialog-CSls2Nuv.js";
4
4
  import { n as format } from "./experimentStatus-BFrKpHAH.js";
5
5
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onUnmounted, openBlock, ref, renderList, toDisplayString, unref, useTemplateRef, watch, withModifiers } from "vue";
@@ -334,4 +334,4 @@ var ExperimentPopover_default = ExperimentPopover_vue_vue_type_script_setup_true
334
334
  //#endregion
335
335
  export { ExperimentPopover_exports as n, ExperimentPopover_default as t };
336
336
 
337
- //# sourceMappingURL=ExperimentPopover-ChKCrklH.js.map
337
+ //# sourceMappingURL=ExperimentPopover-9l4QZlvQ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExperimentPopover-ChKCrklH.js","names":[],"sources":["../src/components/ExperimentPopover.vue","../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** Top-bar control for the attached experiment: a trigger joined to an optional Save button, with a summary panel (change, open, detach). */\nimport { ref, computed, watch, nextTick, onUnmounted, useTemplateRef } from 'vue'\nimport { ArrowLeftRight, Check, ChevronDown, CircleAlert, CloudUpload, ExternalLink, Link, LoaderCircle, Unlink } from 'lucide-vue-next'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { format as formatStatus } from '../utils/experimentStatus'\nimport { formatRelativeTime } from '../utils/time'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentCode?: string\n experimentStatus?: string\n /** Platform id of the bound experiment; enables \"Open in MINT\". */\n experimentId?: number | null\n experimentProject?: string\n experimentOwner?: string\n /** ISO timestamp of the last update, shown as relative time. */\n experimentUpdatedAt?: string\n /** An experiment is bound but the platform has not returned its record yet. */\n isResolving?: boolean\n /** Message explaining why the bound experiment could not be resolved. */\n error?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n saveLabel?: string\n /** Unsaved changes: marks the Save button with a dot. */\n dirty?: boolean\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n /** Empty-state trigger label. */\n placeholder?: string\n /** The trigger name truncates at this width (px); its title shows code and name in full. */\n maxNameWidth?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n experimentId: null,\n isResolving: false,\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n saveLabel: 'Save',\n dirty: false,\n confirmSave: true,\n placeholder: 'Attach experiment',\n maxNameWidth: 200,\n})\n\n// An experiment is bound but has no name to show — either still resolving or\n// resolution failed. The id is never substituted: a name may only come from the\n// platform, so these states say what is happening instead of inventing a label.\nconst isUnresolved = computed(() => props.isResolving || !!props.error)\nconst hasExperiment = computed(() => !!props.experimentName || !!props.experimentCode)\nconst isEmpty = computed(() => !isUnresolved.value && !hasExperiment.value)\nconst hasSave = computed(() => props.showSave && !isUnresolved.value && hasExperiment.value)\n\nconst triggerLabel = computed(() => {\n if (props.isResolving) return 'Loading experiment'\n if (props.error) return 'Experiment unavailable'\n return [props.experimentCode, props.experimentName].filter(Boolean).join(' · ') || props.placeholder\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst { isOpen, close, toggle } = useDropdownState({\n rootRef: useTemplateRef<HTMLElement>('popover'),\n triggerRef: useTemplateRef<HTMLElement>('trigger'),\n closeOnEscape: false,\n})\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\n// The panel hangs from the group's left edge; when that would cross the\n// viewport's right edge (a control at the end of the top bar) it hangs from the right.\nconst panel = useTemplateRef<HTMLElement>('panel')\nconst panelAlignEnd = ref(false)\nwatch(isOpen, async (open) => {\n if (!open) return\n panelAlignEnd.value = false\n await nextTick()\n const right = panel.value?.getBoundingClientRect().right ?? 0\n panelAlignEnd.value = right > document.documentElement.clientWidth\n})\n\nconst saveTitle = computed(() => {\n if (props.saveDisabled && props.saveDisabledMessage) return props.saveDisabledMessage\n if (showSuccess.value && props.saveSuccessMessage) return props.saveSuccessMessage\n return `${props.saveLabel} to experiment`\n})\n\n// A function, not a computed: \"Updated\" is relative to now, so it is\n// re-evaluated each time the panel renders.\nfunction metaRows(): [string, string][] {\n return ([\n ['Project', props.experimentProject],\n ['Status', props.experimentStatus && formatStatus(props.experimentStatus)],\n ['Owner', props.experimentOwner],\n ['Updated', props.experimentUpdatedAt && formatRelativeTime(props.experimentUpdatedAt)],\n ] as [string, string | undefined][]).filter((row): row is [string, string] => !!row[1])\n}\n\nfunction handleTriggerClick() {\n if (isEmpty.value) {\n emit('select')\n return\n }\n toggle()\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n close()\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonUnmounted(() => {\n if (successTimer) clearTimeout(successTimer)\n})\n</script>\n\n<template>\n <div ref=\"popover\" :class=\"['mint-experiment-popover', { 'mint-experiment-popover--save': hasSave }]\">\n <!-- Trigger: code + name when attached, opens the panel; the empty state selects directly. -->\n <button\n ref=\"trigger\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__trigger',\n { 'mint-experiment-popover__trigger--active': isOpen },\n { 'mint-experiment-popover__trigger--empty': isEmpty },\n { 'mint-experiment-popover__trigger--loading': isResolving },\n { 'mint-experiment-popover__trigger--error': !isResolving && !!error },\n ]\"\n :title=\"triggerLabel\"\n :aria-label=\"isUnresolved ? triggerLabel : undefined\"\n :aria-busy=\"isResolving || undefined\"\n :aria-expanded=\"isEmpty ? undefined : isOpen\"\n @click.stop=\"handleTriggerClick\"\n >\n <!-- Resolving and error come first: with an experiment bound but no name\n yet, the empty placeholder would be a lie. -->\n <span v-if=\"isResolving\" class=\"mint-experiment-popover__trigger-skeleton\" aria-hidden=\"true\" />\n <template v-else-if=\"error\">\n <CircleAlert class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">Experiment unavailable</span>\n </template>\n <template v-else-if=\"hasExperiment\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <span\n v-if=\"experimentName\"\n class=\"mint-experiment-popover__trigger-text\"\n :style=\"{ maxWidth: `${maxNameWidth}px` }\"\n >{{ experimentName }}</span>\n </template>\n <template v-else>\n <Link class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">{{ placeholder }}</span>\n </template>\n <ChevronDown v-if=\"!isEmpty\" class=\"mint-experiment-popover__trigger-chevron\" :size=\"13\" aria-hidden=\"true\" />\n </button>\n\n <!-- Save: joined to the trigger at top-bar level, never inside the panel. -->\n <button\n v-if=\"hasSave\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__save',\n { 'mint-experiment-popover__save--loading': saveLoading },\n { 'mint-experiment-popover__save--success': showSuccess },\n { 'mint-experiment-popover__save--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveLoading || (saveDisabled && !showSuccess)\"\n :title=\"saveTitle\"\n :aria-label=\"dirty ? `${saveTitle} (unsaved changes)` : saveTitle\"\n @click.stop=\"handleSave\"\n >\n <LoaderCircle v-if=\"saveLoading\" class=\"mint-experiment-popover__save-spinner\" :size=\"14\" aria-hidden=\"true\" />\n <Check v-else-if=\"showSuccess\" :size=\"14\" aria-hidden=\"true\" />\n <CloudUpload v-else :size=\"14\" aria-hidden=\"true\" />\n <span>{{ saveLoading ? 'Saving…' : saveLabel }}</span>\n <span v-if=\"dirty && !saveLoading\" class=\"mint-experiment-popover__dirty\" aria-hidden=\"true\" />\n </button>\n\n <!-- Summary panel: header, meta, then menu rows. No buttons inside. -->\n <div\n v-if=\"isOpen\"\n ref=\"panel\"\n :class=\"['mint-experiment-popover__panel', { 'mint-experiment-popover__panel--end': panelAlignEnd }]\"\n >\n <!-- Bound, still resolving — placeholders stand in for the record. -->\n <div v-if=\"isResolving\" class=\"mint-experiment-popover__head\" aria-busy=\"true\">\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--code\" />\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--name\" />\n </div>\n\n <!-- Bound, resolution failed — Detach stays reachable below. -->\n <div v-else-if=\"error\" class=\"mint-experiment-popover__head mint-experiment-popover__head--error\" role=\"alert\">\n <b class=\"mint-experiment-popover__name\">\n <CircleAlert :size=\"14\" aria-hidden=\"true\" />\n Experiment unavailable\n </b>\n <span class=\"mint-experiment-popover__message\">{{ error }}</span>\n </div>\n\n <template v-else>\n <div class=\"mint-experiment-popover__head\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <b v-if=\"experimentName\" class=\"mint-experiment-popover__name\">{{ experimentName }}</b>\n </div>\n <dl v-if=\"metaRows().length\" class=\"mint-experiment-popover__meta\">\n <template v-for=\"[label, value] in metaRows()\" :key=\"label\">\n <dt>{{ label }}</dt>\n <dd>{{ value }}</dd>\n </template>\n </dl>\n </template>\n\n <div class=\"mint-experiment-popover__separator\" role=\"separator\" />\n\n <button type=\"button\" class=\"mint-experiment-popover__option\" @click=\"handleSelect\">\n <ArrowLeftRight class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Change experiment…\n </button>\n <a\n v-if=\"experimentId != null\"\n class=\"mint-experiment-popover__option\"\n :href=\"`/experiments/${experimentId}`\"\n target=\"_blank\"\n rel=\"noopener\"\n @click=\"close()\"\n >\n <ExternalLink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Open in MINT\n </a>\n <button\n v-if=\"showDetach\"\n type=\"button\"\n class=\"mint-experiment-popover__option mint-experiment-popover__option--danger\"\n @click=\"handleDetach\"\n >\n <Unlink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Detach\n </button>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model:open=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName ?? experimentCode}?`\"\n tone=\"info\"\n :confirm-label=\"saveLabel\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\n/** Top-bar control for the attached experiment: a trigger joined to an optional Save button, with a summary panel (change, open, detach). */\nimport { ref, computed, watch, nextTick, onUnmounted, useTemplateRef } from 'vue'\nimport { ArrowLeftRight, Check, ChevronDown, CircleAlert, CloudUpload, ExternalLink, Link, LoaderCircle, Unlink } from 'lucide-vue-next'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { format as formatStatus } from '../utils/experimentStatus'\nimport { formatRelativeTime } from '../utils/time'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentCode?: string\n experimentStatus?: string\n /** Platform id of the bound experiment; enables \"Open in MINT\". */\n experimentId?: number | null\n experimentProject?: string\n experimentOwner?: string\n /** ISO timestamp of the last update, shown as relative time. */\n experimentUpdatedAt?: string\n /** An experiment is bound but the platform has not returned its record yet. */\n isResolving?: boolean\n /** Message explaining why the bound experiment could not be resolved. */\n error?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n saveLabel?: string\n /** Unsaved changes: marks the Save button with a dot. */\n dirty?: boolean\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n /** Empty-state trigger label. */\n placeholder?: string\n /** The trigger name truncates at this width (px); its title shows code and name in full. */\n maxNameWidth?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n experimentId: null,\n isResolving: false,\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n saveLabel: 'Save',\n dirty: false,\n confirmSave: true,\n placeholder: 'Attach experiment',\n maxNameWidth: 200,\n})\n\n// An experiment is bound but has no name to show — either still resolving or\n// resolution failed. The id is never substituted: a name may only come from the\n// platform, so these states say what is happening instead of inventing a label.\nconst isUnresolved = computed(() => props.isResolving || !!props.error)\nconst hasExperiment = computed(() => !!props.experimentName || !!props.experimentCode)\nconst isEmpty = computed(() => !isUnresolved.value && !hasExperiment.value)\nconst hasSave = computed(() => props.showSave && !isUnresolved.value && hasExperiment.value)\n\nconst triggerLabel = computed(() => {\n if (props.isResolving) return 'Loading experiment'\n if (props.error) return 'Experiment unavailable'\n return [props.experimentCode, props.experimentName].filter(Boolean).join(' · ') || props.placeholder\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst { isOpen, close, toggle } = useDropdownState({\n rootRef: useTemplateRef<HTMLElement>('popover'),\n triggerRef: useTemplateRef<HTMLElement>('trigger'),\n closeOnEscape: false,\n})\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\n// The panel hangs from the group's left edge; when that would cross the\n// viewport's right edge (a control at the end of the top bar) it hangs from the right.\nconst panel = useTemplateRef<HTMLElement>('panel')\nconst panelAlignEnd = ref(false)\nwatch(isOpen, async (open) => {\n if (!open) return\n panelAlignEnd.value = false\n await nextTick()\n const right = panel.value?.getBoundingClientRect().right ?? 0\n panelAlignEnd.value = right > document.documentElement.clientWidth\n})\n\nconst saveTitle = computed(() => {\n if (props.saveDisabled && props.saveDisabledMessage) return props.saveDisabledMessage\n if (showSuccess.value && props.saveSuccessMessage) return props.saveSuccessMessage\n return `${props.saveLabel} to experiment`\n})\n\n// A function, not a computed: \"Updated\" is relative to now, so it is\n// re-evaluated each time the panel renders.\nfunction metaRows(): [string, string][] {\n return ([\n ['Project', props.experimentProject],\n ['Status', props.experimentStatus && formatStatus(props.experimentStatus)],\n ['Owner', props.experimentOwner],\n ['Updated', props.experimentUpdatedAt && formatRelativeTime(props.experimentUpdatedAt)],\n ] as [string, string | undefined][]).filter((row): row is [string, string] => !!row[1])\n}\n\nfunction handleTriggerClick() {\n if (isEmpty.value) {\n emit('select')\n return\n }\n toggle()\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n close()\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonUnmounted(() => {\n if (successTimer) clearTimeout(successTimer)\n})\n</script>\n\n<template>\n <div ref=\"popover\" :class=\"['mint-experiment-popover', { 'mint-experiment-popover--save': hasSave }]\">\n <!-- Trigger: code + name when attached, opens the panel; the empty state selects directly. -->\n <button\n ref=\"trigger\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__trigger',\n { 'mint-experiment-popover__trigger--active': isOpen },\n { 'mint-experiment-popover__trigger--empty': isEmpty },\n { 'mint-experiment-popover__trigger--loading': isResolving },\n { 'mint-experiment-popover__trigger--error': !isResolving && !!error },\n ]\"\n :title=\"triggerLabel\"\n :aria-label=\"isUnresolved ? triggerLabel : undefined\"\n :aria-busy=\"isResolving || undefined\"\n :aria-expanded=\"isEmpty ? undefined : isOpen\"\n @click.stop=\"handleTriggerClick\"\n >\n <!-- Resolving and error come first: with an experiment bound but no name\n yet, the empty placeholder would be a lie. -->\n <span v-if=\"isResolving\" class=\"mint-experiment-popover__trigger-skeleton\" aria-hidden=\"true\" />\n <template v-else-if=\"error\">\n <CircleAlert class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">Experiment unavailable</span>\n </template>\n <template v-else-if=\"hasExperiment\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <span\n v-if=\"experimentName\"\n class=\"mint-experiment-popover__trigger-text\"\n :style=\"{ maxWidth: `${maxNameWidth}px` }\"\n >{{ experimentName }}</span>\n </template>\n <template v-else>\n <Link class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">{{ placeholder }}</span>\n </template>\n <ChevronDown v-if=\"!isEmpty\" class=\"mint-experiment-popover__trigger-chevron\" :size=\"13\" aria-hidden=\"true\" />\n </button>\n\n <!-- Save: joined to the trigger at top-bar level, never inside the panel. -->\n <button\n v-if=\"hasSave\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__save',\n { 'mint-experiment-popover__save--loading': saveLoading },\n { 'mint-experiment-popover__save--success': showSuccess },\n { 'mint-experiment-popover__save--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveLoading || (saveDisabled && !showSuccess)\"\n :title=\"saveTitle\"\n :aria-label=\"dirty ? `${saveTitle} (unsaved changes)` : saveTitle\"\n @click.stop=\"handleSave\"\n >\n <LoaderCircle v-if=\"saveLoading\" class=\"mint-experiment-popover__save-spinner\" :size=\"14\" aria-hidden=\"true\" />\n <Check v-else-if=\"showSuccess\" :size=\"14\" aria-hidden=\"true\" />\n <CloudUpload v-else :size=\"14\" aria-hidden=\"true\" />\n <span>{{ saveLoading ? 'Saving…' : saveLabel }}</span>\n <span v-if=\"dirty && !saveLoading\" class=\"mint-experiment-popover__dirty\" aria-hidden=\"true\" />\n </button>\n\n <!-- Summary panel: header, meta, then menu rows. No buttons inside. -->\n <div\n v-if=\"isOpen\"\n ref=\"panel\"\n :class=\"['mint-experiment-popover__panel', { 'mint-experiment-popover__panel--end': panelAlignEnd }]\"\n >\n <!-- Bound, still resolving — placeholders stand in for the record. -->\n <div v-if=\"isResolving\" class=\"mint-experiment-popover__head\" aria-busy=\"true\">\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--code\" />\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--name\" />\n </div>\n\n <!-- Bound, resolution failed — Detach stays reachable below. -->\n <div v-else-if=\"error\" class=\"mint-experiment-popover__head mint-experiment-popover__head--error\" role=\"alert\">\n <b class=\"mint-experiment-popover__name\">\n <CircleAlert :size=\"14\" aria-hidden=\"true\" />\n Experiment unavailable\n </b>\n <span class=\"mint-experiment-popover__message\">{{ error }}</span>\n </div>\n\n <template v-else>\n <div class=\"mint-experiment-popover__head\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <b v-if=\"experimentName\" class=\"mint-experiment-popover__name\">{{ experimentName }}</b>\n </div>\n <dl v-if=\"metaRows().length\" class=\"mint-experiment-popover__meta\">\n <template v-for=\"[label, value] in metaRows()\" :key=\"label\">\n <dt>{{ label }}</dt>\n <dd>{{ value }}</dd>\n </template>\n </dl>\n </template>\n\n <div class=\"mint-experiment-popover__separator\" role=\"separator\" />\n\n <button type=\"button\" class=\"mint-experiment-popover__option\" @click=\"handleSelect\">\n <ArrowLeftRight class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Change experiment…\n </button>\n <a\n v-if=\"experimentId != null\"\n class=\"mint-experiment-popover__option\"\n :href=\"`/experiments/${experimentId}`\"\n target=\"_blank\"\n rel=\"noopener\"\n @click=\"close()\"\n >\n <ExternalLink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Open in MINT\n </a>\n <button\n v-if=\"showDetach\"\n type=\"button\"\n class=\"mint-experiment-popover__option mint-experiment-popover__option--danger\"\n @click=\"handleDetach\"\n >\n <Unlink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Detach\n </button>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model:open=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName ?? experimentCode}?`\"\n tone=\"info\"\n :confirm-label=\"saveLabel\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExperimentPopover-9l4QZlvQ.js","names":[],"sources":["../src/components/ExperimentPopover.vue","../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** Top-bar control for the attached experiment: a trigger joined to an optional Save button, with a summary panel (change, open, detach). */\nimport { ref, computed, watch, nextTick, onUnmounted, useTemplateRef } from 'vue'\nimport { ArrowLeftRight, Check, ChevronDown, CircleAlert, CloudUpload, ExternalLink, Link, LoaderCircle, Unlink } from 'lucide-vue-next'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { format as formatStatus } from '../utils/experimentStatus'\nimport { formatRelativeTime } from '../utils/time'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentCode?: string\n experimentStatus?: string\n /** Platform id of the bound experiment; enables \"Open in MINT\". */\n experimentId?: number | null\n experimentProject?: string\n experimentOwner?: string\n /** ISO timestamp of the last update, shown as relative time. */\n experimentUpdatedAt?: string\n /** An experiment is bound but the platform has not returned its record yet. */\n isResolving?: boolean\n /** Message explaining why the bound experiment could not be resolved. */\n error?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n saveLabel?: string\n /** Unsaved changes: marks the Save button with a dot. */\n dirty?: boolean\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n /** Empty-state trigger label. */\n placeholder?: string\n /** The trigger name truncates at this width (px); its title shows code and name in full. */\n maxNameWidth?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n experimentId: null,\n isResolving: false,\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n saveLabel: 'Save',\n dirty: false,\n confirmSave: true,\n placeholder: 'Attach experiment',\n maxNameWidth: 200,\n})\n\n// An experiment is bound but has no name to show — either still resolving or\n// resolution failed. The id is never substituted: a name may only come from the\n// platform, so these states say what is happening instead of inventing a label.\nconst isUnresolved = computed(() => props.isResolving || !!props.error)\nconst hasExperiment = computed(() => !!props.experimentName || !!props.experimentCode)\nconst isEmpty = computed(() => !isUnresolved.value && !hasExperiment.value)\nconst hasSave = computed(() => props.showSave && !isUnresolved.value && hasExperiment.value)\n\nconst triggerLabel = computed(() => {\n if (props.isResolving) return 'Loading experiment'\n if (props.error) return 'Experiment unavailable'\n return [props.experimentCode, props.experimentName].filter(Boolean).join(' · ') || props.placeholder\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst { isOpen, close, toggle } = useDropdownState({\n rootRef: useTemplateRef<HTMLElement>('popover'),\n triggerRef: useTemplateRef<HTMLElement>('trigger'),\n closeOnEscape: false,\n})\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\n// The panel hangs from the group's left edge; when that would cross the\n// viewport's right edge (a control at the end of the top bar) it hangs from the right.\nconst panel = useTemplateRef<HTMLElement>('panel')\nconst panelAlignEnd = ref(false)\nwatch(isOpen, async (open) => {\n if (!open) return\n panelAlignEnd.value = false\n await nextTick()\n const right = panel.value?.getBoundingClientRect().right ?? 0\n panelAlignEnd.value = right > document.documentElement.clientWidth\n})\n\nconst saveTitle = computed(() => {\n if (props.saveDisabled && props.saveDisabledMessage) return props.saveDisabledMessage\n if (showSuccess.value && props.saveSuccessMessage) return props.saveSuccessMessage\n return `${props.saveLabel} to experiment`\n})\n\n// A function, not a computed: \"Updated\" is relative to now, so it is\n// re-evaluated each time the panel renders.\nfunction metaRows(): [string, string][] {\n return ([\n ['Project', props.experimentProject],\n ['Status', props.experimentStatus && formatStatus(props.experimentStatus)],\n ['Owner', props.experimentOwner],\n ['Updated', props.experimentUpdatedAt && formatRelativeTime(props.experimentUpdatedAt)],\n ] as [string, string | undefined][]).filter((row): row is [string, string] => !!row[1])\n}\n\nfunction handleTriggerClick() {\n if (isEmpty.value) {\n emit('select')\n return\n }\n toggle()\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n close()\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonUnmounted(() => {\n if (successTimer) clearTimeout(successTimer)\n})\n</script>\n\n<template>\n <div ref=\"popover\" :class=\"['mint-experiment-popover', { 'mint-experiment-popover--save': hasSave }]\">\n <!-- Trigger: code + name when attached, opens the panel; the empty state selects directly. -->\n <button\n ref=\"trigger\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__trigger',\n { 'mint-experiment-popover__trigger--active': isOpen },\n { 'mint-experiment-popover__trigger--empty': isEmpty },\n { 'mint-experiment-popover__trigger--loading': isResolving },\n { 'mint-experiment-popover__trigger--error': !isResolving && !!error },\n ]\"\n :title=\"triggerLabel\"\n :aria-label=\"isUnresolved ? triggerLabel : undefined\"\n :aria-busy=\"isResolving || undefined\"\n :aria-expanded=\"isEmpty ? undefined : isOpen\"\n @click.stop=\"handleTriggerClick\"\n >\n <!-- Resolving and error come first: with an experiment bound but no name\n yet, the empty placeholder would be a lie. -->\n <span v-if=\"isResolving\" class=\"mint-experiment-popover__trigger-skeleton\" aria-hidden=\"true\" />\n <template v-else-if=\"error\">\n <CircleAlert class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">Experiment unavailable</span>\n </template>\n <template v-else-if=\"hasExperiment\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <span\n v-if=\"experimentName\"\n class=\"mint-experiment-popover__trigger-text\"\n :style=\"{ maxWidth: `${maxNameWidth}px` }\"\n >{{ experimentName }}</span>\n </template>\n <template v-else>\n <Link class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">{{ placeholder }}</span>\n </template>\n <ChevronDown v-if=\"!isEmpty\" class=\"mint-experiment-popover__trigger-chevron\" :size=\"13\" aria-hidden=\"true\" />\n </button>\n\n <!-- Save: joined to the trigger at top-bar level, never inside the panel. -->\n <button\n v-if=\"hasSave\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__save',\n { 'mint-experiment-popover__save--loading': saveLoading },\n { 'mint-experiment-popover__save--success': showSuccess },\n { 'mint-experiment-popover__save--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveLoading || (saveDisabled && !showSuccess)\"\n :title=\"saveTitle\"\n :aria-label=\"dirty ? `${saveTitle} (unsaved changes)` : saveTitle\"\n @click.stop=\"handleSave\"\n >\n <LoaderCircle v-if=\"saveLoading\" class=\"mint-experiment-popover__save-spinner\" :size=\"14\" aria-hidden=\"true\" />\n <Check v-else-if=\"showSuccess\" :size=\"14\" aria-hidden=\"true\" />\n <CloudUpload v-else :size=\"14\" aria-hidden=\"true\" />\n <span>{{ saveLoading ? 'Saving…' : saveLabel }}</span>\n <span v-if=\"dirty && !saveLoading\" class=\"mint-experiment-popover__dirty\" aria-hidden=\"true\" />\n </button>\n\n <!-- Summary panel: header, meta, then menu rows. No buttons inside. -->\n <div\n v-if=\"isOpen\"\n ref=\"panel\"\n :class=\"['mint-experiment-popover__panel', { 'mint-experiment-popover__panel--end': panelAlignEnd }]\"\n >\n <!-- Bound, still resolving — placeholders stand in for the record. -->\n <div v-if=\"isResolving\" class=\"mint-experiment-popover__head\" aria-busy=\"true\">\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--code\" />\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--name\" />\n </div>\n\n <!-- Bound, resolution failed — Detach stays reachable below. -->\n <div v-else-if=\"error\" class=\"mint-experiment-popover__head mint-experiment-popover__head--error\" role=\"alert\">\n <b class=\"mint-experiment-popover__name\">\n <CircleAlert :size=\"14\" aria-hidden=\"true\" />\n Experiment unavailable\n </b>\n <span class=\"mint-experiment-popover__message\">{{ error }}</span>\n </div>\n\n <template v-else>\n <div class=\"mint-experiment-popover__head\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <b v-if=\"experimentName\" class=\"mint-experiment-popover__name\">{{ experimentName }}</b>\n </div>\n <dl v-if=\"metaRows().length\" class=\"mint-experiment-popover__meta\">\n <template v-for=\"[label, value] in metaRows()\" :key=\"label\">\n <dt>{{ label }}</dt>\n <dd>{{ value }}</dd>\n </template>\n </dl>\n </template>\n\n <div class=\"mint-experiment-popover__separator\" role=\"separator\" />\n\n <button type=\"button\" class=\"mint-experiment-popover__option\" @click=\"handleSelect\">\n <ArrowLeftRight class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Change experiment…\n </button>\n <a\n v-if=\"experimentId != null\"\n class=\"mint-experiment-popover__option\"\n :href=\"`/experiments/${experimentId}`\"\n target=\"_blank\"\n rel=\"noopener\"\n @click=\"close()\"\n >\n <ExternalLink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Open in MINT\n </a>\n <button\n v-if=\"showDetach\"\n type=\"button\"\n class=\"mint-experiment-popover__option mint-experiment-popover__option--danger\"\n @click=\"handleDetach\"\n >\n <Unlink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Detach\n </button>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model:open=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName ?? experimentCode}?`\"\n tone=\"info\"\n :confirm-label=\"saveLabel\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\n/** Top-bar control for the attached experiment: a trigger joined to an optional Save button, with a summary panel (change, open, detach). */\nimport { ref, computed, watch, nextTick, onUnmounted, useTemplateRef } from 'vue'\nimport { ArrowLeftRight, Check, ChevronDown, CircleAlert, CloudUpload, ExternalLink, Link, LoaderCircle, Unlink } from 'lucide-vue-next'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { format as formatStatus } from '../utils/experimentStatus'\nimport { formatRelativeTime } from '../utils/time'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentCode?: string\n experimentStatus?: string\n /** Platform id of the bound experiment; enables \"Open in MINT\". */\n experimentId?: number | null\n experimentProject?: string\n experimentOwner?: string\n /** ISO timestamp of the last update, shown as relative time. */\n experimentUpdatedAt?: string\n /** An experiment is bound but the platform has not returned its record yet. */\n isResolving?: boolean\n /** Message explaining why the bound experiment could not be resolved. */\n error?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n saveLabel?: string\n /** Unsaved changes: marks the Save button with a dot. */\n dirty?: boolean\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n /** Empty-state trigger label. */\n placeholder?: string\n /** The trigger name truncates at this width (px); its title shows code and name in full. */\n maxNameWidth?: number\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n experimentId: null,\n isResolving: false,\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n saveLabel: 'Save',\n dirty: false,\n confirmSave: true,\n placeholder: 'Attach experiment',\n maxNameWidth: 200,\n})\n\n// An experiment is bound but has no name to show — either still resolving or\n// resolution failed. The id is never substituted: a name may only come from the\n// platform, so these states say what is happening instead of inventing a label.\nconst isUnresolved = computed(() => props.isResolving || !!props.error)\nconst hasExperiment = computed(() => !!props.experimentName || !!props.experimentCode)\nconst isEmpty = computed(() => !isUnresolved.value && !hasExperiment.value)\nconst hasSave = computed(() => props.showSave && !isUnresolved.value && hasExperiment.value)\n\nconst triggerLabel = computed(() => {\n if (props.isResolving) return 'Loading experiment'\n if (props.error) return 'Experiment unavailable'\n return [props.experimentCode, props.experimentName].filter(Boolean).join(' · ') || props.placeholder\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst { isOpen, close, toggle } = useDropdownState({\n rootRef: useTemplateRef<HTMLElement>('popover'),\n triggerRef: useTemplateRef<HTMLElement>('trigger'),\n closeOnEscape: false,\n})\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\n// The panel hangs from the group's left edge; when that would cross the\n// viewport's right edge (a control at the end of the top bar) it hangs from the right.\nconst panel = useTemplateRef<HTMLElement>('panel')\nconst panelAlignEnd = ref(false)\nwatch(isOpen, async (open) => {\n if (!open) return\n panelAlignEnd.value = false\n await nextTick()\n const right = panel.value?.getBoundingClientRect().right ?? 0\n panelAlignEnd.value = right > document.documentElement.clientWidth\n})\n\nconst saveTitle = computed(() => {\n if (props.saveDisabled && props.saveDisabledMessage) return props.saveDisabledMessage\n if (showSuccess.value && props.saveSuccessMessage) return props.saveSuccessMessage\n return `${props.saveLabel} to experiment`\n})\n\n// A function, not a computed: \"Updated\" is relative to now, so it is\n// re-evaluated each time the panel renders.\nfunction metaRows(): [string, string][] {\n return ([\n ['Project', props.experimentProject],\n ['Status', props.experimentStatus && formatStatus(props.experimentStatus)],\n ['Owner', props.experimentOwner],\n ['Updated', props.experimentUpdatedAt && formatRelativeTime(props.experimentUpdatedAt)],\n ] as [string, string | undefined][]).filter((row): row is [string, string] => !!row[1])\n}\n\nfunction handleTriggerClick() {\n if (isEmpty.value) {\n emit('select')\n return\n }\n toggle()\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n close()\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonUnmounted(() => {\n if (successTimer) clearTimeout(successTimer)\n})\n</script>\n\n<template>\n <div ref=\"popover\" :class=\"['mint-experiment-popover', { 'mint-experiment-popover--save': hasSave }]\">\n <!-- Trigger: code + name when attached, opens the panel; the empty state selects directly. -->\n <button\n ref=\"trigger\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__trigger',\n { 'mint-experiment-popover__trigger--active': isOpen },\n { 'mint-experiment-popover__trigger--empty': isEmpty },\n { 'mint-experiment-popover__trigger--loading': isResolving },\n { 'mint-experiment-popover__trigger--error': !isResolving && !!error },\n ]\"\n :title=\"triggerLabel\"\n :aria-label=\"isUnresolved ? triggerLabel : undefined\"\n :aria-busy=\"isResolving || undefined\"\n :aria-expanded=\"isEmpty ? undefined : isOpen\"\n @click.stop=\"handleTriggerClick\"\n >\n <!-- Resolving and error come first: with an experiment bound but no name\n yet, the empty placeholder would be a lie. -->\n <span v-if=\"isResolving\" class=\"mint-experiment-popover__trigger-skeleton\" aria-hidden=\"true\" />\n <template v-else-if=\"error\">\n <CircleAlert class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">Experiment unavailable</span>\n </template>\n <template v-else-if=\"hasExperiment\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <span\n v-if=\"experimentName\"\n class=\"mint-experiment-popover__trigger-text\"\n :style=\"{ maxWidth: `${maxNameWidth}px` }\"\n >{{ experimentName }}</span>\n </template>\n <template v-else>\n <Link class=\"mint-experiment-popover__trigger-icon\" :size=\"14\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-popover__trigger-text\">{{ placeholder }}</span>\n </template>\n <ChevronDown v-if=\"!isEmpty\" class=\"mint-experiment-popover__trigger-chevron\" :size=\"13\" aria-hidden=\"true\" />\n </button>\n\n <!-- Save: joined to the trigger at top-bar level, never inside the panel. -->\n <button\n v-if=\"hasSave\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__save',\n { 'mint-experiment-popover__save--loading': saveLoading },\n { 'mint-experiment-popover__save--success': showSuccess },\n { 'mint-experiment-popover__save--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveLoading || (saveDisabled && !showSuccess)\"\n :title=\"saveTitle\"\n :aria-label=\"dirty ? `${saveTitle} (unsaved changes)` : saveTitle\"\n @click.stop=\"handleSave\"\n >\n <LoaderCircle v-if=\"saveLoading\" class=\"mint-experiment-popover__save-spinner\" :size=\"14\" aria-hidden=\"true\" />\n <Check v-else-if=\"showSuccess\" :size=\"14\" aria-hidden=\"true\" />\n <CloudUpload v-else :size=\"14\" aria-hidden=\"true\" />\n <span>{{ saveLoading ? 'Saving…' : saveLabel }}</span>\n <span v-if=\"dirty && !saveLoading\" class=\"mint-experiment-popover__dirty\" aria-hidden=\"true\" />\n </button>\n\n <!-- Summary panel: header, meta, then menu rows. No buttons inside. -->\n <div\n v-if=\"isOpen\"\n ref=\"panel\"\n :class=\"['mint-experiment-popover__panel', { 'mint-experiment-popover__panel--end': panelAlignEnd }]\"\n >\n <!-- Bound, still resolving — placeholders stand in for the record. -->\n <div v-if=\"isResolving\" class=\"mint-experiment-popover__head\" aria-busy=\"true\">\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--code\" />\n <span class=\"mint-experiment-popover__skeleton-line mint-experiment-popover__skeleton-line--name\" />\n </div>\n\n <!-- Bound, resolution failed — Detach stays reachable below. -->\n <div v-else-if=\"error\" class=\"mint-experiment-popover__head mint-experiment-popover__head--error\" role=\"alert\">\n <b class=\"mint-experiment-popover__name\">\n <CircleAlert :size=\"14\" aria-hidden=\"true\" />\n Experiment unavailable\n </b>\n <span class=\"mint-experiment-popover__message\">{{ error }}</span>\n </div>\n\n <template v-else>\n <div class=\"mint-experiment-popover__head\">\n <span v-if=\"experimentCode\" class=\"mint-exp-code mint-exp-code--sm\">{{ experimentCode }}</span>\n <b v-if=\"experimentName\" class=\"mint-experiment-popover__name\">{{ experimentName }}</b>\n </div>\n <dl v-if=\"metaRows().length\" class=\"mint-experiment-popover__meta\">\n <template v-for=\"[label, value] in metaRows()\" :key=\"label\">\n <dt>{{ label }}</dt>\n <dd>{{ value }}</dd>\n </template>\n </dl>\n </template>\n\n <div class=\"mint-experiment-popover__separator\" role=\"separator\" />\n\n <button type=\"button\" class=\"mint-experiment-popover__option\" @click=\"handleSelect\">\n <ArrowLeftRight class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Change experiment…\n </button>\n <a\n v-if=\"experimentId != null\"\n class=\"mint-experiment-popover__option\"\n :href=\"`/experiments/${experimentId}`\"\n target=\"_blank\"\n rel=\"noopener\"\n @click=\"close()\"\n >\n <ExternalLink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Open in MINT\n </a>\n <button\n v-if=\"showDetach\"\n type=\"button\"\n class=\"mint-experiment-popover__option mint-experiment-popover__option--danger\"\n @click=\"handleDetach\"\n >\n <Unlink class=\"mint-experiment-popover__option-icon\" :size=\"14\" aria-hidden=\"true\" />\n Detach\n </button>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model:open=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName ?? experimentCode}?`\"\n tone=\"info\"\n :confirm-label=\"saveLabel\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { n as BaseModal_default, o as __exportAll } from "./useDropdownState-Bb2N4m40.js";
2
2
  import { o as useApi, t as useExperimentStore } from "./experiment-DyO54FQB.js";
3
- import { C as formatExperimentDate, n as ResourcePickerListInternal_default, x as datePresetToISO } from "./records-B4yl2oIu.js";
4
- import { n as ActionMenu_default, t as useRequest } from "./useRequest-C9uwM79q.js";
3
+ import { C as formatExperimentDate, n as ResourcePickerListInternal_default, x as datePresetToISO } from "./records-CR_pYzIP.js";
4
+ import { n as ActionMenu_default, t as useRequest } from "./useRequest-CDkrXNU7.js";
5
5
  import { t as useDebouncedWatch } from "./useDebouncedWatch-BPwyl6eS.js";
6
6
  import { n as format, r as tone } from "./experimentStatus-BFrKpHAH.js";
7
7
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, reactive, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, watch, withCtx } from "vue";
@@ -557,4 +557,4 @@ var ExperimentSelectorModal_default = ExperimentSelectorModal_vue_vue_type_scrip
557
557
  //#endregion
558
558
  export { ExperimentSelectorModal_exports as n, useExperimentSelector as r, ExperimentSelectorModal_default as t };
559
559
 
560
- //# sourceMappingURL=ExperimentSelectorModal-BqifjNaN.js.map
560
+ //# sourceMappingURL=ExperimentSelectorModal-pR5fK3pA.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExperimentSelectorModal-BqifjNaN.js","names":["$slots"],"sources":["../src/composables/useExperimentSelector.ts","../src/components/ExperimentSelectorModal.vue","../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequest } from './useRequest'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */\n withStatusCounts?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n /** Experiment count per status under every filter except status itself. */\n statusCounts: Ref<Record<string, number>>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n /** Fetch `statusCounts` from `GET /experiments/stats`. */\n fetchStatusCounts: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const {\n limit = 100,\n immediate = false,\n experimentType,\n allowedExperimentTypes: optionAllowedExperimentTypes,\n apiBaseUrl,\n withStatusCounts = false,\n } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequest('Failed to fetch experiments')\n const isLoading = request.isLoading\n const error = request.error\n const lastLoadedAt = ref<Date | null>(null)\n const page = ref(0)\n let fetchGeneration = 0\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const statusCounts = ref<Record<string, number>>({})\n let statsGeneration = 0\n let countedScope: string | null = null\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n mine: undefined,\n })\n\n function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {\n if (values == null) return null\n return [...new Set(values.filter((value): value is string => typeof value === 'string' && value.length > 0))]\n }\n\n function currentAllowedExperimentTypes(): string[] | null {\n if (optionAllowedExperimentTypes !== undefined) {\n return normalizeExperimentTypes(optionAllowedExperimentTypes)\n }\n return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes)\n }\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n type ExperimentTypeScope =\n | { kind: 'all' }\n | { kind: 'blocked' }\n | { kind: 'single'; experimentType: string }\n | { kind: 'multi'; experimentTypes: string[] }\n\n function resolveExperimentTypeScope(): ExperimentTypeScope {\n const allowedTypes = currentAllowedExperimentTypes()\n const selectedType = experimentType ?? filters.experimentType ?? undefined\n\n if (selectedType) {\n if (allowedTypes && !allowedTypes.includes(selectedType)) {\n return { kind: 'blocked' }\n }\n return { kind: 'single', experimentType: selectedType }\n }\n\n if (allowedTypes) {\n if (allowedTypes.length === 0) return { kind: 'blocked' }\n if (allowedTypes.length === 1) return { kind: 'single', experimentType: allowedTypes[0] }\n return { kind: 'multi', experimentTypes: allowedTypes }\n }\n\n return { kind: 'all' }\n }\n\n /** Query params shared by the list and the stats route; status, sort and paging are list-only. */\n function scopeParams(typeFilter: string | undefined): URLSearchParams {\n const params = new URLSearchParams()\n if (typeFilter) params.set('experiment_type', typeFilter)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n if (filters.mine) params.set('mine', 'true')\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n return params\n }\n\n function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {\n const params = scopeParams(typeFilter)\n if (filters.status) params.set('status', filters.status)\n\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n params.set('limit', String(pageLimit))\n params.set('skip', String(skip))\n\n const query = params.toString()\n const base = platformBase ?? ''\n return `${base}/experiments${query ? `?${query}` : ''}`\n }\n\n function sortExperiments(items: ExperimentSummary[]): ExperimentSummary[] {\n const { sortBy, sortOrder } = parseSortKey()\n const direction = sortOrder === 'desc' ? -1 : 1\n return [...items].sort((a, b) => {\n const aValue = a[sortBy] ?? ''\n const bValue = b[sortBy] ?? ''\n if (aValue < bValue) return -1 * direction\n if (aValue > bValue) return 1 * direction\n return 0\n })\n }\n\n async function fetchExperimentList(typeFilter: string | undefined, skip: number, pageLimit: number) {\n return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))\n }\n\n async function fetchScopedExperimentPage(\n requestedPage: number,\n ): Promise<ExperimentListResponse> {\n const scope = resolveExperimentTypeScope()\n const pageSkip = requestedPage * limit\n\n if (scope.kind === 'blocked') {\n return { experiments: [], total: 0 }\n }\n\n if (scope.kind === 'multi') {\n const perTypeLimit = pageSkip + limit\n const responses = await Promise.all(\n scope.experimentTypes.map(type => fetchExperimentList(type, 0, perTypeLimit)),\n )\n const merged = sortExperiments(responses.flatMap(response => response.experiments))\n return {\n experiments: merged.slice(pageSkip, pageSkip + limit),\n total: responses.reduce((sum, response) => sum + response.total, 0),\n }\n }\n\n return await fetchExperimentList(\n scope.kind === 'single' ? scope.experimentType : undefined,\n pageSkip,\n limit,\n )\n }\n\n async function fetchExperiments(): Promise<void> {\n const generation = ++fetchGeneration\n const requestedPage = page.value\n isLoading.value = true\n request.clearError()\n try {\n const data = await fetchScopedExperimentPage(requestedPage)\n if (generation !== fetchGeneration) return\n if (requestedPage === 0) {\n experiments.value = data.experiments\n } else {\n experiments.value = [...experiments.value, ...data.experiments]\n }\n total.value = data.total\n lastLoadedAt.value = new Date()\n } catch (err) {\n if (generation !== fetchGeneration) return\n request.setError(err, 'Failed to fetch experiments')\n if (requestedPage === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n if (generation === fetchGeneration) {\n isLoading.value = false\n }\n }\n }\n\n function invalidateFetch(): void {\n fetchGeneration += 1\n isLoading.value = false\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n const allowedTypes = currentAllowedExperimentTypes()\n const allowedSet = allowedTypes ? new Set(allowedTypes) : null\n experimentTypes.value = typesRes.value\n .filter(t => !allowedSet || allowedSet.has(t.value))\n .map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) {\n filters.experimentType = undefined\n }\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n /** The filters the stats route reads; status, sort and paging do not change the counts. */\n function countScope(): string {\n return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])\n }\n\n function refreshStatusCountsOnScopeChange(): void {\n if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()\n }\n\n async function fetchStatusCounts(): Promise<void> {\n const generation = ++statsGeneration\n countedScope = countScope()\n const scope = resolveExperimentTypeScope()\n if (scope.kind === 'blocked') {\n statusCounts.value = {}\n return\n }\n const types = scope.kind === 'multi'\n ? scope.experimentTypes\n : [scope.kind === 'single' ? scope.experimentType : undefined]\n const base = platformBase ?? ''\n try {\n const responses = await Promise.all(\n types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(\n `${base}/experiments/stats?${scopeParams(type)}`,\n )),\n )\n if (generation !== statsGeneration) return\n const counts: Record<string, number> = {}\n for (const response of responses) {\n for (const [status, count] of Object.entries(response.by_status)) {\n counts[status] = (counts[status] ?? 0) + count\n }\n }\n statusCounts.value = counts\n } catch {\n // Counts only decorate the status menu; without them it shows bare labels.\n // Forget the scope so the next status or sort change retries the request.\n if (generation === statsGeneration) {\n statusCounts.value = {}\n countedScope = null\n }\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n invalidateFetch()\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n filters.mine = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n watch(\n () => [\n filters.search,\n filters.status,\n filters.project,\n filters.experimentType,\n filters.datePreset,\n filters.mine,\n sortKey.value,\n ],\n () => invalidateFetch(),\n { flush: 'sync' },\n )\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n refreshStatusCountsOnScopeChange()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n // Also checked on a status or sort change: the cancelled debounce may hold a new search.\n refreshStatusCountsOnScopeChange()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n fetchStatusCounts,\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<{\n /** Text above the experiment list: why the caller asks for an experiment. */\n description?: () => unknown\n /** Extra footer content, before Clear selection. `close()` closes the modal without a selection. */\n footer?: (props: { close: () => void }) => unknown\n}>()\n\nfunction close() {\n emit('update:open', false)\n}\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <div v-if=\"$slots.description\" class=\"mint-experiment-selector__description\">\n <slot name=\"description\" />\n </div>\n\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"$slots.footer || currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <div v-if=\"$slots.footer\" class=\"mint-experiment-selector__footer-slot\">\n <slot name=\"footer\" :close=\"close\" />\n </div>\n <button\n v-if=\"currentExperimentId != null\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<{\n /** Text above the experiment list: why the caller asks for an experiment. */\n description?: () => unknown\n /** Extra footer content, before Clear selection. `close()` closes the modal without a selection. */\n footer?: (props: { close: () => void }) => unknown\n}>()\n\nfunction close() {\n emit('update:open', false)\n}\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <div v-if=\"$slots.description\" class=\"mint-experiment-selector__description\">\n <slot name=\"description\" />\n </div>\n\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"$slots.footer || currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <div v-if=\"$slots.footer\" class=\"mint-experiment-selector__footer-slot\">\n <slot name=\"footer\" :close=\"close\" />\n </div>\n <button\n v-if=\"currentExperimentId != null\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ExperimentSelectorModal-pR5fK3pA.js","names":["$slots"],"sources":["../src/composables/useExperimentSelector.ts","../src/components/ExperimentSelectorModal.vue","../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequest } from './useRequest'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */\n withStatusCounts?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n /** Experiment count per status under every filter except status itself. */\n statusCounts: Ref<Record<string, number>>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n /** Fetch `statusCounts` from `GET /experiments/stats`. */\n fetchStatusCounts: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const {\n limit = 100,\n immediate = false,\n experimentType,\n allowedExperimentTypes: optionAllowedExperimentTypes,\n apiBaseUrl,\n withStatusCounts = false,\n } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequest('Failed to fetch experiments')\n const isLoading = request.isLoading\n const error = request.error\n const lastLoadedAt = ref<Date | null>(null)\n const page = ref(0)\n let fetchGeneration = 0\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const statusCounts = ref<Record<string, number>>({})\n let statsGeneration = 0\n let countedScope: string | null = null\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n mine: undefined,\n })\n\n function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {\n if (values == null) return null\n return [...new Set(values.filter((value): value is string => typeof value === 'string' && value.length > 0))]\n }\n\n function currentAllowedExperimentTypes(): string[] | null {\n if (optionAllowedExperimentTypes !== undefined) {\n return normalizeExperimentTypes(optionAllowedExperimentTypes)\n }\n return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes)\n }\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n type ExperimentTypeScope =\n | { kind: 'all' }\n | { kind: 'blocked' }\n | { kind: 'single'; experimentType: string }\n | { kind: 'multi'; experimentTypes: string[] }\n\n function resolveExperimentTypeScope(): ExperimentTypeScope {\n const allowedTypes = currentAllowedExperimentTypes()\n const selectedType = experimentType ?? filters.experimentType ?? undefined\n\n if (selectedType) {\n if (allowedTypes && !allowedTypes.includes(selectedType)) {\n return { kind: 'blocked' }\n }\n return { kind: 'single', experimentType: selectedType }\n }\n\n if (allowedTypes) {\n if (allowedTypes.length === 0) return { kind: 'blocked' }\n if (allowedTypes.length === 1) return { kind: 'single', experimentType: allowedTypes[0] }\n return { kind: 'multi', experimentTypes: allowedTypes }\n }\n\n return { kind: 'all' }\n }\n\n /** Query params shared by the list and the stats route; status, sort and paging are list-only. */\n function scopeParams(typeFilter: string | undefined): URLSearchParams {\n const params = new URLSearchParams()\n if (typeFilter) params.set('experiment_type', typeFilter)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n if (filters.mine) params.set('mine', 'true')\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n return params\n }\n\n function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {\n const params = scopeParams(typeFilter)\n if (filters.status) params.set('status', filters.status)\n\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n params.set('limit', String(pageLimit))\n params.set('skip', String(skip))\n\n const query = params.toString()\n const base = platformBase ?? ''\n return `${base}/experiments${query ? `?${query}` : ''}`\n }\n\n function sortExperiments(items: ExperimentSummary[]): ExperimentSummary[] {\n const { sortBy, sortOrder } = parseSortKey()\n const direction = sortOrder === 'desc' ? -1 : 1\n return [...items].sort((a, b) => {\n const aValue = a[sortBy] ?? ''\n const bValue = b[sortBy] ?? ''\n if (aValue < bValue) return -1 * direction\n if (aValue > bValue) return 1 * direction\n return 0\n })\n }\n\n async function fetchExperimentList(typeFilter: string | undefined, skip: number, pageLimit: number) {\n return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))\n }\n\n async function fetchScopedExperimentPage(\n requestedPage: number,\n ): Promise<ExperimentListResponse> {\n const scope = resolveExperimentTypeScope()\n const pageSkip = requestedPage * limit\n\n if (scope.kind === 'blocked') {\n return { experiments: [], total: 0 }\n }\n\n if (scope.kind === 'multi') {\n const perTypeLimit = pageSkip + limit\n const responses = await Promise.all(\n scope.experimentTypes.map(type => fetchExperimentList(type, 0, perTypeLimit)),\n )\n const merged = sortExperiments(responses.flatMap(response => response.experiments))\n return {\n experiments: merged.slice(pageSkip, pageSkip + limit),\n total: responses.reduce((sum, response) => sum + response.total, 0),\n }\n }\n\n return await fetchExperimentList(\n scope.kind === 'single' ? scope.experimentType : undefined,\n pageSkip,\n limit,\n )\n }\n\n async function fetchExperiments(): Promise<void> {\n const generation = ++fetchGeneration\n const requestedPage = page.value\n isLoading.value = true\n request.clearError()\n try {\n const data = await fetchScopedExperimentPage(requestedPage)\n if (generation !== fetchGeneration) return\n if (requestedPage === 0) {\n experiments.value = data.experiments\n } else {\n experiments.value = [...experiments.value, ...data.experiments]\n }\n total.value = data.total\n lastLoadedAt.value = new Date()\n } catch (err) {\n if (generation !== fetchGeneration) return\n request.setError(err, 'Failed to fetch experiments')\n if (requestedPage === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n if (generation === fetchGeneration) {\n isLoading.value = false\n }\n }\n }\n\n function invalidateFetch(): void {\n fetchGeneration += 1\n isLoading.value = false\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n const allowedTypes = currentAllowedExperimentTypes()\n const allowedSet = allowedTypes ? new Set(allowedTypes) : null\n experimentTypes.value = typesRes.value\n .filter(t => !allowedSet || allowedSet.has(t.value))\n .map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) {\n filters.experimentType = undefined\n }\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n /** The filters the stats route reads; status, sort and paging do not change the counts. */\n function countScope(): string {\n return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])\n }\n\n function refreshStatusCountsOnScopeChange(): void {\n if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()\n }\n\n async function fetchStatusCounts(): Promise<void> {\n const generation = ++statsGeneration\n countedScope = countScope()\n const scope = resolveExperimentTypeScope()\n if (scope.kind === 'blocked') {\n statusCounts.value = {}\n return\n }\n const types = scope.kind === 'multi'\n ? scope.experimentTypes\n : [scope.kind === 'single' ? scope.experimentType : undefined]\n const base = platformBase ?? ''\n try {\n const responses = await Promise.all(\n types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(\n `${base}/experiments/stats?${scopeParams(type)}`,\n )),\n )\n if (generation !== statsGeneration) return\n const counts: Record<string, number> = {}\n for (const response of responses) {\n for (const [status, count] of Object.entries(response.by_status)) {\n counts[status] = (counts[status] ?? 0) + count\n }\n }\n statusCounts.value = counts\n } catch {\n // Counts only decorate the status menu; without them it shows bare labels.\n // Forget the scope so the next status or sort change retries the request.\n if (generation === statsGeneration) {\n statusCounts.value = {}\n countedScope = null\n }\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n invalidateFetch()\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n filters.mine = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n watch(\n () => [\n filters.search,\n filters.status,\n filters.project,\n filters.experimentType,\n filters.datePreset,\n filters.mine,\n sortKey.value,\n ],\n () => invalidateFetch(),\n { flush: 'sync' },\n )\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n refreshStatusCountsOnScopeChange()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n // Also checked on a status or sort change: the cancelled debounce may hold a new search.\n refreshStatusCountsOnScopeChange()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n fetchStatusCounts,\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<{\n /** Text above the experiment list: why the caller asks for an experiment. */\n description?: () => unknown\n /** Extra footer content, before Clear selection. `close()` closes the modal without a selection. */\n footer?: (props: { close: () => void }) => unknown\n}>()\n\nfunction close() {\n emit('update:open', false)\n}\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <div v-if=\"$slots.description\" class=\"mint-experiment-selector__description\">\n <slot name=\"description\" />\n </div>\n\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"$slots.footer || currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <div v-if=\"$slots.footer\" class=\"mint-experiment-selector__footer-slot\">\n <slot name=\"footer\" :close=\"close\" />\n </div>\n <button\n v-if=\"currentExperimentId != null\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\ndefineSlots<{\n /** Text above the experiment list: why the caller asks for an experiment. */\n description?: () => unknown\n /** Extra footer content, before Clear selection. `close()` closes the modal without a selection. */\n footer?: (props: { close: () => void }) => unknown\n}>()\n\nfunction close() {\n emit('update:open', false)\n}\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <div v-if=\"$slots.description\" class=\"mint-experiment-selector__description\">\n <slot name=\"description\" />\n </div>\n\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"$slots.footer || currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <div v-if=\"$slots.footer\" class=\"mint-experiment-selector__footer-slot\">\n <slot name=\"footer\" :close=\"close\" />\n </div>\n <button\n v-if=\"currentExperimentId != null\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n"],"mappings":";;;;;;;;;AAeA,SAAS,qBAAkD;CACzD,IAAI,OAAO,WAAW,aAAa,OAAO,KAAA;CAC1C,OAAQ,OAA8D;AACxE;AAEA,SAAS,oBAAwC;CAC/C,OAAO,mBAAmB,CAAC,EAAE;AAC/B;;AA2DA,SAAgB,sBACd,UAAwC,CAAC,GACZ;CAC7B,MAAM,EACJ,QAAQ,KACR,YAAY,OACZ,gBACA,wBAAwB,8BACxB,YACA,mBAAmB,UACjB;CACJ,MAAM,eAAe,cAAc,kBAAkB;CACrD,MAAM,MAAM,OAAO;CAEnB,MAAM,cAAc,IAAyB,CAAC,CAAC;CAC/C,MAAM,QAAQ,IAAI,CAAC;CACnB,MAAM,qBAAqB,IAA8B,IAAI;CAC7D,MAAM,UAAU,WAAW,6BAA6B;CACxD,MAAM,YAAY,QAAQ;CAC1B,MAAM,QAAQ,QAAQ;CACtB,MAAM,eAAe,IAAiB,IAAI;CAC1C,MAAM,OAAO,IAAI,CAAC;CAClB,IAAI,kBAAkB;CAGtB,MAAM,UAAU,IAAY,iBAAiB;CAG7C,MAAM,kBAAkB,IAA4B,CAAC,CAAC;CACtD,MAAM,WAAW,IAA4B,CAAC,CAAC;CAC/C,IAAI,uBAAuB;CAE3B,MAAM,eAAe,IAA4B,CAAC,CAAC;CACnD,IAAI,kBAAkB;CACtB,IAAI,eAA8B;CAElC,MAAM,UAAU,eAAe,YAAY,MAAM,SAAS,MAAM,KAAK;CAErE,MAAM,UAA6B,SAAS;EAC1C,QAAQ,KAAA;EACR,QAAQ,KAAA;EACR,SAAS,KAAA;EACT,gBAAgB,KAAA;EAChB,YAAY,KAAA;EACZ,MAAM,KAAA;CACR,CAAC;CAED,SAAS,yBAAyB,QAAsD;EACtF,IAAI,UAAU,MAAM,OAAO;EAC3B,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,CAAC,CAAC;CAC9G;CAEA,SAAS,gCAAiD;EACxD,IAAI,iCAAiC,KAAA,GACnC,OAAO,yBAAyB,4BAA4B;EAE9D,OAAO,yBAAyB,mBAAmB,CAAC,EAAE,sBAAsB;CAC9E;CAEA,SAAS,eAA2E;EAClF,MAAM,CAAC,OAAO,SAAS,QAAQ,MAAM,MAAM,GAAG;EAC9C,OAAO;GACL,QAAS,SAAS;GAClB,WAAY,SAAS;EACvB;CACF;CAQA,SAAS,6BAAkD;EACzD,MAAM,eAAe,8BAA8B;EACnD,MAAM,eAAe,kBAAkB,QAAQ,kBAAkB,KAAA;EAEjE,IAAI,cAAc;GAChB,IAAI,gBAAgB,CAAC,aAAa,SAAS,YAAY,GACrD,OAAO,EAAE,MAAM,UAAU;GAE3B,OAAO;IAAE,MAAM;IAAU,gBAAgB;GAAa;EACxD;EAEA,IAAI,cAAc;GAChB,IAAI,aAAa,WAAW,GAAG,OAAO,EAAE,MAAM,UAAU;GACxD,IAAI,aAAa,WAAW,GAAG,OAAO;IAAE,MAAM;IAAU,gBAAgB,aAAa;GAAG;GACxF,OAAO;IAAE,MAAM;IAAS,iBAAiB;GAAa;EACxD;EAEA,OAAO,EAAE,MAAM,MAAM;CACvB;;CAGA,SAAS,YAAY,YAAiD;EACpE,MAAM,SAAS,IAAI,gBAAgB;EACnC,IAAI,YAAY,OAAO,IAAI,mBAAmB,UAAU;EACxD,IAAI,QAAQ,QAAQ,OAAO,IAAI,UAAU,QAAQ,MAAM;EACvD,IAAI,QAAQ,SAAS,OAAO,IAAI,WAAW,QAAQ,OAAO;EAC1D,IAAI,QAAQ,MAAM,OAAO,IAAI,QAAQ,MAAM;EAC3C,IAAI,QAAQ,YACV,OAAO,IAAI,iBAAiB,gBAAgB,QAAQ,UAAU,CAAC;EAEjE,OAAO;CACT;CAEA,SAAS,uBAAuB,YAAgC,MAAc,WAA2B;EACvG,MAAM,SAAS,YAAY,UAAU;EACrC,IAAI,QAAQ,QAAQ,OAAO,IAAI,UAAU,QAAQ,MAAM;EAEvD,MAAM,EAAE,QAAQ,cAAc,aAAa;EAC3C,OAAO,IAAI,WAAW,MAAM;EAC5B,OAAO,IAAI,cAAc,SAAS;EAElC,OAAO,IAAI,SAAS,OAAO,SAAS,CAAC;EACrC,OAAO,IAAI,QAAQ,OAAO,IAAI,CAAC;EAE/B,MAAM,QAAQ,OAAO,SAAS;EAE9B,OAAO,GADM,gBAAgB,GACd,cAAc,QAAQ,IAAI,UAAU;CACrD;CAEA,SAAS,gBAAgB,OAAiD;EACxE,MAAM,EAAE,QAAQ,cAAc,aAAa;EAC3C,MAAM,YAAY,cAAc,SAAS,KAAK;EAC9C,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM;GAC/B,MAAM,SAAS,EAAE,WAAW;GAC5B,MAAM,SAAS,EAAE,WAAW;GAC5B,IAAI,SAAS,QAAQ,OAAO,KAAK;GACjC,IAAI,SAAS,QAAQ,OAAO,IAAI;GAChC,OAAO;EACT,CAAC;CACH;CAEA,eAAe,oBAAoB,YAAgC,MAAc,WAAmB;EAClG,OAAO,MAAM,IAAI,IAA4B,uBAAuB,YAAY,MAAM,SAAS,CAAC;CAClG;CAEA,eAAe,0BACb,eACiC;EACjC,MAAM,QAAQ,2BAA2B;EACzC,MAAM,WAAW,gBAAgB;EAEjC,IAAI,MAAM,SAAS,WACjB,OAAO;GAAE,aAAa,CAAC;GAAG,OAAO;EAAE;EAGrC,IAAI,MAAM,SAAS,SAAS;GAC1B,MAAM,eAAe,WAAW;GAChC,MAAM,YAAY,MAAM,QAAQ,IAC9B,MAAM,gBAAgB,KAAI,SAAQ,oBAAoB,MAAM,GAAG,YAAY,CAAC,CAC9E;GAEA,OAAO;IACL,aAFa,gBAAgB,UAAU,SAAQ,aAAY,SAAS,WAAW,CAElE,CAAA,CAAO,MAAM,UAAU,WAAW,KAAK;IACpD,OAAO,UAAU,QAAQ,KAAK,aAAa,MAAM,SAAS,OAAO,CAAC;GACpE;EACF;EAEA,OAAO,MAAM,oBACX,MAAM,SAAS,WAAW,MAAM,iBAAiB,KAAA,GACjD,UACA,KACF;CACF;CAEA,eAAe,mBAAkC;EAC/C,MAAM,aAAa,EAAE;EACrB,MAAM,gBAAgB,KAAK;EAC3B,UAAU,QAAQ;EAClB,QAAQ,WAAW;EACnB,IAAI;GACF,MAAM,OAAO,MAAM,0BAA0B,aAAa;GAC1D,IAAI,eAAe,iBAAiB;GACpC,IAAI,kBAAkB,GACpB,YAAY,QAAQ,KAAK;QAEzB,YAAY,QAAQ,CAAC,GAAG,YAAY,OAAO,GAAG,KAAK,WAAW;GAEhE,MAAM,QAAQ,KAAK;GACnB,aAAa,wBAAQ,IAAI,KAAK;EAChC,SAAS,KAAK;GACZ,IAAI,eAAe,iBAAiB;GACpC,QAAQ,SAAS,KAAK,6BAA6B;GACnD,IAAI,kBAAkB,GAAG;IACvB,YAAY,QAAQ,CAAC;IACrB,MAAM,QAAQ;GAChB;EACF,UAAU;GACR,IAAI,eAAe,iBACjB,UAAU,QAAQ;EAEtB;CACF;CAEA,SAAS,kBAAwB;EAC/B,mBAAmB;EACnB,UAAU,QAAQ;CACpB;CAEA,eAAe,qBAAoC;EACjD,IAAI,sBAAsB;EAC1B,uBAAuB;EAEvB,MAAM,OAAO,gBAAgB;EAC7B,MAAM,CAAC,UAAU,eAAe,MAAM,QAAQ,WAAW,CACvD,IAAI,IAA6D,GAAG,KAAK,8BAA8B,GACvG,IAAI,IAAsE,GAAG,KAAK,UAAU,CAC9F,CAAC;EAED,IAAI,SAAS,WAAW,eAAe,MAAM,QAAQ,SAAS,KAAK,GAAG;GACpE,MAAM,eAAe,8BAA8B;GACnD,MAAM,aAAa,eAAe,IAAI,IAAI,YAAY,IAAI;GAC1D,gBAAgB,QAAQ,SAAS,MAC9B,QAAO,MAAK,CAAC,cAAc,WAAW,IAAI,EAAE,KAAK,CAAC,CAAC,CACnD,KAAI,OAAM;IACT,OAAO,EAAE;IACT,OAAO,EAAE;IACT,OAAO,EAAE;GACX,EAAE;GACJ,IAAI,QAAQ,kBAAkB,cAAc,CAAC,WAAW,IAAI,QAAQ,cAAc,GAChF,QAAQ,iBAAiB,KAAA;EAE7B;EAEA,IAAI,YAAY,WAAW,eAAe,YAAY,OAAO,YAAY,MAAM,QAAQ,YAAY,MAAM,QAAQ,GAC/G,SAAS,QAAQ,YAAY,MAAM,SAAS,KAAI,OAAM;GACpD,OAAO,EAAE;GACT,OAAO,EAAE;EACX,EAAE;CAEN;;CAGA,SAAS,aAAqB;EAC5B,OAAO,KAAK,UAAU;GAAC,QAAQ;GAAQ,QAAQ;GAAS,QAAQ;GAAgB,QAAQ;GAAY,QAAQ;EAAI,CAAC;CACnH;CAEA,SAAS,mCAAyC;EAChD,IAAI,oBAAoB,WAAW,MAAM,cAAc,kBAAkB;CAC3E;CAEA,eAAe,oBAAmC;EAChD,MAAM,aAAa,EAAE;EACrB,eAAe,WAAW;EAC1B,MAAM,QAAQ,2BAA2B;EACzC,IAAI,MAAM,SAAS,WAAW;GAC5B,aAAa,QAAQ,CAAC;GACtB;EACF;EACA,MAAM,QAAQ,MAAM,SAAS,UACzB,MAAM,kBACN,CAAC,MAAM,SAAS,WAAW,MAAM,iBAAiB,KAAA,CAAS;EAC/D,MAAM,OAAO,gBAAgB;EAC7B,IAAI;GACF,MAAM,YAAY,MAAM,QAAQ,IAC9B,MAAM,KAAI,SAAQ,IAAI,IACpB,GAAG,KAAK,qBAAqB,YAAY,IAAI,GAC/C,CAAC,CACH;GACA,IAAI,eAAe,iBAAiB;GACpC,MAAM,SAAiC,CAAC;GACxC,KAAK,MAAM,YAAY,WACrB,KAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,SAAS,SAAS,GAC7D,OAAO,WAAW,OAAO,WAAW,KAAK;GAG7C,aAAa,QAAQ;EACvB,QAAQ;GAGN,IAAI,eAAe,iBAAiB;IAClC,aAAa,QAAQ,CAAC;IACtB,eAAe;GACjB;EACF;CACF;CAEA,eAAe,WAA0B;EACvC,IAAI,CAAC,QAAQ,SAAS,UAAU,OAAO;EACvC,KAAK;EACL,MAAM,iBAAiB;CACzB;CAEA,SAAS,QAAc;EACrB,KAAK,QAAQ;EACb,YAAY,QAAQ,CAAC;EACrB,MAAM,QAAQ;EACd,iBAAiB;CACnB;CAEA,SAAS,OAAO,YAAqC;EACnD,mBAAmB,QAAQ;CAC7B;CAEA,SAAS,QAAc;EACrB,gBAAgB;EAChB,mBAAmB,QAAQ;EAC3B,QAAQ,SAAS,KAAA;EACjB,QAAQ,SAAS,KAAA;EACjB,QAAQ,UAAU,KAAA;EAClB,QAAQ,iBAAiB,KAAA;EACzB,QAAQ,aAAa,KAAA;EACrB,QAAQ,OAAO,KAAA;EACf,QAAQ,QAAQ;EAChB,KAAK,QAAQ;EACb,QAAQ,WAAW;CACrB;CAEA,YACQ;EACJ,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACV,SACM,gBAAgB,GACtB,EAAE,OAAO,OAAO,CAClB;CAEA,MAAM,cAAc,wBACZ,QAAQ,cACR;EACJ,KAAK,QAAQ;EACb,iBAAiB;EACjB,iCAAiC;CACnC,GACA,EAAE,OAAO,IAAI,CACf;CAGA,YACQ;EAAC,QAAQ;EAAQ,QAAQ;EAAS,QAAQ;EAAgB,QAAQ;EAAY,QAAQ;EAAM,QAAQ;CAAK,SACzG;EACJ,YAAY,OAAO;EACnB,KAAK,QAAQ;EACb,iBAAiB;EAEjB,iCAAiC;CACnC,CACF;CAEA,IAAI,WACF,iBAAiB;CAGnB,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO;EACP;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECxaA,MAAM,QAAQ;EASd,MAAM,OAAO;EAWb,SAAS,QAAQ;GACf,KAAK,eAAe,KAAK;EAC3B;EAEA,MAAM,kBAAkB,mBAAmB;EAE3C,MAAM,EACJ,aACA,SACA,WACA,OACA,iBACA,UACA,cACA,OAAO,kBACP,oBACA,sBACE,sBAAsB;GACxB,gBAAgB,MAAM;GACtB,wBAAwB,MAAM;GAC9B,kBAAkB;EACpB,CAAC;EAGD,MAAM,YAAY,IAAI,CAAC;EAEvB,MAAM,eAAe;GAAC;GAAW;GAAW;GAAa;EAAW;EAEpE,MAAM,0BAA0B,eAC9B,CAAC,MAAM,kBAAkB,gBAAgB,MAAM,SAAS,CAC1D;EAeA,SAAS,YAAY,KAAgB,UAAkB,SAA+D;GACpH,MAAM,UAAU,QAAQ,QAAQ;GAChC,OAAO,CAAC;IAAE,OAAO;IAAI,OAAO;GAAS,GAAG,GAAG,OAAO,CAAC,CAAC,KAAI,YAAW;IACjE,IAAI,OAAO;IACX,OAAO,OAAO;IACd,SAAS,OAAO,UAAU;GAC5B,EAAE;EACJ;EAEA,MAAM,cAAc,eAA6B;GAC/C,MAAM,QAAsB,CAAC;IAC3B,KAAK;IACL,OAAO;IACP,MAAM;IACN,OAAO,YAAY,UAAU,cAAc,aAAa,KAAI,WAAU;KACpE,MAAM,QAAQ,aAAa,MAAM;KACjC,MAAM,QAAQ,OAAwB,MAAM;KAC5C,OAAO;MAAE,OAAO;MAAQ,OAAO,UAAU,KAAA,IAAY,QAAQ,GAAG,MAAM,KAAK;KAAQ;IACrF,CAAC,CAAC;IACF,aAAa,QAAQ,SAAS,OAAwB,QAAQ,MAAM,IAAI,KAAA;IACxE,SAAS,QAAQ,SAAS,KAAsB,QAAQ,MAAM,IAAI,KAAA;GACpE,CAAC;GACD,IAAI,SAAS,MAAM,QACjB,MAAM,KAAK;IACT,KAAK;IACL,OAAO;IACP,MAAM;IACN,OAAO,YAAY,WAAW,eAAe,SAAS,KAAK;IAC3D,aAAa,QAAQ,WAAW,KAAA;GAClC,CAAC;GAEH,IAAI,wBAAwB,OAC1B,MAAM,KAAK;IACT,KAAK;IACL,OAAO;IACP,MAAM;IACN,OAAO,YAAY,kBAAkB,YAAY,gBAAgB,KAAK;IACtE,aAAa,gBAAgB,MAAM,MAAK,MAAK,EAAE,UAAU,QAAQ,cAAc,CAAC,EAAE,SAC7E,QAAQ,kBAAkB,KAAA;GACjC,CAAC;GAEH,OAAO;EACT,CAAC;EAED,MAAM,oBAAoB,eACxB;GAAC,QAAQ;GAAQ,QAAQ;GAAS,QAAQ;GAAgB,QAAQ;EAAI,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAC1F;EAEA,SAAS,eAAe;GACtB,QAAQ,SAAS,KAAA;GACjB,QAAQ,UAAU,KAAA;GAClB,QAAQ,iBAAiB,KAAA;GACzB,QAAQ,OAAO,KAAA;EACjB;EAEA,SAAS,SAAS,YAA+B;GAC/C,MAAM,QAAQ,WAAW;GACzB,OAAO;IACL,MAAM,WAAW;IACjB,OAAO,WAAW;IAClB,MAAM,CACJ,WAAW,gBAAgB,WAAW,WAAW,IACjD,QAAQ,CAAC,MAAM,YAAY,MAAM,SAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,MAAM,WAAW,EAC5F,CAAC,CAAC,OAAO,OAAO;IAChB,QAAQ,WAAW,SACf;KAAE,OAAO,OAAwB,WAAW,MAAM;KAAG,MAAM,KAAsB,WAAW,MAAM;IAAE,IACpG,KAAA;IACJ,MAAM,qBAAqB,WAAW,UAAU;GAClD;EACF;EAEA,SAAS,UAA6C,KAAQ,OAAwB;GACnF,QAAqC,OAAO,OAAO,KAAK,KAAK,KAAA;EAChE;EAEA,SAAS,aAAa,YAA+B;GACnD,gBAAgB,OAAO,UAAU;GACjC,KAAK,eAAe,KAAK;EAC3B;EAEA,SAAS,iBAAiB;GACxB,gBAAgB,MAAM;GACtB,KAAK,eAAe,KAAK;EAC3B;EAGA,YACQ,MAAM,OACX,WAAW;GACV,IAAI,QAAQ;IACV,UAAU;IACV,mBAAmB;IACnB,iBAAiB;IACjB,kBAAkB;GACpB;EACF,GACA,EAAE,WAAW,KAAK,CACpB;;GAIE,OAAA,UAAA,GAAA,YA4FY,mBAAA;IA3FT,MAAM,QAAA;IACN,OAAO,QAAA;IACP,MAAM,QAAA;IACN,iBAAW,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,eAAgB,MAAM;;IAExC,SAAA,cAEM;KAFKA,KAAAA,OAAO,eAAlB,UAAA,GAAA,mBAEM,OAFN,YAEM,CADJ,WAA2B,KAAA,QAAA,aAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;MAG7B,UAAA,GAAA,YAmE6B,oCAAA;MAlE1B,KAAK,UAAA;MACL,QAAQ,MAAA,OAAA,CAAO,CAAC,UAAM;MACvB,OAAM;MACN,oBAAA;MACA,OAAM;MACN,aAAY;MACX,OAAO,MAAA,WAAA;MACP,aAAW,eAAe,WAAW;MAC3B;MACV,gBAAc,QAAA;MACd,SAAS,MAAA,SAAA;MACT,OAAO,MAAA,KAAA;MACR,eAAY;MACX,mBAAa,OAAA,OAAA,OAAA,MAAA,WAAE,MAAA,OAAA,CAAO,CAAC,SAAS;MAChC,UAAQ;;MAEE,SAAO,cAEa;QAD7B,UAAA,IAAA,GAAA,mBA6Ba,UAAA,MAAA,WA5BI,YAAA,QAAR,SAAI;QADb,OAAA,UAAA,GAAA,YA6Ba,oBAAA;SA3BV,KAAK,KAAK;SACV,OAAO,KAAK;SACZ,OAAO,KAAK;SACZ,WAAQ,SAAQ,UAAU,KAAK,KAAK,KAAK,EAAE;;SAEjC,SAAO,SAoBP,EAAA,MApBiB,UAAU,QAAQ,cAAO,CACnD,mBAmBS,UAAA;UAlBP,MAAK;UACL,OAAK,eAAA,CAAC,oCAAkC,EAAA,4CACc,KAAK,YAAW,CAAA,CAAA;UACrE,cAAU,GAAK,KAAK,MAAK,IAAK,KAAK,eAAe,KAAK,MAAK,EAAA,CAAI;UACjE,iBAAc;UACb,iBAAe;UACf,SAAO;UACP,WAAS;;UAGF,KAAK,WADb,UAAA,GAAA,mBAKE,QAAA;;WAHA,OAAK,eAAA,CAAC,iCAA+B,kCACK,KAAK,SAAO,CAAA;WACtD,eAAY;UAEd,GAAA,MAAA,CAAA,MAAA,UAAA,GAAA,YAAkE,wBAAlD,KAAK,IAAI,GAAA;;WAAU,MAAM;WAAI,eAAY;;UACzD,mBAAgG,QAAhG,YAAgG,gBAAxC,KAAK,eAAe,KAAK,KAAK,GAAA,CAAA;UACtF,YAA6C,MAAA,WAAA,GAAA;WAA/B,MAAM;WAAI,eAAY;;;;;;;;;;OAI1C,mBASS,UAAA;QARP,MAAK;QACL,OAAK,eAAA,CAAC,oCAAkC,EAAA,4CACc,MAAA,OAAA,CAAO,CAAC,KAAI,CAAA,CAAA;QACjE,gBAAY,CAAA,CAAI,MAAA,OAAA,CAAO,CAAC;QACxB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,MAAA,OAAA,CAAO,CAAC,OAAO,MAAA,OAAA,CAAO,CAAC,OAAO,KAAA,IAAS;OAE/C,GAAA,CAAA,YAAsC,MAAA,IAAA,GAAA;QAA/B,MAAM;QAAI,eAAY;OAAS,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,UAExC,EAAA,EAAA,GAAA,IAAA,UAAA;OAEQ,kBAAA,SADR,UAAA,GAAA,mBAOS,UAAA;;QALP,MAAK;QACL,OAAM;QACL,SAAO;OACT,GAAA,YACO,gBAAG,kBAAA,KAAiB,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;;;;;;;;;;;KAKrBA,KAAAA,OAAO,UAAU,QAAA,uBAAmB,QAA/C,UAAA,GAAA,mBAYM,OAZN,YAYM,CAXOA,KAAAA,OAAO,UAAlB,UAAA,GAAA,mBAEM,OAFN,YAEM,CADJ,WAAqC,KAAA,QAAA,UAAA,EAAT,MAAK,CAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,GAG3B,QAAA,uBAAmB,QAD3B,UAAA,GAAA,mBAOS,UAAA;;MALP,MAAK;MACL,OAAM;MACL,SAAO;KACT,GAAA,mBAED,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
@@ -1,4 +1,4 @@
1
- import { R as useRovingFocus, W as normalizeOptionInput } from "./records-B4yl2oIu.js";
1
+ import { R as useRovingFocus, W as normalizeOptionInput } from "./records-CR_pYzIP.js";
2
2
  import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, renderList, toDisplayString } from "vue";
3
3
  //#region src/components/SegmentedControl.vue?vue&type=script&setup=true&lang.ts
4
4
  var _hoisted_1 = [
@@ -126,4 +126,4 @@ var SegmentedControl_default = /* @__PURE__ */ defineComponent({
126
126
  //#endregion
127
127
  export { SegmentedControl_default as t };
128
128
 
129
- //# sourceMappingURL=SegmentedControl-hOgBVdju.js.map
129
+ //# sourceMappingURL=SegmentedControl-CJ7EAmOq.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedControl-hOgBVdju.js","names":[],"sources":["../src/components/SegmentedControl.vue","../src/components/SegmentedControl.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** Renders a segmented button group for single-option selection, defaulting to the card variant. */\nimport { computed, type ComponentPublicInstance } from 'vue'\nimport type { OptionPrimitive, SelectOption, SelectOptionInput, SegmentedControlVariant, SegmentedControlSize } from '../types'\nimport { normalizeOptionInput } from '../utils/options'\nimport { useRovingFocus } from '../composables/useRovingFocus'\n\ninterface Props {\n modelValue: string | number\n options: SelectOptionInput[]\n variant?: SegmentedControlVariant\n size?: SegmentedControlSize\n fullWidth?: boolean\n disabled?: boolean\n disabledValues?: Array<string | number>\n}\n\ntype SegmentedOption = SelectOption<OptionPrimitive>\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'card',\n size: 'md',\n fullWidth: false,\n disabled: false,\n disabledValues: () => [],\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number]\n}>()\n\nconst normalizedOptions = computed<SegmentedOption[]>(() => props.options.map(normalizeOptionInput<OptionPrimitive, SegmentedOption>))\nconst disabledValueSet = computed(() => new Set(props.disabledValues))\n\nfunction isOptionDisabled(option: SegmentedOption) {\n return option.disabled === true || disabledValueSet.value.has(option.value)\n}\n\nfunction isSelectionDisabled(option: SegmentedOption) {\n return props.disabled || isOptionDisabled(option)\n}\n\nfunction handleSelect(option: SegmentedOption) {\n if (isSelectionDisabled(option)) return\n emit('update:modelValue', option.value)\n}\n\n// Roving tabindex over the radio group: only the checked option is tabbable and\n// Left/Right (plus Home/End) move between the selectable options, matching BaseTabs.\nconst optionRefs = new Map<string, HTMLElement>()\n\nfunction optionId(option: SegmentedOption): string {\n return String(option.value)\n}\n\nfunction setOptionRef(id: string, el: Element | ComponentPublicInstance | null) {\n if (el instanceof HTMLElement) {\n optionRefs.set(id, el)\n } else {\n optionRefs.delete(id)\n }\n}\n\nconst selectableOptions = computed(() =>\n normalizedOptions.value.filter(option => !isSelectionDisabled(option)),\n)\n\n/** The one option carrying `tabindex=\"0\"`: the checked one, else the first selectable. */\nconst tabbableId = computed(() => {\n const checked = selectableOptions.value.find(option => option.value === props.modelValue)\n const target = checked ?? selectableOptions.value[0]\n return target ? optionId(target) : ''\n})\n\nfunction isTabbable(option: SegmentedOption): boolean {\n return optionId(option) === tabbableId.value\n}\n\nconst { handleKeydown: handleArrowKeydown } = useRovingFocus({\n ids: () => selectableOptions.value.map(optionId),\n activeId: () => tabbableId.value,\n activate: (id) => {\n const option = selectableOptions.value.find(candidate => optionId(candidate) === id)\n if (option) handleSelect(option)\n },\n getElement: (id) => optionRefs.get(id),\n orientation: 'horizontal',\n})\n\nfunction handleKeydown(event: KeyboardEvent, option: SegmentedOption) {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n handleSelect(option)\n return\n }\n handleArrowKeydown(event)\n}\n</script>\n\n<template>\n <div\n :class=\"[\n 'mint-segmented-control',\n `mint-segmented-control--${variant}`,\n `mint-segmented-control--${size}`,\n fullWidth ? 'mint-segmented-control--full-width' : '',\n disabled ? 'mint-segmented-control--disabled' : '',\n ]\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in normalizedOptions\"\n :key=\"String(option.value)\"\n :ref=\"(el) => setOptionRef(String(option.value), el)\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"modelValue === option.value\"\n :tabindex=\"isTabbable(option) ? 0 : -1\"\n :disabled=\"isSelectionDisabled(option)\"\n :class=\"[\n 'mint-segmented-control__option',\n `mint-segmented-control__option--${variant}`,\n `mint-segmented-control__option--${size}`,\n modelValue === option.value ? 'mint-segmented-control__option--active' : '',\n isOptionDisabled(option) ? 'mint-segmented-control__option--disabled' : '',\n ]\"\n @click=\"handleSelect(option)\"\n @keydown=\"handleKeydown($event, option)\"\n >\n <span class=\"mint-segmented-control__label\"><span\n v-if=\"option.color\"\n class=\"mint-segmented-control__swatch\"\n :style=\"{ background: option.color }\"\n aria-hidden=\"true\"\n />{{ option.label }}</span>\n <span\n v-if=\"option.description && variant === 'card'\"\n class=\"mint-segmented-control__description\"\n >\n {{ option.description }}\n </span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/** Renders a segmented button group for single-option selection, defaulting to the card variant. */\nimport { computed, type ComponentPublicInstance } from 'vue'\nimport type { OptionPrimitive, SelectOption, SelectOptionInput, SegmentedControlVariant, SegmentedControlSize } from '../types'\nimport { normalizeOptionInput } from '../utils/options'\nimport { useRovingFocus } from '../composables/useRovingFocus'\n\ninterface Props {\n modelValue: string | number\n options: SelectOptionInput[]\n variant?: SegmentedControlVariant\n size?: SegmentedControlSize\n fullWidth?: boolean\n disabled?: boolean\n disabledValues?: Array<string | number>\n}\n\ntype SegmentedOption = SelectOption<OptionPrimitive>\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'card',\n size: 'md',\n fullWidth: false,\n disabled: false,\n disabledValues: () => [],\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number]\n}>()\n\nconst normalizedOptions = computed<SegmentedOption[]>(() => props.options.map(normalizeOptionInput<OptionPrimitive, SegmentedOption>))\nconst disabledValueSet = computed(() => new Set(props.disabledValues))\n\nfunction isOptionDisabled(option: SegmentedOption) {\n return option.disabled === true || disabledValueSet.value.has(option.value)\n}\n\nfunction isSelectionDisabled(option: SegmentedOption) {\n return props.disabled || isOptionDisabled(option)\n}\n\nfunction handleSelect(option: SegmentedOption) {\n if (isSelectionDisabled(option)) return\n emit('update:modelValue', option.value)\n}\n\n// Roving tabindex over the radio group: only the checked option is tabbable and\n// Left/Right (plus Home/End) move between the selectable options, matching BaseTabs.\nconst optionRefs = new Map<string, HTMLElement>()\n\nfunction optionId(option: SegmentedOption): string {\n return String(option.value)\n}\n\nfunction setOptionRef(id: string, el: Element | ComponentPublicInstance | null) {\n if (el instanceof HTMLElement) {\n optionRefs.set(id, el)\n } else {\n optionRefs.delete(id)\n }\n}\n\nconst selectableOptions = computed(() =>\n normalizedOptions.value.filter(option => !isSelectionDisabled(option)),\n)\n\n/** The one option carrying `tabindex=\"0\"`: the checked one, else the first selectable. */\nconst tabbableId = computed(() => {\n const checked = selectableOptions.value.find(option => option.value === props.modelValue)\n const target = checked ?? selectableOptions.value[0]\n return target ? optionId(target) : ''\n})\n\nfunction isTabbable(option: SegmentedOption): boolean {\n return optionId(option) === tabbableId.value\n}\n\nconst { handleKeydown: handleArrowKeydown } = useRovingFocus({\n ids: () => selectableOptions.value.map(optionId),\n activeId: () => tabbableId.value,\n activate: (id) => {\n const option = selectableOptions.value.find(candidate => optionId(candidate) === id)\n if (option) handleSelect(option)\n },\n getElement: (id) => optionRefs.get(id),\n orientation: 'horizontal',\n})\n\nfunction handleKeydown(event: KeyboardEvent, option: SegmentedOption) {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n handleSelect(option)\n return\n }\n handleArrowKeydown(event)\n}\n</script>\n\n<template>\n <div\n :class=\"[\n 'mint-segmented-control',\n `mint-segmented-control--${variant}`,\n `mint-segmented-control--${size}`,\n fullWidth ? 'mint-segmented-control--full-width' : '',\n disabled ? 'mint-segmented-control--disabled' : '',\n ]\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in normalizedOptions\"\n :key=\"String(option.value)\"\n :ref=\"(el) => setOptionRef(String(option.value), el)\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"modelValue === option.value\"\n :tabindex=\"isTabbable(option) ? 0 : -1\"\n :disabled=\"isSelectionDisabled(option)\"\n :class=\"[\n 'mint-segmented-control__option',\n `mint-segmented-control__option--${variant}`,\n `mint-segmented-control__option--${size}`,\n modelValue === option.value ? 'mint-segmented-control__option--active' : '',\n isOptionDisabled(option) ? 'mint-segmented-control__option--disabled' : '',\n ]\"\n @click=\"handleSelect(option)\"\n @keydown=\"handleKeydown($event, option)\"\n >\n <span class=\"mint-segmented-control__label\"><span\n v-if=\"option.color\"\n class=\"mint-segmented-control__swatch\"\n :style=\"{ background: option.color }\"\n aria-hidden=\"true\"\n />{{ option.label }}</span>\n <span\n v-if=\"option.description && variant === 'card'\"\n class=\"mint-segmented-control__description\"\n >\n {{ option.description }}\n </span>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"SegmentedControl-CJ7EAmOq.js","names":[],"sources":["../src/components/SegmentedControl.vue","../src/components/SegmentedControl.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** Renders a segmented button group for single-option selection, defaulting to the card variant. */\nimport { computed, type ComponentPublicInstance } from 'vue'\nimport type { OptionPrimitive, SelectOption, SelectOptionInput, SegmentedControlVariant, SegmentedControlSize } from '../types'\nimport { normalizeOptionInput } from '../utils/options'\nimport { useRovingFocus } from '../composables/useRovingFocus'\n\ninterface Props {\n modelValue: string | number\n options: SelectOptionInput[]\n variant?: SegmentedControlVariant\n size?: SegmentedControlSize\n fullWidth?: boolean\n disabled?: boolean\n disabledValues?: Array<string | number>\n}\n\ntype SegmentedOption = SelectOption<OptionPrimitive>\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'card',\n size: 'md',\n fullWidth: false,\n disabled: false,\n disabledValues: () => [],\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number]\n}>()\n\nconst normalizedOptions = computed<SegmentedOption[]>(() => props.options.map(normalizeOptionInput<OptionPrimitive, SegmentedOption>))\nconst disabledValueSet = computed(() => new Set(props.disabledValues))\n\nfunction isOptionDisabled(option: SegmentedOption) {\n return option.disabled === true || disabledValueSet.value.has(option.value)\n}\n\nfunction isSelectionDisabled(option: SegmentedOption) {\n return props.disabled || isOptionDisabled(option)\n}\n\nfunction handleSelect(option: SegmentedOption) {\n if (isSelectionDisabled(option)) return\n emit('update:modelValue', option.value)\n}\n\n// Roving tabindex over the radio group: only the checked option is tabbable and\n// Left/Right (plus Home/End) move between the selectable options, matching BaseTabs.\nconst optionRefs = new Map<string, HTMLElement>()\n\nfunction optionId(option: SegmentedOption): string {\n return String(option.value)\n}\n\nfunction setOptionRef(id: string, el: Element | ComponentPublicInstance | null) {\n if (el instanceof HTMLElement) {\n optionRefs.set(id, el)\n } else {\n optionRefs.delete(id)\n }\n}\n\nconst selectableOptions = computed(() =>\n normalizedOptions.value.filter(option => !isSelectionDisabled(option)),\n)\n\n/** The one option carrying `tabindex=\"0\"`: the checked one, else the first selectable. */\nconst tabbableId = computed(() => {\n const checked = selectableOptions.value.find(option => option.value === props.modelValue)\n const target = checked ?? selectableOptions.value[0]\n return target ? optionId(target) : ''\n})\n\nfunction isTabbable(option: SegmentedOption): boolean {\n return optionId(option) === tabbableId.value\n}\n\nconst { handleKeydown: handleArrowKeydown } = useRovingFocus({\n ids: () => selectableOptions.value.map(optionId),\n activeId: () => tabbableId.value,\n activate: (id) => {\n const option = selectableOptions.value.find(candidate => optionId(candidate) === id)\n if (option) handleSelect(option)\n },\n getElement: (id) => optionRefs.get(id),\n orientation: 'horizontal',\n})\n\nfunction handleKeydown(event: KeyboardEvent, option: SegmentedOption) {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n handleSelect(option)\n return\n }\n handleArrowKeydown(event)\n}\n</script>\n\n<template>\n <div\n :class=\"[\n 'mint-segmented-control',\n `mint-segmented-control--${variant}`,\n `mint-segmented-control--${size}`,\n fullWidth ? 'mint-segmented-control--full-width' : '',\n disabled ? 'mint-segmented-control--disabled' : '',\n ]\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in normalizedOptions\"\n :key=\"String(option.value)\"\n :ref=\"(el) => setOptionRef(String(option.value), el)\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"modelValue === option.value\"\n :tabindex=\"isTabbable(option) ? 0 : -1\"\n :disabled=\"isSelectionDisabled(option)\"\n :class=\"[\n 'mint-segmented-control__option',\n `mint-segmented-control__option--${variant}`,\n `mint-segmented-control__option--${size}`,\n modelValue === option.value ? 'mint-segmented-control__option--active' : '',\n isOptionDisabled(option) ? 'mint-segmented-control__option--disabled' : '',\n ]\"\n @click=\"handleSelect(option)\"\n @keydown=\"handleKeydown($event, option)\"\n >\n <span class=\"mint-segmented-control__label\"><span\n v-if=\"option.color\"\n class=\"mint-segmented-control__swatch\"\n :style=\"{ background: option.color }\"\n aria-hidden=\"true\"\n />{{ option.label }}</span>\n <span\n v-if=\"option.description && variant === 'card'\"\n class=\"mint-segmented-control__description\"\n >\n {{ option.description }}\n </span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/** Renders a segmented button group for single-option selection, defaulting to the card variant. */\nimport { computed, type ComponentPublicInstance } from 'vue'\nimport type { OptionPrimitive, SelectOption, SelectOptionInput, SegmentedControlVariant, SegmentedControlSize } from '../types'\nimport { normalizeOptionInput } from '../utils/options'\nimport { useRovingFocus } from '../composables/useRovingFocus'\n\ninterface Props {\n modelValue: string | number\n options: SelectOptionInput[]\n variant?: SegmentedControlVariant\n size?: SegmentedControlSize\n fullWidth?: boolean\n disabled?: boolean\n disabledValues?: Array<string | number>\n}\n\ntype SegmentedOption = SelectOption<OptionPrimitive>\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'card',\n size: 'md',\n fullWidth: false,\n disabled: false,\n disabledValues: () => [],\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number]\n}>()\n\nconst normalizedOptions = computed<SegmentedOption[]>(() => props.options.map(normalizeOptionInput<OptionPrimitive, SegmentedOption>))\nconst disabledValueSet = computed(() => new Set(props.disabledValues))\n\nfunction isOptionDisabled(option: SegmentedOption) {\n return option.disabled === true || disabledValueSet.value.has(option.value)\n}\n\nfunction isSelectionDisabled(option: SegmentedOption) {\n return props.disabled || isOptionDisabled(option)\n}\n\nfunction handleSelect(option: SegmentedOption) {\n if (isSelectionDisabled(option)) return\n emit('update:modelValue', option.value)\n}\n\n// Roving tabindex over the radio group: only the checked option is tabbable and\n// Left/Right (plus Home/End) move between the selectable options, matching BaseTabs.\nconst optionRefs = new Map<string, HTMLElement>()\n\nfunction optionId(option: SegmentedOption): string {\n return String(option.value)\n}\n\nfunction setOptionRef(id: string, el: Element | ComponentPublicInstance | null) {\n if (el instanceof HTMLElement) {\n optionRefs.set(id, el)\n } else {\n optionRefs.delete(id)\n }\n}\n\nconst selectableOptions = computed(() =>\n normalizedOptions.value.filter(option => !isSelectionDisabled(option)),\n)\n\n/** The one option carrying `tabindex=\"0\"`: the checked one, else the first selectable. */\nconst tabbableId = computed(() => {\n const checked = selectableOptions.value.find(option => option.value === props.modelValue)\n const target = checked ?? selectableOptions.value[0]\n return target ? optionId(target) : ''\n})\n\nfunction isTabbable(option: SegmentedOption): boolean {\n return optionId(option) === tabbableId.value\n}\n\nconst { handleKeydown: handleArrowKeydown } = useRovingFocus({\n ids: () => selectableOptions.value.map(optionId),\n activeId: () => tabbableId.value,\n activate: (id) => {\n const option = selectableOptions.value.find(candidate => optionId(candidate) === id)\n if (option) handleSelect(option)\n },\n getElement: (id) => optionRefs.get(id),\n orientation: 'horizontal',\n})\n\nfunction handleKeydown(event: KeyboardEvent, option: SegmentedOption) {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n handleSelect(option)\n return\n }\n handleArrowKeydown(event)\n}\n</script>\n\n<template>\n <div\n :class=\"[\n 'mint-segmented-control',\n `mint-segmented-control--${variant}`,\n `mint-segmented-control--${size}`,\n fullWidth ? 'mint-segmented-control--full-width' : '',\n disabled ? 'mint-segmented-control--disabled' : '',\n ]\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in normalizedOptions\"\n :key=\"String(option.value)\"\n :ref=\"(el) => setOptionRef(String(option.value), el)\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"modelValue === option.value\"\n :tabindex=\"isTabbable(option) ? 0 : -1\"\n :disabled=\"isSelectionDisabled(option)\"\n :class=\"[\n 'mint-segmented-control__option',\n `mint-segmented-control__option--${variant}`,\n `mint-segmented-control__option--${size}`,\n modelValue === option.value ? 'mint-segmented-control__option--active' : '',\n isOptionDisabled(option) ? 'mint-segmented-control__option--disabled' : '',\n ]\"\n @click=\"handleSelect(option)\"\n @keydown=\"handleKeydown($event, option)\"\n >\n <span class=\"mint-segmented-control__label\"><span\n v-if=\"option.color\"\n class=\"mint-segmented-control__swatch\"\n :style=\"{ background: option.color }\"\n aria-hidden=\"true\"\n />{{ option.label }}</span>\n <span\n v-if=\"option.description && variant === 'card'\"\n class=\"mint-segmented-control__description\"\n >\n {{ option.description }}\n </span>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { a as BaseButton_default, n as BaseModal_default, o as __exportAll } from "./useDropdownState-Bb2N4m40.js";
2
2
  import { s as colorPalettes, u as useSettingsStore } from "./experiment-DyO54FQB.js";
3
- import { G as BaseInput_default } from "./records-B4yl2oIu.js";
4
- import { o as usePlatform } from "./useListReorder-C_8RYeVP.js";
5
- import { t as SegmentedControl_default } from "./SegmentedControl-hOgBVdju.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";
3
+ import { G as BaseInput_default } from "./records-CR_pYzIP.js";
4
+ import { M as usePlatform } from "./useListReorder-CQ9OCScc.js";
5
+ import { t as SegmentedControl_default } from "./SegmentedControl-CJ7EAmOq.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-6oTT-gPO.js";
7
7
  import { r as useAuthStore } from "./auth-SliLnFre.js";
8
- import { n as isControlModelBinding } from "./controlSchemaModel-CIH5Hqpw.js";
9
8
  import { canAccessByPolicy } from "./permissions.js";
9
+ import { n as isControlModelBinding } from "./controlSchemaModel-DCUOx8ll.js";
10
10
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, vShow, watch, withCtx, withDirectives } from "vue";
11
11
  //#region src/components/internal/SettingsPaneHeadInternal.vue?vue&type=script&setup=true&lang.ts
12
12
  var _hoisted_1$1 = { class: "mint-settings-modal__head" };
@@ -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-CoZK3EDN.js.map
667
+ //# sourceMappingURL=SettingsModal-Dyitgzfp.js.map