@morscherlab/mint-sdk 1.4.0-beta.11 → 1.4.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AlertBox-B2Bgr3ei.js → AlertBox-BdoZqANv.js} +2 -2
- package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-BdoZqANv.js.map} +1 -1
- package/dist/{AppSidebar-mvm--GxN.js → AppSidebar-DGK5i-hJ.js} +7 -7
- package/dist/{AppSidebar-mvm--GxN.js.map → AppSidebar-DGK5i-hJ.js.map} +1 -1
- package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-CbD7BR_s.js} +2 -2
- package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-CbD7BR_s.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-BoofZZxF.js} +4 -4
- package/dist/{ExperimentSelectorModal-65peG4Up.js.map → ExperimentSelectorModal-BoofZZxF.js.map} +1 -1
- package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-BkwYOKpG.js} +2 -2
- package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-BkwYOKpG.js.map} +1 -1
- package/dist/{SettingsModal-DO3IZ7qz.js → SettingsModal-BXsjIVnm.js} +25 -24
- package/dist/SettingsModal-BXsjIVnm.js.map +1 -0
- package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
- package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-BF5g4wRC.js} +2 -2
- package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-BF5g4wRC.js.map} +1 -1
- package/dist/{data-CgILxDLW.js → data-ihsBd9bs.js} +10 -10
- package/dist/{data-CgILxDLW.js.map → data-ihsBd9bs.js.map} +1 -1
- package/dist/data.js +7 -7
- package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
- package/dist/experiment-DyO54FQB.js.map +1 -0
- package/dist/index.js +18 -18
- package/dist/{install-mB8-IyvO.js → install-CFgfRO64.js} +13 -13
- package/dist/{install-mB8-IyvO.js.map → install-CFgfRO64.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-D5bIX426.js → lab-B-pLwc5U.js} +10 -10
- package/dist/{lab-D5bIX426.js.map → lab-B-pLwc5U.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B6gpM_3e.js → records-DnK_1Q_S.js} +2 -2
- package/dist/{records-B6gpM_3e.js.map → records-DnK_1Q_S.js.map} +1 -1
- package/dist/{runtime-D7vKpfbF.js → runtime-pKfF_iql.js} +9 -9
- package/dist/{runtime-D7vKpfbF.js.map → runtime-pKfF_iql.js.map} +1 -1
- package/dist/runtime.js +7 -7
- package/dist/{shell-CywENwBT.js → shell-BIH8Rc21.js} +4 -4
- package/dist/{shell-CywENwBT.js.map → shell-BIH8Rc21.js.map} +1 -1
- package/dist/shell.js +4 -4
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +2 -2
- package/dist/stores/settings.d.ts +4 -12
- package/dist/styles.css +121 -15
- package/dist/types/platform.d.ts +1 -1
- package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-BGN1dmcR.js} +4 -4
- package/dist/{useExperimentSamples-DHnxKFAF.js.map → useExperimentSamples-BGN1dmcR.js.map} +1 -1
- package/dist/{useListReorder-BitdUhxn.js → useListReorder-DPynGrCj.js} +3 -3
- package/dist/{useListReorder-BitdUhxn.js.map → useListReorder-DPynGrCj.js.map} +1 -1
- package/dist/{useRackEditor-Q8xO-cCy.js → useRackEditor-B2FrQA0p.js} +7 -7
- package/dist/{useRackEditor-Q8xO-cCy.js.map → useRackEditor-B2FrQA0p.js.map} +1 -1
- package/dist/{useRealtime-C3iy9QkU.js → useRealtime-yWjTTnc9.js} +2 -2
- package/dist/{useRealtime-C3iy9QkU.js.map → useRealtime-yWjTTnc9.js.map} +1 -1
- package/dist/{useRequest-D506eHi8.js → useRequest-Dqw12k2s.js} +3 -3
- package/dist/{useRequest-D506eHi8.js.map → useRequest-Dqw12k2s.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/stores/settings.test.ts +21 -21
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/components/SettingsModal.vue +7 -12
- package/src/stores/index.ts +0 -2
- package/src/stores/settings.ts +20 -50
- package/src/styles/variables.css +121 -15
- package/src/types/platform.ts +1 -1
- package/dist/SettingsModal-DO3IZ7qz.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as __exportAll, t as useDropdownState } from "./useDropdownState-8yWMu-7_.js";
|
|
2
|
-
import { A as formatRelativeTime } from "./records-
|
|
2
|
+
import { A as formatRelativeTime } from "./records-DnK_1Q_S.js";
|
|
3
3
|
import { t as ConfirmDialog_default } from "./ConfirmDialog-nbJj51is.js";
|
|
4
4
|
import { n as format } from "./experimentStatus-Cqp6qNrR.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-
|
|
337
|
+
//# sourceMappingURL=ExperimentPopover-CbD7BR_s.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExperimentPopover-5T1fAq2L.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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyCA,MAAM,QAAQ;EAiBd,MAAM,eAAe,eAAe,MAAM,eAAe,CAAC,CAAC,MAAM,KAAK;EACtE,MAAM,gBAAgB,eAAe,CAAC,CAAC,MAAM,kBAAkB,CAAC,CAAC,MAAM,cAAc;EACrF,MAAM,UAAU,eAAe,CAAC,aAAa,SAAS,CAAC,cAAc,KAAK;EAC1E,MAAM,UAAU,eAAe,MAAM,YAAY,CAAC,aAAa,SAAS,cAAc,KAAK;EAE3F,MAAM,eAAe,eAAe;GAClC,IAAI,MAAM,aAAa,OAAO;GAC9B,IAAI,MAAM,OAAO,OAAO;GACxB,OAAO,CAAC,MAAM,gBAAgB,MAAM,cAAc,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,KAAK,KAAK,MAAM;EAC3F,CAAC;EAED,MAAM,OAAO;EAMb,MAAM,EAAE,QAAQ,OAAO,WAAW,iBAAiB;GACjD,SAAS,eAA4B,SAAS;GAC9C,YAAY,eAA4B,SAAS;GACjD,eAAe;EACjB,CAAC;EACD,MAAM,cAAc,IAAI,KAAK;EAC7B,MAAM,cAAc,IAAI,KAAK;EAI7B,MAAM,QAAQ,eAA4B,OAAO;EACjD,MAAM,gBAAgB,IAAI,KAAK;EAC/B,MAAM,QAAQ,OAAO,SAAS;GAC5B,IAAI,CAAC,MAAM;GACX,cAAc,QAAQ;GACtB,MAAM,SAAS;GACf,MAAM,QAAQ,MAAM,OAAO,sBAAsB,CAAC,CAAC,SAAS;GAC5D,cAAc,QAAQ,QAAQ,SAAS,gBAAgB;EACzD,CAAC;EAED,MAAM,YAAY,eAAe;GAC/B,IAAI,MAAM,gBAAgB,MAAM,qBAAqB,OAAO,MAAM;GAClE,IAAI,YAAY,SAAS,MAAM,oBAAoB,OAAO,MAAM;GAChE,OAAO,GAAG,MAAM,UAAU;EAC5B,CAAC;EAID,SAAS,WAA+B;GACtC,OAAQ;IACN,CAAC,WAAW,MAAM,iBAAiB;IACnC,CAAC,UAAU,MAAM,oBAAoB,OAAa,MAAM,gBAAgB,CAAC;IACzE,CAAC,SAAS,MAAM,eAAe;IAC/B,CAAC,WAAW,MAAM,uBAAuB,mBAAmB,MAAM,mBAAmB,CAAC;GACxF,CAAC,CAAoC,QAAQ,QAAiC,CAAC,CAAC,IAAI,EAAE;EACxF;EAEA,SAAS,qBAAqB;GAC5B,IAAI,QAAQ,OAAO;IACjB,KAAK,QAAQ;IACb;GACF;GACA,OAAO;EACT;EAEA,SAAS,eAAe;GACtB,KAAK,QAAQ;GACb,MAAM;EACR;EAEA,SAAS,aAAa;GACpB,IAAI,MAAM,gBAAgB,MAAM,aAAa;GAC7C,MAAM;GACN,IAAI,MAAM,aACR,YAAY,QAAQ;QAEpB,KAAK,MAAM;EAEf;EAEA,SAAS,oBAAoB;GAC3B,YAAY,QAAQ;GACpB,KAAK,MAAM;EACb;EAEA,SAAS,eAAe;GACtB,KAAK,QAAQ;GACb,MAAM;EACR;EAEA,IAAI,eAAqD;EAGzD,YAAY,MAAM,qBAAqB,QAAQ;GAC7C,IAAI,cAAc,aAAa,YAAY;GAC3C,IAAI,KAAK;IACP,YAAY,QAAQ;IACpB,eAAe,iBAAiB;KAC9B,YAAY,QAAQ;KACpB,eAAe;IACjB,GAAG,GAAI;GACT;EACF,CAAC;EAED,kBAAkB;GAChB,IAAI,cAAc,aAAa,YAAY;EAC7C,CAAC;;GAIC,OAAA,UAAA,GAAA,mBAsIM,OAAA;IAtID,KAAI;IAAW,OAAK,eAAA,CAAA,2BAAA,EAAA,iCAAiE,QAAA,MAAO,CAAA,CAAA;;IAE/F,mBAoCS,UAAA;KAnCP,KAAI;KACJ,MAAK;KACJ,OAAK,eAAA;;MAAsG,EAAA,4CAAA,MAAA,MAAA,EAAM;MAAyD,EAAA,2CAAA,QAAA,MAAO;MAA2D,EAAA,6CAAA,QAAA,YAAW;MAA0D,EAAA,2CAAA,CAAA,QAAA,eAAW,CAAA,CAAM,QAAA,MAAK;;KAOvU,OAAO,aAAA;KACP,cAAY,aAAA,QAAe,aAAA,QAAe,KAAA;KAC1C,aAAW,QAAA,eAAe,KAAA;KAC1B,iBAAe,QAAA,QAAU,KAAA,IAAY,MAAA,MAAA;KACrC,SAAK,cAAO,oBAAkB,CAAA,MAAA,CAAA;IAInB,GAAA,CAAA,QAAA,eAAZ,UAAA,GAAA,mBAAgG,QAAhG,UAAgG,KAC3E,QAAA,SAArB,UAAA,GAAA,mBAGW,UAAA,EAAA,KAAA,EAAA,GAAA,CAFT,YAA2F,MAAA,WAAA,GAAA;KAA9E,OAAM;KAAyC,MAAM;KAAI,eAAY;IAClF,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,mBAAiF,QAAA,EAA3E,OAAM,wCAAuC,GAAC,0BAAsB,EAAA,EAAA,GAAA,EAAA,KAEvD,cAAA,SAArB,UAAA,GAAA,mBAOW,UAAA,EAAA,KAAA,EAAA,GAAA,CANG,QAAA,kBAAZ,UAAA,GAAA,mBAA+F,QAA/F,YAA+F,gBAAxB,QAAA,cAAc,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,GAE7E,QAAA,kBADR,UAAA,GAAA,mBAI4B,QAAA;;KAF1B,OAAM;KACL,OAAK,eAAA,EAAA,UAAA,GAAiB,QAAA,aAAY,IAAA,CAAA;IACjC,GAAA,gBAAA,QAAA,cAAc,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,GAAA,EAAA,MAEpB,UAAA,GAAA,mBAGW,UAAA,EAAA,KAAA,EAAA,GAAA,CAFT,YAAoF,MAAA,IAAA,GAAA;KAA9E,OAAM;KAAyC,MAAM;KAAI,eAAY;IAC3E,CAAA,GAAA,mBAA4E,QAA5E,YAA4E,gBAArB,QAAA,WAAW,GAAA,CAAA,CAAA,GAAA,EAAA,IAEhD,CAAA,QAAA,SAApB,UAAA,GAAA,YAA8G,MAAA,WAAA,GAAA;;KAAjF,OAAM;KAA4C,MAAM;KAAI,eAAY;;IAK/F,QAAA,SADR,UAAA,GAAA,mBAmBS,UAAA;;KAjBP,MAAK;KACJ,OAAK,eAAA;;MAAiG,EAAA,0CAAA,QAAA,YAAW;MAAwD,EAAA,0CAAA,YAAA,MAAW;MAAyD,EAAA,2CAAA,QAAA,gBAAY,CAAK,YAAA,MAAW;;KAMzQ,UAAU,QAAA,eAAgB,QAAA,gBAAY,CAAK,YAAA;KAC3C,OAAO,UAAA;KACP,cAAY,QAAA,QAAK,GAAM,UAAA,MAAS,sBAAuB,UAAA;KACvD,SAAK,cAAO,YAAU,CAAA,MAAA,CAAA;;KAEH,QAAA,eAApB,UAAA,GAAA,YAA+G,MAAA,YAAA,GAAA;;MAA9E,OAAM;MAAyC,MAAM;MAAI,eAAY;KACpF,CAAA,KAAA,YAAA,SAAlB,UAAA,GAAA,YAA+D,MAAA,KAAA,GAAA;;MAA/B,MAAM;MAAI,eAAY;KACtD,CAAA,MAAA,UAAA,GAAA,YAAoD,MAAA,WAAA,GAAA;;MAA/B,MAAM;MAAI,eAAY;;KAC3C,mBAAsD,QAAA,MAAA,gBAA7C,QAAA,cAAW,YAAe,QAAA,SAAS,GAAA,CAAA;KAChC,QAAA,SAAK,CAAK,QAAA,eAAtB,UAAA,GAAA,mBAA+F,QAA/F,UAA+F,KAAA,mBAAA,IAAA,IAAA;;IAKzF,MAAA,MAAA,KADR,UAAA,GAAA,mBA2DM,OAAA;;KAzDA,SAAA;KAAJ,KAAI;KACH,OAAK,eAAA,CAAA,kCAAA,EAAA,uCAA8E,cAAA,MAAa,CAAA,CAAA;;KAGtF,QAAA,eAAX,UAAA,GAAA,mBAGM,OAHN,YAGM,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAFJ,mBAAoG,QAAA,EAA9F,OAAM,sFAAqF,GAAA,MAAA,EAAA,GACjG,mBAAoG,QAAA,EAA9F,OAAM,sFAAqF,GAAA,MAAA,EAAA,CAAA,EAAA,CAAA,KAInF,QAAA,SAAhB,UAAA,GAAA,mBAMM,OANN,YAMM,CALJ,mBAGI,KAHJ,YAGI,CAFF,YAA6C,MAAA,WAAA,GAAA;MAA/B,MAAM;MAAI,eAAY;KAAS,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,4BAE/C,EAAA,EAAA,CAAA,GACA,mBAAiE,QAAjE,aAAiE,gBAAf,QAAA,KAAK,GAAA,CAAA,CAAA,CAAA,MAGzD,UAAA,GAAA,mBAWW,UAAA,EAAA,KAAA,EAAA,GAAA,CAVT,mBAGM,OAHN,aAGM,CAFQ,QAAA,kBAAZ,UAAA,GAAA,mBAA+F,QAA/F,aAA+F,gBAAxB,QAAA,cAAc,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,GAC5E,QAAA,kBAAT,UAAA,GAAA,mBAAuF,KAAvF,aAAuF,gBAArB,QAAA,cAAc,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,GAExE,SAAQ,CAAA,CAAG,UAArB,UAAA,GAAA,mBAKK,MALL,aAKK,EAJH,UAAA,IAAA,GAAA,mBAGW,UAAA,MAAA,WAHwB,SAAQ,IAAA,CAAzB,OAAO,WAAK;MAAuB,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,MAAK,GAAA,CACxD,mBAAoB,MAAA,MAAA,gBAAb,KAAK,GAAA,CAAA,GACZ,mBAAoB,MAAA,MAAA,gBAAb,KAAK,GAAA,CAAA,CAAA,GAAA,EAAA;;KAKlB,OAAA,OAAA,OAAA,KAAA,mBAAmE,OAAA;MAA9D,OAAM;MAAqC,MAAK;;KAErD,mBAGS,UAAA;MAHD,MAAK;MAAS,OAAM;MAAmC,SAAO;KACpE,GAAA,CAAA,YAA6F,MAAA,cAAA,GAAA;MAA7E,OAAM;MAAwC,MAAM;MAAI,eAAY;KAAS,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,wBAE/F,EAAA,EAAA,CAAA;KAEQ,QAAA,gBAAY,QADpB,UAAA,GAAA,mBAUI,KAAA;;MARF,OAAM;MACL,MAAI,gBAAkB,QAAA;MACvB,QAAO;MACP,KAAI;MACH,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,MAAA,KAAA,CAAK,CAAA;KAEb,GAAA,CAAA,YAA2F,MAAA,YAAA,GAAA;MAA7E,OAAM;MAAwC,MAAM;MAAI,eAAY;KAAS,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,kBAE7F,EAAA,EAAA,GAAA,GAAA,WAAA,KAAA,mBAAA,IAAA,IAAA;KAEQ,QAAA,cADR,UAAA,GAAA,mBAQS,UAAA;;MANP,MAAK;MACL,OAAM;MACL,SAAO;KAER,GAAA,CAAA,YAAqF,MAAA,MAAA,GAAA;MAA7E,OAAM;MAAwC,MAAM;MAAI,eAAY;KAAS,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,YAEvF,EAAA,EAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;;IAIF,YAQE,uBAAA;KAPQ,MAAM,YAAA;KAAA,iBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,YAAW,QAAA;KACxB,OAAO,QAAA,gBAAY;KACnB,SAAS,QAAA,kBAAc,wBAA4B,QAAA,kBAAkB,QAAA,eAAc;KACpF,MAAK;KACJ,iBAAe,QAAA;KACf,SAAS,QAAA;KACT,WAAS"}
|
|
1
|
+
{"version":3,"file":"ExperimentPopover-CbD7BR_s.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-8yWMu-7_.js";
|
|
2
|
-
import { o as useApi, t as useExperimentStore } from "./experiment-
|
|
3
|
-
import { C as formatExperimentDate, n as ResourcePickerListInternal_default, x as datePresetToISO } from "./records-
|
|
4
|
-
import { n as ActionMenu_default, t as useRequest } from "./useRequest-
|
|
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-DnK_1Q_S.js";
|
|
4
|
+
import { n as ActionMenu_default, t as useRequest } from "./useRequest-Dqw12k2s.js";
|
|
5
5
|
import { t as useDebouncedWatch } from "./useDebouncedWatch-BPwyl6eS.js";
|
|
6
6
|
import { n as format, r as tone } from "./experimentStatus-Cqp6qNrR.js";
|
|
7
7
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, reactive, ref, renderList, resolveDynamicComponent, toDisplayString, unref, watch, withCtx } from "vue";
|
|
@@ -541,4 +541,4 @@ var ExperimentSelectorModal_default = ExperimentSelectorModal_vue_vue_type_scrip
|
|
|
541
541
|
//#endregion
|
|
542
542
|
export { ExperimentSelectorModal_exports as n, useExperimentSelector as r, ExperimentSelectorModal_default as t };
|
|
543
543
|
|
|
544
|
-
//# sourceMappingURL=ExperimentSelectorModal-
|
|
544
|
+
//# sourceMappingURL=ExperimentSelectorModal-BoofZZxF.js.map
|
package/dist/{ExperimentSelectorModal-65peG4Up.js.map → ExperimentSelectorModal-BoofZZxF.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExperimentSelectorModal-65peG4Up.js","names":[],"sources":["../src/composables/useExperimentSelector.ts","../src/components/ExperimentSelectorModal.vue","../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequest } from './useRequest'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */\n withStatusCounts?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n /** Experiment count per status under every filter except status itself. */\n statusCounts: Ref<Record<string, number>>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n /** Fetch `statusCounts` from `GET /experiments/stats`. */\n fetchStatusCounts: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const {\n limit = 100,\n immediate = false,\n experimentType,\n allowedExperimentTypes: optionAllowedExperimentTypes,\n apiBaseUrl,\n withStatusCounts = false,\n } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequest('Failed to fetch experiments')\n const isLoading = request.isLoading\n const error = request.error\n const lastLoadedAt = ref<Date | null>(null)\n const page = ref(0)\n let fetchGeneration = 0\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const statusCounts = ref<Record<string, number>>({})\n let statsGeneration = 0\n let countedScope: string | null = null\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n mine: undefined,\n })\n\n function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {\n if (values == null) return null\n return [...new Set(values.filter((value): value is string => typeof value === 'string' && value.length > 0))]\n }\n\n function currentAllowedExperimentTypes(): string[] | null {\n if (optionAllowedExperimentTypes !== undefined) {\n return normalizeExperimentTypes(optionAllowedExperimentTypes)\n }\n return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes)\n }\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n type ExperimentTypeScope =\n | { kind: 'all' }\n | { kind: 'blocked' }\n | { kind: 'single'; experimentType: string }\n | { kind: 'multi'; experimentTypes: string[] }\n\n function resolveExperimentTypeScope(): ExperimentTypeScope {\n const allowedTypes = currentAllowedExperimentTypes()\n const selectedType = experimentType ?? filters.experimentType ?? undefined\n\n if (selectedType) {\n if (allowedTypes && !allowedTypes.includes(selectedType)) {\n return { kind: 'blocked' }\n }\n return { kind: 'single', experimentType: selectedType }\n }\n\n if (allowedTypes) {\n if (allowedTypes.length === 0) return { kind: 'blocked' }\n if (allowedTypes.length === 1) return { kind: 'single', experimentType: allowedTypes[0] }\n return { kind: 'multi', experimentTypes: allowedTypes }\n }\n\n return { kind: 'all' }\n }\n\n /** Query params shared by the list and the stats route; status, sort and paging are list-only. */\n function scopeParams(typeFilter: string | undefined): URLSearchParams {\n const params = new URLSearchParams()\n if (typeFilter) params.set('experiment_type', typeFilter)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n if (filters.mine) params.set('mine', 'true')\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n return params\n }\n\n function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {\n const params = scopeParams(typeFilter)\n if (filters.status) params.set('status', filters.status)\n\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n params.set('limit', String(pageLimit))\n params.set('skip', String(skip))\n\n const query = params.toString()\n const base = platformBase ?? ''\n return `${base}/experiments${query ? `?${query}` : ''}`\n }\n\n function sortExperiments(items: ExperimentSummary[]): ExperimentSummary[] {\n const { sortBy, sortOrder } = parseSortKey()\n const direction = sortOrder === 'desc' ? -1 : 1\n return [...items].sort((a, b) => {\n const aValue = a[sortBy] ?? ''\n const bValue = b[sortBy] ?? ''\n if (aValue < bValue) return -1 * direction\n if (aValue > bValue) return 1 * direction\n return 0\n })\n }\n\n async function fetchExperimentList(typeFilter: string | undefined, skip: number, pageLimit: number) {\n return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))\n }\n\n async function fetchScopedExperimentPage(\n requestedPage: number,\n ): Promise<ExperimentListResponse> {\n const scope = resolveExperimentTypeScope()\n const pageSkip = requestedPage * limit\n\n if (scope.kind === 'blocked') {\n return { experiments: [], total: 0 }\n }\n\n if (scope.kind === 'multi') {\n const perTypeLimit = pageSkip + limit\n const responses = await Promise.all(\n scope.experimentTypes.map(type => fetchExperimentList(type, 0, perTypeLimit)),\n )\n const merged = sortExperiments(responses.flatMap(response => response.experiments))\n return {\n experiments: merged.slice(pageSkip, pageSkip + limit),\n total: responses.reduce((sum, response) => sum + response.total, 0),\n }\n }\n\n return await fetchExperimentList(\n scope.kind === 'single' ? scope.experimentType : undefined,\n pageSkip,\n limit,\n )\n }\n\n async function fetchExperiments(): Promise<void> {\n const generation = ++fetchGeneration\n const requestedPage = page.value\n isLoading.value = true\n request.clearError()\n try {\n const data = await fetchScopedExperimentPage(requestedPage)\n if (generation !== fetchGeneration) return\n if (requestedPage === 0) {\n experiments.value = data.experiments\n } else {\n experiments.value = [...experiments.value, ...data.experiments]\n }\n total.value = data.total\n lastLoadedAt.value = new Date()\n } catch (err) {\n if (generation !== fetchGeneration) return\n request.setError(err, 'Failed to fetch experiments')\n if (requestedPage === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n if (generation === fetchGeneration) {\n isLoading.value = false\n }\n }\n }\n\n function invalidateFetch(): void {\n fetchGeneration += 1\n isLoading.value = false\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n const allowedTypes = currentAllowedExperimentTypes()\n const allowedSet = allowedTypes ? new Set(allowedTypes) : null\n experimentTypes.value = typesRes.value\n .filter(t => !allowedSet || allowedSet.has(t.value))\n .map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) {\n filters.experimentType = undefined\n }\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n /** The filters the stats route reads; status, sort and paging do not change the counts. */\n function countScope(): string {\n return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])\n }\n\n function refreshStatusCountsOnScopeChange(): void {\n if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()\n }\n\n async function fetchStatusCounts(): Promise<void> {\n const generation = ++statsGeneration\n countedScope = countScope()\n const scope = resolveExperimentTypeScope()\n if (scope.kind === 'blocked') {\n statusCounts.value = {}\n return\n }\n const types = scope.kind === 'multi'\n ? scope.experimentTypes\n : [scope.kind === 'single' ? scope.experimentType : undefined]\n const base = platformBase ?? ''\n try {\n const responses = await Promise.all(\n types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(\n `${base}/experiments/stats?${scopeParams(type)}`,\n )),\n )\n if (generation !== statsGeneration) return\n const counts: Record<string, number> = {}\n for (const response of responses) {\n for (const [status, count] of Object.entries(response.by_status)) {\n counts[status] = (counts[status] ?? 0) + count\n }\n }\n statusCounts.value = counts\n } catch {\n // Counts only decorate the status menu; without them it shows bare labels.\n // Forget the scope so the next status or sort change retries the request.\n if (generation === statsGeneration) {\n statusCounts.value = {}\n countedScope = null\n }\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n invalidateFetch()\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n filters.mine = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n watch(\n () => [\n filters.search,\n filters.status,\n filters.project,\n filters.experimentType,\n filters.datePreset,\n filters.mine,\n sortKey.value,\n ],\n () => invalidateFetch(),\n { flush: 'sync' },\n )\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n refreshStatusCountsOnScopeChange()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n // Also checked on a status or sort change: the cancelled debounce may hold a new search.\n refreshStatusCountsOnScopeChange()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n fetchStatusCounts,\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ExperimentSelectorModal-BoofZZxF.js","names":[],"sources":["../src/composables/useExperimentSelector.ts","../src/components/ExperimentSelectorModal.vue","../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequest } from './useRequest'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */\n withStatusCounts?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n /** Experiment count per status under every filter except status itself. */\n statusCounts: Ref<Record<string, number>>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n /** Fetch `statusCounts` from `GET /experiments/stats`. */\n fetchStatusCounts: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const {\n limit = 100,\n immediate = false,\n experimentType,\n allowedExperimentTypes: optionAllowedExperimentTypes,\n apiBaseUrl,\n withStatusCounts = false,\n } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequest('Failed to fetch experiments')\n const isLoading = request.isLoading\n const error = request.error\n const lastLoadedAt = ref<Date | null>(null)\n const page = ref(0)\n let fetchGeneration = 0\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const statusCounts = ref<Record<string, number>>({})\n let statsGeneration = 0\n let countedScope: string | null = null\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n mine: undefined,\n })\n\n function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {\n if (values == null) return null\n return [...new Set(values.filter((value): value is string => typeof value === 'string' && value.length > 0))]\n }\n\n function currentAllowedExperimentTypes(): string[] | null {\n if (optionAllowedExperimentTypes !== undefined) {\n return normalizeExperimentTypes(optionAllowedExperimentTypes)\n }\n return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes)\n }\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n type ExperimentTypeScope =\n | { kind: 'all' }\n | { kind: 'blocked' }\n | { kind: 'single'; experimentType: string }\n | { kind: 'multi'; experimentTypes: string[] }\n\n function resolveExperimentTypeScope(): ExperimentTypeScope {\n const allowedTypes = currentAllowedExperimentTypes()\n const selectedType = experimentType ?? filters.experimentType ?? undefined\n\n if (selectedType) {\n if (allowedTypes && !allowedTypes.includes(selectedType)) {\n return { kind: 'blocked' }\n }\n return { kind: 'single', experimentType: selectedType }\n }\n\n if (allowedTypes) {\n if (allowedTypes.length === 0) return { kind: 'blocked' }\n if (allowedTypes.length === 1) return { kind: 'single', experimentType: allowedTypes[0] }\n return { kind: 'multi', experimentTypes: allowedTypes }\n }\n\n return { kind: 'all' }\n }\n\n /** Query params shared by the list and the stats route; status, sort and paging are list-only. */\n function scopeParams(typeFilter: string | undefined): URLSearchParams {\n const params = new URLSearchParams()\n if (typeFilter) params.set('experiment_type', typeFilter)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n if (filters.mine) params.set('mine', 'true')\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n return params\n }\n\n function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {\n const params = scopeParams(typeFilter)\n if (filters.status) params.set('status', filters.status)\n\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n params.set('limit', String(pageLimit))\n params.set('skip', String(skip))\n\n const query = params.toString()\n const base = platformBase ?? ''\n return `${base}/experiments${query ? `?${query}` : ''}`\n }\n\n function sortExperiments(items: ExperimentSummary[]): ExperimentSummary[] {\n const { sortBy, sortOrder } = parseSortKey()\n const direction = sortOrder === 'desc' ? -1 : 1\n return [...items].sort((a, b) => {\n const aValue = a[sortBy] ?? ''\n const bValue = b[sortBy] ?? ''\n if (aValue < bValue) return -1 * direction\n if (aValue > bValue) return 1 * direction\n return 0\n })\n }\n\n async function fetchExperimentList(typeFilter: string | undefined, skip: number, pageLimit: number) {\n return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))\n }\n\n async function fetchScopedExperimentPage(\n requestedPage: number,\n ): Promise<ExperimentListResponse> {\n const scope = resolveExperimentTypeScope()\n const pageSkip = requestedPage * limit\n\n if (scope.kind === 'blocked') {\n return { experiments: [], total: 0 }\n }\n\n if (scope.kind === 'multi') {\n const perTypeLimit = pageSkip + limit\n const responses = await Promise.all(\n scope.experimentTypes.map(type => fetchExperimentList(type, 0, perTypeLimit)),\n )\n const merged = sortExperiments(responses.flatMap(response => response.experiments))\n return {\n experiments: merged.slice(pageSkip, pageSkip + limit),\n total: responses.reduce((sum, response) => sum + response.total, 0),\n }\n }\n\n return await fetchExperimentList(\n scope.kind === 'single' ? scope.experimentType : undefined,\n pageSkip,\n limit,\n )\n }\n\n async function fetchExperiments(): Promise<void> {\n const generation = ++fetchGeneration\n const requestedPage = page.value\n isLoading.value = true\n request.clearError()\n try {\n const data = await fetchScopedExperimentPage(requestedPage)\n if (generation !== fetchGeneration) return\n if (requestedPage === 0) {\n experiments.value = data.experiments\n } else {\n experiments.value = [...experiments.value, ...data.experiments]\n }\n total.value = data.total\n lastLoadedAt.value = new Date()\n } catch (err) {\n if (generation !== fetchGeneration) return\n request.setError(err, 'Failed to fetch experiments')\n if (requestedPage === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n if (generation === fetchGeneration) {\n isLoading.value = false\n }\n }\n }\n\n function invalidateFetch(): void {\n fetchGeneration += 1\n isLoading.value = false\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n const allowedTypes = currentAllowedExperimentTypes()\n const allowedSet = allowedTypes ? new Set(allowedTypes) : null\n experimentTypes.value = typesRes.value\n .filter(t => !allowedSet || allowedSet.has(t.value))\n .map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) {\n filters.experimentType = undefined\n }\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n /** The filters the stats route reads; status, sort and paging do not change the counts. */\n function countScope(): string {\n return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])\n }\n\n function refreshStatusCountsOnScopeChange(): void {\n if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()\n }\n\n async function fetchStatusCounts(): Promise<void> {\n const generation = ++statsGeneration\n countedScope = countScope()\n const scope = resolveExperimentTypeScope()\n if (scope.kind === 'blocked') {\n statusCounts.value = {}\n return\n }\n const types = scope.kind === 'multi'\n ? scope.experimentTypes\n : [scope.kind === 'single' ? scope.experimentType : undefined]\n const base = platformBase ?? ''\n try {\n const responses = await Promise.all(\n types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(\n `${base}/experiments/stats?${scopeParams(type)}`,\n )),\n )\n if (generation !== statsGeneration) return\n const counts: Record<string, number> = {}\n for (const response of responses) {\n for (const [status, count] of Object.entries(response.by_status)) {\n counts[status] = (counts[status] ?? 0) + count\n }\n }\n statusCounts.value = counts\n } catch {\n // Counts only decorate the status menu; without them it shows bare labels.\n // Forget the scope so the next status or sort change retries the request.\n if (generation === statsGeneration) {\n statusCounts.value = {}\n countedScope = null\n }\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n invalidateFetch()\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n filters.mine = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n watch(\n () => [\n filters.search,\n filters.status,\n filters.project,\n filters.experimentType,\n filters.datePreset,\n filters.mine,\n sortKey.value,\n ],\n () => invalidateFetch(),\n { flush: 'sync' },\n )\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n refreshStatusCountsOnScopeChange()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n // Also checked on a status or sort change: the cancelled debounce may hold a new search.\n refreshStatusCountsOnScopeChange()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n fetchStatusCounts,\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as useRovingFocus, W as normalizeOptionInput } from "./records-
|
|
1
|
+
import { R as useRovingFocus, W as normalizeOptionInput } from "./records-DnK_1Q_S.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-
|
|
129
|
+
//# sourceMappingURL=SegmentedControl-BkwYOKpG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentedControl-BtnnjWzs.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-BkwYOKpG.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,11 +1,11 @@
|
|
|
1
1
|
import { a as BaseButton_default, n as BaseModal_default, o as __exportAll } from "./useDropdownState-8yWMu-7_.js";
|
|
2
|
-
import {
|
|
3
|
-
import { G as BaseInput_default } from "./records-
|
|
4
|
-
import { o as usePlatform } from "./useListReorder-
|
|
5
|
-
import { t as SegmentedControl_default } from "./SegmentedControl-
|
|
6
|
-
import { a as pickExistingRecordKeys, c as valuesEqual, h as normalizeItemInput, i as formSchemaFieldNames, k as controlsToSettingsSchema, l as useFormBuilder, o as pickRecordKeys, r as FormFieldRendererInternal_default, s as recordValuesEqualForKeys } from "./AppSidebar-
|
|
2
|
+
import { s as colorPalettes, u as useSettingsStore } from "./experiment-DyO54FQB.js";
|
|
3
|
+
import { G as BaseInput_default } from "./records-DnK_1Q_S.js";
|
|
4
|
+
import { o as usePlatform } from "./useListReorder-DPynGrCj.js";
|
|
5
|
+
import { t as SegmentedControl_default } from "./SegmentedControl-BkwYOKpG.js";
|
|
6
|
+
import { a as pickExistingRecordKeys, c as valuesEqual, h as normalizeItemInput, i as formSchemaFieldNames, k as controlsToSettingsSchema, l as useFormBuilder, o as pickRecordKeys, r as FormFieldRendererInternal_default, s as recordValuesEqualForKeys } from "./AppSidebar-DGK5i-hJ.js";
|
|
7
7
|
import { r as useAuthStore } from "./auth-SliLnFre.js";
|
|
8
|
-
import { n as isControlModelBinding } from "./controlSchemaModel-
|
|
8
|
+
import { n as isControlModelBinding } from "./controlSchemaModel-BF5g4wRC.js";
|
|
9
9
|
import { canAccessByPolicy } from "./permissions.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
|
|
@@ -132,10 +132,7 @@ var _hoisted_14 = { class: "mint-settings-modal__theme-label" };
|
|
|
132
132
|
var _hoisted_15 = { class: "mint-settings-modal__setting" };
|
|
133
133
|
var _hoisted_16 = { class: "mint-settings-modal__palettes" };
|
|
134
134
|
var _hoisted_17 = ["aria-pressed", "onClick"];
|
|
135
|
-
var _hoisted_18 =
|
|
136
|
-
class: "mint-settings-modal__swatches",
|
|
137
|
-
"aria-hidden": "true"
|
|
138
|
-
};
|
|
135
|
+
var _hoisted_18 = ["data-palette"];
|
|
139
136
|
var _hoisted_19 = { class: "mint-settings-modal__palette-name" };
|
|
140
137
|
var _hoisted_20 = { class: "mint-settings-modal__setting mint-settings-modal__setting--inline" };
|
|
141
138
|
var _hoisted_21 = {
|
|
@@ -375,13 +372,13 @@ var SettingsModal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
|
|
|
375
372
|
halves: ["light", "dark"]
|
|
376
373
|
}
|
|
377
374
|
];
|
|
378
|
-
/**
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
375
|
+
/** Accent tokens a palette swatch shows; `data-palette` on the strip resolves them per palette. */
|
|
376
|
+
const paletteSwatchTokens = [
|
|
377
|
+
"--color-primary",
|
|
378
|
+
"--color-primary-hover",
|
|
379
|
+
"--color-primary-soft",
|
|
380
|
+
"--color-primary-border"
|
|
381
|
+
];
|
|
385
382
|
const densityPreviewRows = [
|
|
386
383
|
[
|
|
387
384
|
"S-0012",
|
|
@@ -570,7 +567,7 @@ var SettingsModal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
|
|
|
570
567
|
"aria-hidden": "true"
|
|
571
568
|
}, null, -1)), createTextVNode(" " + toDisplayString(opt.label), 1)])], 10, _hoisted_12);
|
|
572
569
|
}), 64))])]),
|
|
573
|
-
createElementVNode("div", _hoisted_15, [_cache[10] || (_cache[10] = createElementVNode("div", { class: "mint-settings-modal__setting-text" }, [createElementVNode("span", { class: "mint-settings-modal__setting-label" }, "Color palette"), createElementVNode("span", { class: "mint-settings-modal__setting-hint" }, "
|
|
570
|
+
createElementVNode("div", _hoisted_15, [_cache[10] || (_cache[10] = createElementVNode("div", { class: "mint-settings-modal__setting-text" }, [createElementVNode("span", { class: "mint-settings-modal__setting-label" }, "Color palette"), createElementVNode("span", { class: "mint-settings-modal__setting-hint" }, "Accent colour across the interface")], -1)), createElementVNode("div", _hoisted_16, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(colorPalettes), (palette, key) => {
|
|
574
571
|
return openBlock(), createElementBlock("button", {
|
|
575
572
|
key,
|
|
576
573
|
type: "button",
|
|
@@ -578,13 +575,17 @@ var SettingsModal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ de
|
|
|
578
575
|
class: normalizeClass(["mint-settings-modal__palette", { "mint-settings-modal__palette--active": unref(settings).colorPalette === key }]),
|
|
579
576
|
onClick: ($event) => unref(settings).colorPalette = key
|
|
580
577
|
}, [
|
|
581
|
-
createElementVNode("span",
|
|
582
|
-
|
|
583
|
-
|
|
578
|
+
createElementVNode("span", {
|
|
579
|
+
class: "mint-settings-modal__swatches",
|
|
580
|
+
"data-palette": key,
|
|
581
|
+
"aria-hidden": "true"
|
|
582
|
+
}, [(openBlock(), createElementBlock(Fragment, null, renderList(paletteSwatchTokens, (token) => {
|
|
583
|
+
return createElementVNode("span", {
|
|
584
|
+
key: token,
|
|
584
585
|
class: "mint-settings-modal__swatch",
|
|
585
|
-
style: normalizeStyle({ backgroundColor:
|
|
586
|
+
style: normalizeStyle({ backgroundColor: `var(${token})` })
|
|
586
587
|
}, null, 4);
|
|
587
|
-
}),
|
|
588
|
+
}), 64))], 8, _hoisted_18),
|
|
588
589
|
createElementVNode("span", _hoisted_19, toDisplayString(palette.name), 1),
|
|
589
590
|
_cache[9] || (_cache[9] = createElementVNode("svg", {
|
|
590
591
|
class: "mint-settings-modal__palette-check",
|
|
@@ -663,4 +664,4 @@ var SettingsModal_default = SettingsModal_vue_vue_type_script_setup_true_lang_de
|
|
|
663
664
|
//#endregion
|
|
664
665
|
export { SettingsModal_exports as n, SettingsModal_default as t };
|
|
665
666
|
|
|
666
|
-
//# sourceMappingURL=SettingsModal-
|
|
667
|
+
//# sourceMappingURL=SettingsModal-BXsjIVnm.js.map
|