@morscherlab/mint-sdk 1.4.0-beta.13 → 1.4.0-beta.14
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-BdoZqANv.js → AlertBox-SnsbVow-.js} +3 -3
- package/dist/{AlertBox-BdoZqANv.js.map → AlertBox-SnsbVow-.js.map} +1 -1
- package/dist/{AppSidebar-CIXJBzDv.js → AppSidebar-Bi8g9mRt.js} +7 -7
- package/dist/{AppSidebar-CIXJBzDv.js.map → AppSidebar-Bi8g9mRt.js.map} +1 -1
- package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CSls2Nuv.js} +2 -2
- package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CSls2Nuv.js.map} +1 -1
- package/dist/{ExperimentPopover-CbD7BR_s.js → ExperimentPopover-ChKCrklH.js} +5 -5
- package/dist/{ExperimentPopover-CbD7BR_s.js.map → ExperimentPopover-ChKCrklH.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BbFtr1Fr.js → ExperimentSelectorModal-BqifjNaN.js} +5 -5
- package/dist/{ExperimentSelectorModal-BbFtr1Fr.js.map → ExperimentSelectorModal-BqifjNaN.js.map} +1 -1
- package/dist/{SegmentedControl-BkwYOKpG.js → SegmentedControl-hOgBVdju.js} +2 -2
- package/dist/{SegmentedControl-BkwYOKpG.js.map → SegmentedControl-hOgBVdju.js.map} +1 -1
- package/dist/{SettingsModal-CdhFMi8j.js → SettingsModal-jra_2u0o.js} +7 -7
- package/dist/{SettingsModal-CdhFMi8j.js.map → SettingsModal-jra_2u0o.js.map} +1 -1
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/{controlSchemaModel-BF5g4wRC.js → controlSchemaModel-CIH5Hqpw.js} +2 -2
- package/dist/{controlSchemaModel-BF5g4wRC.js.map → controlSchemaModel-CIH5Hqpw.js.map} +1 -1
- package/dist/{data-DUt0TIp5.js → data-OZ94DhVC.js} +11 -11
- package/dist/{data-DUt0TIp5.js.map → data-OZ94DhVC.js.map} +1 -1
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-BFrKpHAH.js} +2 -2
- package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-BFrKpHAH.js.map} +1 -1
- package/dist/index.js +18 -18
- package/dist/{install-Czoe1rA8.js → install-Bx_6KwJA.js} +15 -15
- package/dist/{install-Czoe1rA8.js.map → install-Bx_6KwJA.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-B-pLwc5U.js → lab-ZDKlx1Oo.js} +10 -10
- package/dist/{lab-B-pLwc5U.js.map → lab-ZDKlx1Oo.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-DnK_1Q_S.js → records-B4yl2oIu.js} +2 -2
- package/dist/{records-DnK_1Q_S.js.map → records-B4yl2oIu.js.map} +1 -1
- package/dist/{runtime-fPb_FEZr.js → runtime-BJvGms6h.js} +9 -9
- package/dist/{runtime-fPb_FEZr.js.map → runtime-BJvGms6h.js.map} +1 -1
- package/dist/runtime.js +6 -6
- package/dist/{shell-lDf_EY-x.js → shell-Czyrzk7l.js} +5 -5
- package/dist/{shell-lDf_EY-x.js.map → shell-Czyrzk7l.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +8 -0
- package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-Bb2N4m40.js} +15 -3
- package/dist/useDropdownState-Bb2N4m40.js.map +1 -0
- package/dist/{useExperimentSamples-BGN1dmcR.js → useExperimentSamples-BLx29CkM.js} +3 -3
- package/dist/{useExperimentSamples-BGN1dmcR.js.map → useExperimentSamples-BLx29CkM.js.map} +1 -1
- package/dist/{useListReorder-DPynGrCj.js → useListReorder-C_8RYeVP.js} +3 -3
- package/dist/{useListReorder-DPynGrCj.js.map → useListReorder-C_8RYeVP.js.map} +1 -1
- package/dist/{useRackEditor-B2FrQA0p.js → useRackEditor-BhYlXKFM.js} +8 -8
- package/dist/{useRackEditor-B2FrQA0p.js.map → useRackEditor-BhYlXKFM.js.map} +1 -1
- package/dist/{useRealtime-yWjTTnc9.js → useRealtime-CUMbW_ln.js} +2 -2
- package/dist/{useRealtime-yWjTTnc9.js.map → useRealtime-CUMbW_ln.js.map} +1 -1
- package/dist/{useRequest-Dqw12k2s.js → useRequest-C9uwM79q.js} +3 -3
- package/dist/{useRequest-Dqw12k2s.js.map → useRequest-C9uwM79q.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/components/BaseButton.vue +16 -2
- package/src/styles/components/button.css +8 -0
- package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as normalizeJobState, l as jobStatusLabel, s as isActiveJobStatus, u as normalizeJobPercent } from "./records-
|
|
1
|
+
import { d as normalizeJobState, l as jobStatusLabel, s as isActiveJobStatus, u as normalizeJobPercent } from "./records-B4yl2oIu.js";
|
|
2
2
|
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, getCurrentScope, nextTick, normalizeClass, normalizeStyle, onMounted, onScopeDispose, onUnmounted, openBlock, readonly, ref, renderList, renderSlot, shallowRef, toDisplayString, toRef, toValue, unref, useId, watch, withCtx } from "vue";
|
|
3
3
|
//#region src/composables/useJobCenter.ts
|
|
4
4
|
/**
|
|
@@ -696,4 +696,4 @@ function subscribeRealtime(topics, handler, withPresence) {
|
|
|
696
696
|
//#endregion
|
|
697
697
|
export { combinePluginJobSources as a, JobsStatusTray_default as i, subscribeRealtime as n, useJobCenter as o, useRealtime as r, currentPresence as t };
|
|
698
698
|
|
|
699
|
-
//# sourceMappingURL=useRealtime-
|
|
699
|
+
//# sourceMappingURL=useRealtime-CUMbW_ln.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRealtime-yWjTTnc9.js","names":[],"sources":["../src/composables/useJobCenter.ts","../src/components/JobsStatusTray.vue","../src/components/JobsStatusTray.vue","../src/composables/useRealtime.ts"],"sourcesContent":["import {\n computed,\n onMounted,\n onUnmounted,\n ref,\n shallowRef,\n toValue,\n type ComputedRef,\n type MaybeRefOrGetter,\n type Ref,\n type ShallowRef,\n} from 'vue'\n\nimport { isActiveJobStatus, normalizeJobState } from '../jobs'\nimport type {\n JobListPayload,\n JobState,\n JobStateInput,\n} from '../types/jobs'\n\nexport type JobsStatusTrayTab = 'active' | 'history'\n\n/**\n * The canonical owner of a collection of plugin jobs.\n *\n * Sources, rather than job-center UI, own polling, event streams, and all job\n * mutations. This keeps components and consumers on one reactive job graph.\n */\nexport interface PluginJobCenterSource {\n jobs: MaybeRefOrGetter<readonly JobStateInput[]>\n refresh?: () => Promise<JobListPayload | readonly JobStateInput[] | void>\n cancel?: (jobId: string) => Promise<JobStateInput | void>\n delete?: (jobId: string) => Promise<boolean | void>\n clear?: () => Promise<number | void>\n dispose?: () => void\n}\n\nexport interface CombinedPluginJobCenterSource extends PluginJobCenterSource {\n jobs: ComputedRef<readonly JobStateInput[]>\n}\n\n/**\n * Combines independent job runtimes without copying their reactive state.\n *\n * Job ids must be globally unique across the sources. A duplicate id throws\n * when jobs are read or an id-routed action is requested, rather than silently\n * dispatching a mutation to the wrong owner.\n */\nexport function combinePluginJobSources(\n sources: readonly PluginJobCenterSource[],\n): CombinedPluginJobCenterSource {\n const jobs = computed<readonly JobStateInput[]>(() => {\n const combined: JobStateInput[] = []\n const owners = new Set<string>()\n for (const source of sources) {\n for (const job of toValue(source.jobs)) {\n if (owners.has(job.job_id)) throw duplicatePluginJobId(job.job_id)\n owners.add(job.job_id)\n combined.push(job)\n }\n }\n return combined\n })\n\n function ownerFor(jobId: string): PluginJobCenterSource {\n let owner: PluginJobCenterSource | undefined\n for (const source of sources) {\n if (!toValue(source.jobs).some(job => job.job_id === jobId)) continue\n if (owner) throw duplicatePluginJobId(jobId)\n owner = source\n }\n if (!owner) throw new Error(`Unknown plugin job id: ${jobId}`)\n return owner\n }\n\n const combined: CombinedPluginJobCenterSource = { jobs }\n if (sources.some(source => source.refresh)) {\n combined.refresh = async () => {\n await Promise.all(sources.map(source => source.refresh?.()))\n return jobs.value\n }\n }\n if (sources.some(source => source.cancel)) {\n combined.cancel = async (jobId) => {\n const owner = ownerFor(jobId)\n if (!owner.cancel) throw new Error(`Job ${jobId} cannot be cancelled by its source`)\n return owner.cancel(jobId)\n }\n }\n if (sources.length > 0 && sources.every(source => source.delete)) {\n combined.delete = async (jobId) => {\n const owner = ownerFor(jobId)\n if (!owner.delete) throw new Error(`Job ${jobId} cannot be deleted by its source`)\n return owner.delete(jobId)\n }\n }\n // Never report a successful combined clear after mutating only a subset.\n // Without bulk support from every source, the job center can still fall\n // back to this source's id-routed delete method when all owners expose it.\n if (sources.length > 0 && sources.every(source => source.clear)) {\n combined.clear = async () => {\n const results = await Promise.all(\n sources.map(source => source.clear!()),\n )\n return results.every((result): result is number => typeof result === 'number')\n ? results.reduce((total, result) => total + result, 0)\n : undefined\n }\n }\n if (sources.some(source => source.dispose)) {\n combined.dispose = () => {\n const errors: unknown[] = []\n for (const source of sources) {\n try {\n source.dispose?.()\n } catch (cause) {\n errors.push(cause)\n }\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) throw new PluginJobSourceDisposeError(errors)\n }\n }\n return combined\n}\n\nclass PluginJobSourceDisposeError extends Error {\n readonly cause: unknown\n readonly errors: readonly unknown[]\n\n constructor(errors: readonly unknown[]) {\n super('Failed to dispose plugin job sources')\n this.name = 'PluginJobSourceDisposeError'\n this.cause = errors[0]\n this.errors = Object.freeze([...errors])\n }\n}\n\nfunction duplicatePluginJobId(jobId: string): Error {\n return new Error(`Duplicate plugin job id across combined sources: ${jobId}`)\n}\n\nexport interface UseJobCenterOptions {\n source?: MaybeRefOrGetter<PluginJobCenterSource | undefined>\n loadOnMount?: boolean\n}\n\nexport interface UseJobCenterReturn {\n jobs: ComputedRef<JobState[]>\n activeJobs: ComputedRef<JobState[]>\n historyJobs: ComputedRef<JobState[]>\n deletableJobs: ComputedRef<JobState[]>\n featuredJob: ComputedRef<JobState | null>\n canRefresh: ComputedRef<boolean>\n canCancel: ComputedRef<boolean>\n canDelete: ComputedRef<boolean>\n canClearFinished: ComputedRef<boolean>\n isOpen: Ref<boolean>\n activeTab: Ref<JobsStatusTrayTab>\n isLoading: Ref<boolean>\n error: ShallowRef<string | null>\n pendingJobIds: Ref<Set<string>>\n open: () => void\n close: () => void\n toggle: () => void\n refresh: () => Promise<boolean>\n cancelJob: (job: JobState) => Promise<boolean>\n deleteJob: (job: JobState) => Promise<boolean>\n clearFinished: () => Promise<boolean>\n isPending: (job: JobState) => boolean\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error ? error.message : fallback\n}\n\n/**\n * Headless controller for plugin job presentation and actions.\n *\n * Every job list is derived directly from the source. The controller never\n * mirrors source jobs into another ref.\n */\nexport function useJobCenter(\n options: UseJobCenterOptions = {},\n): UseJobCenterReturn {\n const isOpen = ref(false)\n const activeTab = ref<JobsStatusTrayTab>('active')\n const isLoading = ref(false)\n const error = shallowRef<string | null>(null)\n const pendingJobIds = ref(new Set<string>())\n let requestGeneration = 0\n let active = true\n const source = computed(() => toValue(options.source))\n\n const jobs = computed<JobState[]>(() => {\n const currentSource = source.value\n return currentSource ? [...toValue(currentSource.jobs)].map(normalizeJobState) : []\n })\n const activeJobs = computed(() => jobs.value.filter(job => isActiveJobStatus(job.status)))\n const historyJobs = computed(() => jobs.value.filter(job => !isActiveJobStatus(job.status)))\n const deletableJobs = computed(() => historyJobs.value.filter(job => job.can_delete))\n const featuredJob = computed(() => activeJobs.value[0] ?? historyJobs.value[0] ?? null)\n const canRefresh = computed(() => Boolean(source.value?.refresh))\n const canCancel = computed(() => Boolean(source.value?.cancel))\n const canDelete = computed(() => Boolean(source.value?.delete))\n const canClearFinished = computed(() => Boolean(\n source.value?.clear || source.value?.delete,\n ))\n\n function setPending(jobId: string, pending: boolean): void {\n const next = new Set(pendingJobIds.value)\n if (pending) next.add(jobId)\n else next.delete(jobId)\n pendingJobIds.value = next\n }\n\n async function refresh(): Promise<boolean> {\n const listJobs = source.value?.refresh\n if (!listJobs) return false\n const generation = ++requestGeneration\n isLoading.value = true\n error.value = null\n try {\n await listJobs()\n return active && generation === requestGeneration\n } catch (cause) {\n if (active && generation === requestGeneration) {\n error.value = errorMessage(cause, 'Failed to load jobs')\n }\n return false\n } finally {\n if (active && generation === requestGeneration) isLoading.value = false\n }\n }\n\n async function cancelJob(job: JobState): Promise<boolean> {\n const sourceCancel = source.value?.cancel\n if (!job.can_cancel || !sourceCancel || pendingJobIds.value.has(job.job_id)) {\n return false\n }\n setPending(job.job_id, true)\n error.value = null\n try {\n await sourceCancel(job.job_id)\n return active\n } catch (cause) {\n if (active) error.value = errorMessage(cause, 'Failed to cancel job')\n return false\n } finally {\n if (active) setPending(job.job_id, false)\n }\n }\n\n async function deleteJob(job: JobState): Promise<boolean> {\n const sourceDelete = source.value?.delete\n if (!job.can_delete || !sourceDelete || pendingJobIds.value.has(job.job_id)) {\n return false\n }\n setPending(job.job_id, true)\n error.value = null\n try {\n const deleted = await sourceDelete(job.job_id)\n return active && deleted !== false\n } catch (cause) {\n if (active) error.value = errorMessage(cause, 'Failed to delete job')\n return false\n } finally {\n if (active) setPending(job.job_id, false)\n }\n }\n\n async function clearFinished(): Promise<boolean> {\n const targets = deletableJobs.value\n if (targets.length === 0) return false\n error.value = null\n try {\n const currentSource = source.value\n if (currentSource?.clear) {\n const deleted = await currentSource.clear()\n if (!active) return false\n return typeof deleted !== 'number' || deleted === targets.length\n }\n if (!currentSource?.delete) return false\n const results = await Promise.all(targets.map(job => deleteJob(job)))\n return results.every(Boolean)\n } catch (cause) {\n if (active) error.value = errorMessage(cause, 'Failed to clear jobs')\n return false\n }\n }\n\n onMounted(() => {\n if (options.loadOnMount !== false) void refresh()\n })\n onUnmounted(() => {\n active = false\n requestGeneration += 1\n })\n\n return {\n jobs,\n activeJobs,\n historyJobs,\n deletableJobs,\n featuredJob,\n canRefresh,\n canCancel,\n canDelete,\n canClearFinished,\n isOpen,\n activeTab,\n isLoading,\n error,\n pendingJobIds,\n open: () => { isOpen.value = true },\n close: () => { isOpen.value = false },\n toggle: () => { isOpen.value = !isOpen.value },\n refresh,\n cancelJob,\n deleteJob,\n clearFinished,\n isPending: job => pendingJobIds.value.has(job.job_id),\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Floating, source-driven job status tray for plugin and platform workflows.\n *\n * @example Recommended\n * ```vue\n * <JobsStatusTray :source=\"jobs\" />\n * ```\n */\nimport { computed, nextTick, ref, toRef, useId } from 'vue'\n\nimport {\n jobStatusLabel,\n normalizeJobPercent,\n} from '../jobs'\nimport {\n useJobCenter,\n type PluginJobCenterSource,\n} from '../composables/useJobCenter'\nimport type { JobState } from '../types/jobs'\n\ninterface Props {\n source?: PluginJobCenterSource\n loadOnMount?: boolean\n title?: string\n teleportTo?: string | HTMLElement | false\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n loadOnMount: true,\n title: 'Analysis jobs',\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n select: [job: JobState]\n error: [message: string]\n}>()\n\ndefineSlots<{\n /** Extra per-row actions, rendered before the built-in Cancel / Delete. */\n 'job-actions'?: (props: { job: JobState }) => unknown\n}>()\n\nconst panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`\nconst titleId = `${panelId}-title`\nconst triggerRef = ref<HTMLButtonElement | null>(null)\nconst panelRef = ref<HTMLElement | null>(null)\nconst teleportTarget = computed(() => props.teleportTo || 'body')\nconst tray = useJobCenter({\n source: toRef(props, 'source'),\n loadOnMount: props.loadOnMount,\n})\n// 2f: a job waiting for input is the only one that needs a person — pin it first.\nconst activeRows = computed(() => [\n ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),\n ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),\n])\nconst subtitle = computed(() => {\n const count = (status: JobState['status']) => tray.activeJobs.value.filter(job => job.status === status).length\n const parts = [\n [count('running'), 'running'],\n [count('queued'), 'queued'],\n [count('awaiting_input'), 'waiting'],\n ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)\n return parts.length ? parts.join(' · ') : `${tray.jobs.value.length} total`\n})\nconst canClear = computed(() => (\n tray.deletableJobs.value.length > 0\n && tray.canClearFinished.value\n))\n\nasync function openTray(): Promise<void> {\n tray.open()\n await nextTick()\n panelRef.value?.focus()\n}\n\nasync function closeTray(): Promise<void> {\n tray.close()\n await nextTick()\n triggerRef.value?.focus()\n}\n\nasync function toggleTray(): Promise<void> {\n if (tray.isOpen.value) await closeTray()\n else await openTray()\n}\n\nfunction onPanelKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') {\n void closeTray()\n return\n }\n if (event.key !== 'Tab' || !panelRef.value) return\n const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n ))\n if (focusable.length === 0) {\n event.preventDefault()\n panelRef.value.focus()\n return\n }\n const first = focusable[0]\n const last = focusable[focusable.length - 1]\n const activeElement = document.activeElement as HTMLElement | null\n if (!activeElement || !focusable.includes(activeElement)) {\n event.preventDefault()\n if (event.shiftKey) last?.focus()\n else first?.focus()\n } else if (event.shiftKey && activeElement === first) {\n event.preventDefault()\n last?.focus()\n } else if (!event.shiftKey && activeElement === last) {\n event.preventDefault()\n first?.focus()\n }\n}\n\nfunction progressText(job: JobState): string {\n if (job.status === 'awaiting_input') {\n return job.progress.message || 'Waiting for external input'\n }\n if (job.status === 'queued' && job.queue_position > 0) {\n return `Queue position ${job.queue_position}`\n }\n return job.progress.message || job.progress.stage || jobStatusLabel(job.status)\n}\n\nfunction formatTimestamp(value: string | null): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime())\n ? ''\n : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)\n}\n\nfunction statusClass(job: JobState): string {\n return `mint-jobs-tray__status--${job.status.replace('_', '-')}`\n}\n\nfunction jobPercent(job: JobState): number {\n return Math.round(normalizeJobPercent(job.progress.percent))\n}\n\nasync function cancelJob(job: JobState): Promise<void> {\n const succeeded = await tray.cancelJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function deleteJob(job: JobState): Promise<void> {\n const succeeded = await tray.deleteJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function clearFinished(): Promise<void> {\n const succeeded = await tray.clearFinished()\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTarget\" :disabled=\"teleportTo === false\">\n <div class=\"mint-jobs-tray\">\n <button\n ref=\"triggerRef\"\n type=\"button\"\n class=\"mint-jobs-tray__trigger\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"tray.isOpen.value\"\n :aria-controls=\"panelId\"\n @click=\"toggleTray\"\n >\n <svg\n class=\"mint-jobs-tray__trigger-icon\"\n :class=\"{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path d=\"M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2\" />\n <path d=\"M9 5a3 3 0 0 1 6 0v2H9V5Z\" />\n </svg>\n <span class=\"mint-jobs-tray__trigger-label\">\n {{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}\n </span>\n <span\n v-if=\"tray.activeJobs.value.length > 0\"\n class=\"mint-jobs-tray__count\"\n aria-live=\"polite\"\n >\n {{ tray.activeJobs.value.length }}\n </span>\n </button>\n\n <Transition name=\"mint-jobs-tray-fade\">\n <div\n v-if=\"tray.isOpen.value\"\n class=\"mint-jobs-tray__backdrop\"\n aria-hidden=\"true\"\n @click=\"closeTray\"\n />\n </Transition>\n\n <Transition name=\"mint-jobs-tray-slide\">\n <section\n v-if=\"tray.isOpen.value\"\n :id=\"panelId\"\n ref=\"panelRef\"\n class=\"mint-jobs-tray__panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"titleId\"\n tabindex=\"-1\"\n @keydown=\"onPanelKeydown\"\n >\n <header class=\"mint-jobs-tray__header\">\n <div>\n <h2 :id=\"titleId\" class=\"mint-jobs-tray__title\">{{ title }}</h2>\n <p class=\"mint-jobs-tray__subtitle\" :title=\"subtitle\">{{ subtitle }}</p>\n </div>\n <div class=\"mint-jobs-tray__header-actions\">\n <button\n v-if=\"tray.canRefresh.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Refresh jobs\"\n :disabled=\"tray.isLoading.value\"\n @click=\"tray.refresh\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M20 6v5h-5M4 18v-5h5\" />\n <path d=\"M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Close jobs\"\n @click=\"closeTray\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m6 6 12 12M18 6 6 18\" />\n </svg>\n </button>\n </div>\n </header>\n\n <div class=\"mint-jobs-tray__tabs\" role=\"tablist\" aria-label=\"Job groups\">\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"active\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'active' }\"\n :aria-selected=\"tray.activeTab.value === 'active'\"\n @click=\"tray.activeTab.value = 'active'\"\n >\n Active\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.activeJobs.value.length }}</span>\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"history\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'history' }\"\n :aria-selected=\"tray.activeTab.value === 'history'\"\n @click=\"tray.activeTab.value = 'history'\"\n >\n History\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.historyJobs.value.length }}</span>\n </button>\n </div>\n\n <p v-if=\"tray.error.value\" class=\"mint-jobs-tray__error\" role=\"alert\">\n {{ tray.error.value }}\n </p>\n\n <div class=\"mint-jobs-tray__body\">\n <template v-if=\"tray.activeTab.value === 'active'\">\n <p v-if=\"tray.activeJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No active jobs\n </p>\n <article\n v-for=\"job in activeRows\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n :class=\"{ 'mint-jobs-tray__job--awaiting-input': job.status === 'awaiting_input' }\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n <span v-if=\"job.status === 'running'\" class=\"mint-jobs-tray__percent\">{{ jobPercent(job) }}%</span>\n </div>\n <div\n v-if=\"job.status === 'running'\"\n class=\"mint-jobs-tray__progress\"\n role=\"progressbar\"\n aria-label=\"Job progress\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"jobPercent(job)\"\n >\n <span :style=\"{ width: `${jobPercent(job)}%` }\" />\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ progressText(job) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_cancel && tray.canCancel.value\"\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n aria-label=\"Cancel job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"cancelJob(job)\"\n >\n Cancel\n </button>\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n\n <template v-else>\n <div v-if=\"canClear\" class=\"mint-jobs-tray__clear-row\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n @click=\"clearFinished\"\n >\n Clear finished\n </button>\n </div>\n <p v-if=\"tray.historyJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No job history\n </p>\n <article\n v-for=\"job in tray.historyJobs.value\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n </div>\n </section>\n </Transition>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Floating, source-driven job status tray for plugin and platform workflows.\n *\n * @example Recommended\n * ```vue\n * <JobsStatusTray :source=\"jobs\" />\n * ```\n */\nimport { computed, nextTick, ref, toRef, useId } from 'vue'\n\nimport {\n jobStatusLabel,\n normalizeJobPercent,\n} from '../jobs'\nimport {\n useJobCenter,\n type PluginJobCenterSource,\n} from '../composables/useJobCenter'\nimport type { JobState } from '../types/jobs'\n\ninterface Props {\n source?: PluginJobCenterSource\n loadOnMount?: boolean\n title?: string\n teleportTo?: string | HTMLElement | false\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n loadOnMount: true,\n title: 'Analysis jobs',\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n select: [job: JobState]\n error: [message: string]\n}>()\n\ndefineSlots<{\n /** Extra per-row actions, rendered before the built-in Cancel / Delete. */\n 'job-actions'?: (props: { job: JobState }) => unknown\n}>()\n\nconst panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`\nconst titleId = `${panelId}-title`\nconst triggerRef = ref<HTMLButtonElement | null>(null)\nconst panelRef = ref<HTMLElement | null>(null)\nconst teleportTarget = computed(() => props.teleportTo || 'body')\nconst tray = useJobCenter({\n source: toRef(props, 'source'),\n loadOnMount: props.loadOnMount,\n})\n// 2f: a job waiting for input is the only one that needs a person — pin it first.\nconst activeRows = computed(() => [\n ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),\n ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),\n])\nconst subtitle = computed(() => {\n const count = (status: JobState['status']) => tray.activeJobs.value.filter(job => job.status === status).length\n const parts = [\n [count('running'), 'running'],\n [count('queued'), 'queued'],\n [count('awaiting_input'), 'waiting'],\n ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)\n return parts.length ? parts.join(' · ') : `${tray.jobs.value.length} total`\n})\nconst canClear = computed(() => (\n tray.deletableJobs.value.length > 0\n && tray.canClearFinished.value\n))\n\nasync function openTray(): Promise<void> {\n tray.open()\n await nextTick()\n panelRef.value?.focus()\n}\n\nasync function closeTray(): Promise<void> {\n tray.close()\n await nextTick()\n triggerRef.value?.focus()\n}\n\nasync function toggleTray(): Promise<void> {\n if (tray.isOpen.value) await closeTray()\n else await openTray()\n}\n\nfunction onPanelKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') {\n void closeTray()\n return\n }\n if (event.key !== 'Tab' || !panelRef.value) return\n const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n ))\n if (focusable.length === 0) {\n event.preventDefault()\n panelRef.value.focus()\n return\n }\n const first = focusable[0]\n const last = focusable[focusable.length - 1]\n const activeElement = document.activeElement as HTMLElement | null\n if (!activeElement || !focusable.includes(activeElement)) {\n event.preventDefault()\n if (event.shiftKey) last?.focus()\n else first?.focus()\n } else if (event.shiftKey && activeElement === first) {\n event.preventDefault()\n last?.focus()\n } else if (!event.shiftKey && activeElement === last) {\n event.preventDefault()\n first?.focus()\n }\n}\n\nfunction progressText(job: JobState): string {\n if (job.status === 'awaiting_input') {\n return job.progress.message || 'Waiting for external input'\n }\n if (job.status === 'queued' && job.queue_position > 0) {\n return `Queue position ${job.queue_position}`\n }\n return job.progress.message || job.progress.stage || jobStatusLabel(job.status)\n}\n\nfunction formatTimestamp(value: string | null): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime())\n ? ''\n : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)\n}\n\nfunction statusClass(job: JobState): string {\n return `mint-jobs-tray__status--${job.status.replace('_', '-')}`\n}\n\nfunction jobPercent(job: JobState): number {\n return Math.round(normalizeJobPercent(job.progress.percent))\n}\n\nasync function cancelJob(job: JobState): Promise<void> {\n const succeeded = await tray.cancelJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function deleteJob(job: JobState): Promise<void> {\n const succeeded = await tray.deleteJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function clearFinished(): Promise<void> {\n const succeeded = await tray.clearFinished()\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTarget\" :disabled=\"teleportTo === false\">\n <div class=\"mint-jobs-tray\">\n <button\n ref=\"triggerRef\"\n type=\"button\"\n class=\"mint-jobs-tray__trigger\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"tray.isOpen.value\"\n :aria-controls=\"panelId\"\n @click=\"toggleTray\"\n >\n <svg\n class=\"mint-jobs-tray__trigger-icon\"\n :class=\"{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path d=\"M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2\" />\n <path d=\"M9 5a3 3 0 0 1 6 0v2H9V5Z\" />\n </svg>\n <span class=\"mint-jobs-tray__trigger-label\">\n {{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}\n </span>\n <span\n v-if=\"tray.activeJobs.value.length > 0\"\n class=\"mint-jobs-tray__count\"\n aria-live=\"polite\"\n >\n {{ tray.activeJobs.value.length }}\n </span>\n </button>\n\n <Transition name=\"mint-jobs-tray-fade\">\n <div\n v-if=\"tray.isOpen.value\"\n class=\"mint-jobs-tray__backdrop\"\n aria-hidden=\"true\"\n @click=\"closeTray\"\n />\n </Transition>\n\n <Transition name=\"mint-jobs-tray-slide\">\n <section\n v-if=\"tray.isOpen.value\"\n :id=\"panelId\"\n ref=\"panelRef\"\n class=\"mint-jobs-tray__panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"titleId\"\n tabindex=\"-1\"\n @keydown=\"onPanelKeydown\"\n >\n <header class=\"mint-jobs-tray__header\">\n <div>\n <h2 :id=\"titleId\" class=\"mint-jobs-tray__title\">{{ title }}</h2>\n <p class=\"mint-jobs-tray__subtitle\" :title=\"subtitle\">{{ subtitle }}</p>\n </div>\n <div class=\"mint-jobs-tray__header-actions\">\n <button\n v-if=\"tray.canRefresh.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Refresh jobs\"\n :disabled=\"tray.isLoading.value\"\n @click=\"tray.refresh\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M20 6v5h-5M4 18v-5h5\" />\n <path d=\"M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Close jobs\"\n @click=\"closeTray\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m6 6 12 12M18 6 6 18\" />\n </svg>\n </button>\n </div>\n </header>\n\n <div class=\"mint-jobs-tray__tabs\" role=\"tablist\" aria-label=\"Job groups\">\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"active\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'active' }\"\n :aria-selected=\"tray.activeTab.value === 'active'\"\n @click=\"tray.activeTab.value = 'active'\"\n >\n Active\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.activeJobs.value.length }}</span>\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"history\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'history' }\"\n :aria-selected=\"tray.activeTab.value === 'history'\"\n @click=\"tray.activeTab.value = 'history'\"\n >\n History\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.historyJobs.value.length }}</span>\n </button>\n </div>\n\n <p v-if=\"tray.error.value\" class=\"mint-jobs-tray__error\" role=\"alert\">\n {{ tray.error.value }}\n </p>\n\n <div class=\"mint-jobs-tray__body\">\n <template v-if=\"tray.activeTab.value === 'active'\">\n <p v-if=\"tray.activeJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No active jobs\n </p>\n <article\n v-for=\"job in activeRows\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n :class=\"{ 'mint-jobs-tray__job--awaiting-input': job.status === 'awaiting_input' }\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n <span v-if=\"job.status === 'running'\" class=\"mint-jobs-tray__percent\">{{ jobPercent(job) }}%</span>\n </div>\n <div\n v-if=\"job.status === 'running'\"\n class=\"mint-jobs-tray__progress\"\n role=\"progressbar\"\n aria-label=\"Job progress\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"jobPercent(job)\"\n >\n <span :style=\"{ width: `${jobPercent(job)}%` }\" />\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ progressText(job) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_cancel && tray.canCancel.value\"\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n aria-label=\"Cancel job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"cancelJob(job)\"\n >\n Cancel\n </button>\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n\n <template v-else>\n <div v-if=\"canClear\" class=\"mint-jobs-tray__clear-row\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n @click=\"clearFinished\"\n >\n Clear finished\n </button>\n </div>\n <p v-if=\"tray.historyJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No job history\n </p>\n <article\n v-for=\"job in tray.historyJobs.value\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n </div>\n </section>\n </Transition>\n </div>\n </Teleport>\n</template>\n","import {\n getCurrentScope,\n onScopeDispose,\n readonly,\n ref,\n toValue,\n watch,\n type MaybeRefOrGetter,\n type Ref,\n} from 'vue'\n\n/**\n * One event of the platform's realtime stream (`GET /api/events`).\n *\n * Platform events carry no entity data: refetch the entity through REST.\n * `type` is `resync` (refetch everything you show), the audit action of a\n * committed write, such as `experiment.update` or `analysis.artifact_create`,\n * or the `\"<plugin>.<event>\"` name of a plugin event, which carries `payload`.\n * Only `usePresence` receives `presence.changed`.\n */\nexport interface RealtimeEvent {\n type: string\n id?: string\n /**\n * `experiment:{id}`, `project:{id}`, `experiments`, `user:{id}`, or a plugin's\n * `plugin:{name}` / `plugin:{name}:experiment:{id}`; absent on `resync`.\n */\n topic?: string\n /** A plugin event: the JSON payload the plugin published (at most 16 KiB). */\n payload?: unknown\n /** Changed fields of an update, when the write names them. */\n fields?: string[] | null\n actor_id?: string | null\n request_id?: string | null\n at?: string\n /** `presence.changed`: the topic's viewers (at most 20, `editing` first). */\n viewers?: { user_id: string, activity: string }[]\n /** `presence.changed`: the number of viewers, also those beyond `viewers`. */\n count?: number\n}\n\nexport type RealtimeHandler = (event: RealtimeEvent) => void\n\nexport interface UseRealtimeReturn {\n /** True while the shared stream is open. */\n connected: Readonly<Ref<boolean>>\n /** Unsubscribe; also runs when the calling component or effect scope is disposed. */\n stop: () => void\n}\n\nconst EVENTS_URL = '/api/events'\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\n/** Merges the subscriptions of components that mount together into one connection. */\nconst TOPIC_DEBOUNCE_MS = 50\nconst RESYNC: RealtimeEvent = { type: 'resync' }\n\ninterface Listener {\n topics: () => string[]\n handler: RealtimeHandler\n /** Receives `presence.changed`; a write listener would refetch on each viewer change. */\n presence: boolean\n}\n\nconst listeners = new Set<Listener>()\n/** The last `presence.changed` of each topic of the open stream; the server sends one per topic at open. */\nconst presence = new Map<string, RealtimeEvent>()\nconst connected = ref(false)\nlet source: EventSource | null = null\nlet sourceTopics = ''\nlet failures = 0\nlet syncTimer: ReturnType<typeof setTimeout> | null = null\nlet retryTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction dispatch(event: RealtimeEvent): void {\n const isPresence = event.type === 'presence.changed'\n if (isPresence && event.topic) presence.set(event.topic, event)\n for (const listener of [...listeners]) {\n if (isPresence && !listener.presence) continue\n if (event.type === 'resync' || (event.topic && listener.topics().includes(event.topic))) {\n listener.handler(event)\n }\n }\n}\n\nfunction close(): void {\n source?.close()\n source = null\n sourceTopics = ''\n presence.clear()\n connected.value = false\n}\n\nfunction open(topics: string): void {\n const stream = new EventSource(`${EVENTS_URL}?topics=${encodeURIComponent(topics)}`)\n source = stream\n sourceTopics = topics\n stream.onopen = () => {\n connected.value = true\n // The server subscribes only now: a write between a caller's REST load (or\n // the close of the previous stream) and this point sent no event, so every caller refetches.\n dispatch(RESYNC)\n failures = 0\n }\n stream.onmessage = (message) => {\n dispatch(JSON.parse(message.data) as RealtimeEvent)\n }\n stream.onerror = () => {\n // EventSource retries a dropped connection itself but gives up after an\n // HTTP error, so one backoff path handles both.\n if (source !== stream) return\n close()\n // A topic change during the wait does not shorten it: the retry reads the topics when it runs.\n if (syncTimer !== null) clearTimeout(syncTimer)\n syncTimer = null\n retryTimer = setTimeout(() => {\n retryTimer = null\n sync()\n }, Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** failures))\n failures += 1\n }\n}\n\nfunction sync(): void {\n syncTimer = null\n const topics = [...new Set([...listeners].flatMap(listener => listener.topics()))].sort().join(',')\n if (source && topics === sourceTopics) return\n close()\n if (topics) open(topics)\n}\n\nfunction schedule(): void {\n if (retryTimer !== null) return\n if (syncTimer !== null) clearTimeout(syncTimer)\n syncTimer = setTimeout(sync, TOPIC_DEBOUNCE_MS)\n}\n\n/**\n * Subscribe to topics of the platform's realtime stream.\n *\n * Every caller in a tab shares one `EventSource`, opened on the union of\n * their topics and reopened when that union changes. `handler` receives the\n * write events of the caller's own topics (not `presence.changed`, which goes\n * to `usePresence`) plus every `resync`: one comes from the\n * server when this tab fell behind, and one follows each open of the stream:\n * the first open, each reconnect (backoff from 1 s to 30 s) and each reopen for\n * a changed topic union. On `resync`, refetch. The union of all callers' topics\n * in a tab may hold at most 50 topics; the platform refuses a larger stream.\n *\n * The stream authenticates with the session cookie, so it works in the\n * platform and in plugin frontends served by it. It exists only when the\n * platform's `realtime` feature flag is on; check that flag first.\n *\n * A plugin topic reaches a caller whose role may use the plugin; an\n * experiment plugin topic also needs a caller who may view the experiment.\n *\n * @example\n * useRealtime(() => [`experiment:${id.value}`], () => refetch())\n * @example\n * useRealtime(['plugin:drp'], (event) => {\n * if (event.type === 'drp.run.progress') progress.value = event.payload as RunProgress\n * })\n */\nexport function useRealtime(\n topics: MaybeRefOrGetter<string[]>,\n handler: RealtimeHandler,\n): UseRealtimeReturn {\n return subscribeRealtime(topics, handler, false)\n}\n\n/** The last `presence.changed` of `topic` while the open stream views it (for `usePresence`). */\nexport function currentPresence(topic: string): RealtimeEvent | undefined {\n return presence.get(topic)\n}\n\n/** `useRealtime`, and with `withPresence` also the `presence.changed` events (for `usePresence`). */\nexport function subscribeRealtime(\n topics: MaybeRefOrGetter<string[]>,\n handler: RealtimeHandler,\n withPresence: boolean,\n): UseRealtimeReturn {\n const listener: Listener = { topics: () => toValue(topics), handler, presence: withPresence }\n listeners.add(listener)\n const stopWatch = watch(() => listener.topics().join(','), () => schedule())\n schedule()\n\n function stop(): void {\n stopWatch()\n if (listeners.delete(listener)) schedule()\n }\n\n if (getCurrentScope()) onScopeDispose(stop)\n return { connected: readonly(connected), stop }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useRealtime-CUMbW_ln.js","names":[],"sources":["../src/composables/useJobCenter.ts","../src/components/JobsStatusTray.vue","../src/components/JobsStatusTray.vue","../src/composables/useRealtime.ts"],"sourcesContent":["import {\n computed,\n onMounted,\n onUnmounted,\n ref,\n shallowRef,\n toValue,\n type ComputedRef,\n type MaybeRefOrGetter,\n type Ref,\n type ShallowRef,\n} from 'vue'\n\nimport { isActiveJobStatus, normalizeJobState } from '../jobs'\nimport type {\n JobListPayload,\n JobState,\n JobStateInput,\n} from '../types/jobs'\n\nexport type JobsStatusTrayTab = 'active' | 'history'\n\n/**\n * The canonical owner of a collection of plugin jobs.\n *\n * Sources, rather than job-center UI, own polling, event streams, and all job\n * mutations. This keeps components and consumers on one reactive job graph.\n */\nexport interface PluginJobCenterSource {\n jobs: MaybeRefOrGetter<readonly JobStateInput[]>\n refresh?: () => Promise<JobListPayload | readonly JobStateInput[] | void>\n cancel?: (jobId: string) => Promise<JobStateInput | void>\n delete?: (jobId: string) => Promise<boolean | void>\n clear?: () => Promise<number | void>\n dispose?: () => void\n}\n\nexport interface CombinedPluginJobCenterSource extends PluginJobCenterSource {\n jobs: ComputedRef<readonly JobStateInput[]>\n}\n\n/**\n * Combines independent job runtimes without copying their reactive state.\n *\n * Job ids must be globally unique across the sources. A duplicate id throws\n * when jobs are read or an id-routed action is requested, rather than silently\n * dispatching a mutation to the wrong owner.\n */\nexport function combinePluginJobSources(\n sources: readonly PluginJobCenterSource[],\n): CombinedPluginJobCenterSource {\n const jobs = computed<readonly JobStateInput[]>(() => {\n const combined: JobStateInput[] = []\n const owners = new Set<string>()\n for (const source of sources) {\n for (const job of toValue(source.jobs)) {\n if (owners.has(job.job_id)) throw duplicatePluginJobId(job.job_id)\n owners.add(job.job_id)\n combined.push(job)\n }\n }\n return combined\n })\n\n function ownerFor(jobId: string): PluginJobCenterSource {\n let owner: PluginJobCenterSource | undefined\n for (const source of sources) {\n if (!toValue(source.jobs).some(job => job.job_id === jobId)) continue\n if (owner) throw duplicatePluginJobId(jobId)\n owner = source\n }\n if (!owner) throw new Error(`Unknown plugin job id: ${jobId}`)\n return owner\n }\n\n const combined: CombinedPluginJobCenterSource = { jobs }\n if (sources.some(source => source.refresh)) {\n combined.refresh = async () => {\n await Promise.all(sources.map(source => source.refresh?.()))\n return jobs.value\n }\n }\n if (sources.some(source => source.cancel)) {\n combined.cancel = async (jobId) => {\n const owner = ownerFor(jobId)\n if (!owner.cancel) throw new Error(`Job ${jobId} cannot be cancelled by its source`)\n return owner.cancel(jobId)\n }\n }\n if (sources.length > 0 && sources.every(source => source.delete)) {\n combined.delete = async (jobId) => {\n const owner = ownerFor(jobId)\n if (!owner.delete) throw new Error(`Job ${jobId} cannot be deleted by its source`)\n return owner.delete(jobId)\n }\n }\n // Never report a successful combined clear after mutating only a subset.\n // Without bulk support from every source, the job center can still fall\n // back to this source's id-routed delete method when all owners expose it.\n if (sources.length > 0 && sources.every(source => source.clear)) {\n combined.clear = async () => {\n const results = await Promise.all(\n sources.map(source => source.clear!()),\n )\n return results.every((result): result is number => typeof result === 'number')\n ? results.reduce((total, result) => total + result, 0)\n : undefined\n }\n }\n if (sources.some(source => source.dispose)) {\n combined.dispose = () => {\n const errors: unknown[] = []\n for (const source of sources) {\n try {\n source.dispose?.()\n } catch (cause) {\n errors.push(cause)\n }\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) throw new PluginJobSourceDisposeError(errors)\n }\n }\n return combined\n}\n\nclass PluginJobSourceDisposeError extends Error {\n readonly cause: unknown\n readonly errors: readonly unknown[]\n\n constructor(errors: readonly unknown[]) {\n super('Failed to dispose plugin job sources')\n this.name = 'PluginJobSourceDisposeError'\n this.cause = errors[0]\n this.errors = Object.freeze([...errors])\n }\n}\n\nfunction duplicatePluginJobId(jobId: string): Error {\n return new Error(`Duplicate plugin job id across combined sources: ${jobId}`)\n}\n\nexport interface UseJobCenterOptions {\n source?: MaybeRefOrGetter<PluginJobCenterSource | undefined>\n loadOnMount?: boolean\n}\n\nexport interface UseJobCenterReturn {\n jobs: ComputedRef<JobState[]>\n activeJobs: ComputedRef<JobState[]>\n historyJobs: ComputedRef<JobState[]>\n deletableJobs: ComputedRef<JobState[]>\n featuredJob: ComputedRef<JobState | null>\n canRefresh: ComputedRef<boolean>\n canCancel: ComputedRef<boolean>\n canDelete: ComputedRef<boolean>\n canClearFinished: ComputedRef<boolean>\n isOpen: Ref<boolean>\n activeTab: Ref<JobsStatusTrayTab>\n isLoading: Ref<boolean>\n error: ShallowRef<string | null>\n pendingJobIds: Ref<Set<string>>\n open: () => void\n close: () => void\n toggle: () => void\n refresh: () => Promise<boolean>\n cancelJob: (job: JobState) => Promise<boolean>\n deleteJob: (job: JobState) => Promise<boolean>\n clearFinished: () => Promise<boolean>\n isPending: (job: JobState) => boolean\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error ? error.message : fallback\n}\n\n/**\n * Headless controller for plugin job presentation and actions.\n *\n * Every job list is derived directly from the source. The controller never\n * mirrors source jobs into another ref.\n */\nexport function useJobCenter(\n options: UseJobCenterOptions = {},\n): UseJobCenterReturn {\n const isOpen = ref(false)\n const activeTab = ref<JobsStatusTrayTab>('active')\n const isLoading = ref(false)\n const error = shallowRef<string | null>(null)\n const pendingJobIds = ref(new Set<string>())\n let requestGeneration = 0\n let active = true\n const source = computed(() => toValue(options.source))\n\n const jobs = computed<JobState[]>(() => {\n const currentSource = source.value\n return currentSource ? [...toValue(currentSource.jobs)].map(normalizeJobState) : []\n })\n const activeJobs = computed(() => jobs.value.filter(job => isActiveJobStatus(job.status)))\n const historyJobs = computed(() => jobs.value.filter(job => !isActiveJobStatus(job.status)))\n const deletableJobs = computed(() => historyJobs.value.filter(job => job.can_delete))\n const featuredJob = computed(() => activeJobs.value[0] ?? historyJobs.value[0] ?? null)\n const canRefresh = computed(() => Boolean(source.value?.refresh))\n const canCancel = computed(() => Boolean(source.value?.cancel))\n const canDelete = computed(() => Boolean(source.value?.delete))\n const canClearFinished = computed(() => Boolean(\n source.value?.clear || source.value?.delete,\n ))\n\n function setPending(jobId: string, pending: boolean): void {\n const next = new Set(pendingJobIds.value)\n if (pending) next.add(jobId)\n else next.delete(jobId)\n pendingJobIds.value = next\n }\n\n async function refresh(): Promise<boolean> {\n const listJobs = source.value?.refresh\n if (!listJobs) return false\n const generation = ++requestGeneration\n isLoading.value = true\n error.value = null\n try {\n await listJobs()\n return active && generation === requestGeneration\n } catch (cause) {\n if (active && generation === requestGeneration) {\n error.value = errorMessage(cause, 'Failed to load jobs')\n }\n return false\n } finally {\n if (active && generation === requestGeneration) isLoading.value = false\n }\n }\n\n async function cancelJob(job: JobState): Promise<boolean> {\n const sourceCancel = source.value?.cancel\n if (!job.can_cancel || !sourceCancel || pendingJobIds.value.has(job.job_id)) {\n return false\n }\n setPending(job.job_id, true)\n error.value = null\n try {\n await sourceCancel(job.job_id)\n return active\n } catch (cause) {\n if (active) error.value = errorMessage(cause, 'Failed to cancel job')\n return false\n } finally {\n if (active) setPending(job.job_id, false)\n }\n }\n\n async function deleteJob(job: JobState): Promise<boolean> {\n const sourceDelete = source.value?.delete\n if (!job.can_delete || !sourceDelete || pendingJobIds.value.has(job.job_id)) {\n return false\n }\n setPending(job.job_id, true)\n error.value = null\n try {\n const deleted = await sourceDelete(job.job_id)\n return active && deleted !== false\n } catch (cause) {\n if (active) error.value = errorMessage(cause, 'Failed to delete job')\n return false\n } finally {\n if (active) setPending(job.job_id, false)\n }\n }\n\n async function clearFinished(): Promise<boolean> {\n const targets = deletableJobs.value\n if (targets.length === 0) return false\n error.value = null\n try {\n const currentSource = source.value\n if (currentSource?.clear) {\n const deleted = await currentSource.clear()\n if (!active) return false\n return typeof deleted !== 'number' || deleted === targets.length\n }\n if (!currentSource?.delete) return false\n const results = await Promise.all(targets.map(job => deleteJob(job)))\n return results.every(Boolean)\n } catch (cause) {\n if (active) error.value = errorMessage(cause, 'Failed to clear jobs')\n return false\n }\n }\n\n onMounted(() => {\n if (options.loadOnMount !== false) void refresh()\n })\n onUnmounted(() => {\n active = false\n requestGeneration += 1\n })\n\n return {\n jobs,\n activeJobs,\n historyJobs,\n deletableJobs,\n featuredJob,\n canRefresh,\n canCancel,\n canDelete,\n canClearFinished,\n isOpen,\n activeTab,\n isLoading,\n error,\n pendingJobIds,\n open: () => { isOpen.value = true },\n close: () => { isOpen.value = false },\n toggle: () => { isOpen.value = !isOpen.value },\n refresh,\n cancelJob,\n deleteJob,\n clearFinished,\n isPending: job => pendingJobIds.value.has(job.job_id),\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Floating, source-driven job status tray for plugin and platform workflows.\n *\n * @example Recommended\n * ```vue\n * <JobsStatusTray :source=\"jobs\" />\n * ```\n */\nimport { computed, nextTick, ref, toRef, useId } from 'vue'\n\nimport {\n jobStatusLabel,\n normalizeJobPercent,\n} from '../jobs'\nimport {\n useJobCenter,\n type PluginJobCenterSource,\n} from '../composables/useJobCenter'\nimport type { JobState } from '../types/jobs'\n\ninterface Props {\n source?: PluginJobCenterSource\n loadOnMount?: boolean\n title?: string\n teleportTo?: string | HTMLElement | false\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n loadOnMount: true,\n title: 'Analysis jobs',\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n select: [job: JobState]\n error: [message: string]\n}>()\n\ndefineSlots<{\n /** Extra per-row actions, rendered before the built-in Cancel / Delete. */\n 'job-actions'?: (props: { job: JobState }) => unknown\n}>()\n\nconst panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`\nconst titleId = `${panelId}-title`\nconst triggerRef = ref<HTMLButtonElement | null>(null)\nconst panelRef = ref<HTMLElement | null>(null)\nconst teleportTarget = computed(() => props.teleportTo || 'body')\nconst tray = useJobCenter({\n source: toRef(props, 'source'),\n loadOnMount: props.loadOnMount,\n})\n// 2f: a job waiting for input is the only one that needs a person — pin it first.\nconst activeRows = computed(() => [\n ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),\n ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),\n])\nconst subtitle = computed(() => {\n const count = (status: JobState['status']) => tray.activeJobs.value.filter(job => job.status === status).length\n const parts = [\n [count('running'), 'running'],\n [count('queued'), 'queued'],\n [count('awaiting_input'), 'waiting'],\n ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)\n return parts.length ? parts.join(' · ') : `${tray.jobs.value.length} total`\n})\nconst canClear = computed(() => (\n tray.deletableJobs.value.length > 0\n && tray.canClearFinished.value\n))\n\nasync function openTray(): Promise<void> {\n tray.open()\n await nextTick()\n panelRef.value?.focus()\n}\n\nasync function closeTray(): Promise<void> {\n tray.close()\n await nextTick()\n triggerRef.value?.focus()\n}\n\nasync function toggleTray(): Promise<void> {\n if (tray.isOpen.value) await closeTray()\n else await openTray()\n}\n\nfunction onPanelKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') {\n void closeTray()\n return\n }\n if (event.key !== 'Tab' || !panelRef.value) return\n const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n ))\n if (focusable.length === 0) {\n event.preventDefault()\n panelRef.value.focus()\n return\n }\n const first = focusable[0]\n const last = focusable[focusable.length - 1]\n const activeElement = document.activeElement as HTMLElement | null\n if (!activeElement || !focusable.includes(activeElement)) {\n event.preventDefault()\n if (event.shiftKey) last?.focus()\n else first?.focus()\n } else if (event.shiftKey && activeElement === first) {\n event.preventDefault()\n last?.focus()\n } else if (!event.shiftKey && activeElement === last) {\n event.preventDefault()\n first?.focus()\n }\n}\n\nfunction progressText(job: JobState): string {\n if (job.status === 'awaiting_input') {\n return job.progress.message || 'Waiting for external input'\n }\n if (job.status === 'queued' && job.queue_position > 0) {\n return `Queue position ${job.queue_position}`\n }\n return job.progress.message || job.progress.stage || jobStatusLabel(job.status)\n}\n\nfunction formatTimestamp(value: string | null): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime())\n ? ''\n : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)\n}\n\nfunction statusClass(job: JobState): string {\n return `mint-jobs-tray__status--${job.status.replace('_', '-')}`\n}\n\nfunction jobPercent(job: JobState): number {\n return Math.round(normalizeJobPercent(job.progress.percent))\n}\n\nasync function cancelJob(job: JobState): Promise<void> {\n const succeeded = await tray.cancelJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function deleteJob(job: JobState): Promise<void> {\n const succeeded = await tray.deleteJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function clearFinished(): Promise<void> {\n const succeeded = await tray.clearFinished()\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTarget\" :disabled=\"teleportTo === false\">\n <div class=\"mint-jobs-tray\">\n <button\n ref=\"triggerRef\"\n type=\"button\"\n class=\"mint-jobs-tray__trigger\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"tray.isOpen.value\"\n :aria-controls=\"panelId\"\n @click=\"toggleTray\"\n >\n <svg\n class=\"mint-jobs-tray__trigger-icon\"\n :class=\"{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path d=\"M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2\" />\n <path d=\"M9 5a3 3 0 0 1 6 0v2H9V5Z\" />\n </svg>\n <span class=\"mint-jobs-tray__trigger-label\">\n {{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}\n </span>\n <span\n v-if=\"tray.activeJobs.value.length > 0\"\n class=\"mint-jobs-tray__count\"\n aria-live=\"polite\"\n >\n {{ tray.activeJobs.value.length }}\n </span>\n </button>\n\n <Transition name=\"mint-jobs-tray-fade\">\n <div\n v-if=\"tray.isOpen.value\"\n class=\"mint-jobs-tray__backdrop\"\n aria-hidden=\"true\"\n @click=\"closeTray\"\n />\n </Transition>\n\n <Transition name=\"mint-jobs-tray-slide\">\n <section\n v-if=\"tray.isOpen.value\"\n :id=\"panelId\"\n ref=\"panelRef\"\n class=\"mint-jobs-tray__panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"titleId\"\n tabindex=\"-1\"\n @keydown=\"onPanelKeydown\"\n >\n <header class=\"mint-jobs-tray__header\">\n <div>\n <h2 :id=\"titleId\" class=\"mint-jobs-tray__title\">{{ title }}</h2>\n <p class=\"mint-jobs-tray__subtitle\" :title=\"subtitle\">{{ subtitle }}</p>\n </div>\n <div class=\"mint-jobs-tray__header-actions\">\n <button\n v-if=\"tray.canRefresh.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Refresh jobs\"\n :disabled=\"tray.isLoading.value\"\n @click=\"tray.refresh\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M20 6v5h-5M4 18v-5h5\" />\n <path d=\"M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Close jobs\"\n @click=\"closeTray\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m6 6 12 12M18 6 6 18\" />\n </svg>\n </button>\n </div>\n </header>\n\n <div class=\"mint-jobs-tray__tabs\" role=\"tablist\" aria-label=\"Job groups\">\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"active\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'active' }\"\n :aria-selected=\"tray.activeTab.value === 'active'\"\n @click=\"tray.activeTab.value = 'active'\"\n >\n Active\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.activeJobs.value.length }}</span>\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"history\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'history' }\"\n :aria-selected=\"tray.activeTab.value === 'history'\"\n @click=\"tray.activeTab.value = 'history'\"\n >\n History\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.historyJobs.value.length }}</span>\n </button>\n </div>\n\n <p v-if=\"tray.error.value\" class=\"mint-jobs-tray__error\" role=\"alert\">\n {{ tray.error.value }}\n </p>\n\n <div class=\"mint-jobs-tray__body\">\n <template v-if=\"tray.activeTab.value === 'active'\">\n <p v-if=\"tray.activeJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No active jobs\n </p>\n <article\n v-for=\"job in activeRows\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n :class=\"{ 'mint-jobs-tray__job--awaiting-input': job.status === 'awaiting_input' }\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n <span v-if=\"job.status === 'running'\" class=\"mint-jobs-tray__percent\">{{ jobPercent(job) }}%</span>\n </div>\n <div\n v-if=\"job.status === 'running'\"\n class=\"mint-jobs-tray__progress\"\n role=\"progressbar\"\n aria-label=\"Job progress\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"jobPercent(job)\"\n >\n <span :style=\"{ width: `${jobPercent(job)}%` }\" />\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ progressText(job) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_cancel && tray.canCancel.value\"\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n aria-label=\"Cancel job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"cancelJob(job)\"\n >\n Cancel\n </button>\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n\n <template v-else>\n <div v-if=\"canClear\" class=\"mint-jobs-tray__clear-row\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n @click=\"clearFinished\"\n >\n Clear finished\n </button>\n </div>\n <p v-if=\"tray.historyJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No job history\n </p>\n <article\n v-for=\"job in tray.historyJobs.value\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n </div>\n </section>\n </Transition>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Floating, source-driven job status tray for plugin and platform workflows.\n *\n * @example Recommended\n * ```vue\n * <JobsStatusTray :source=\"jobs\" />\n * ```\n */\nimport { computed, nextTick, ref, toRef, useId } from 'vue'\n\nimport {\n jobStatusLabel,\n normalizeJobPercent,\n} from '../jobs'\nimport {\n useJobCenter,\n type PluginJobCenterSource,\n} from '../composables/useJobCenter'\nimport type { JobState } from '../types/jobs'\n\ninterface Props {\n source?: PluginJobCenterSource\n loadOnMount?: boolean\n title?: string\n teleportTo?: string | HTMLElement | false\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n loadOnMount: true,\n title: 'Analysis jobs',\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n select: [job: JobState]\n error: [message: string]\n}>()\n\ndefineSlots<{\n /** Extra per-row actions, rendered before the built-in Cancel / Delete. */\n 'job-actions'?: (props: { job: JobState }) => unknown\n}>()\n\nconst panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`\nconst titleId = `${panelId}-title`\nconst triggerRef = ref<HTMLButtonElement | null>(null)\nconst panelRef = ref<HTMLElement | null>(null)\nconst teleportTarget = computed(() => props.teleportTo || 'body')\nconst tray = useJobCenter({\n source: toRef(props, 'source'),\n loadOnMount: props.loadOnMount,\n})\n// 2f: a job waiting for input is the only one that needs a person — pin it first.\nconst activeRows = computed(() => [\n ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),\n ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),\n])\nconst subtitle = computed(() => {\n const count = (status: JobState['status']) => tray.activeJobs.value.filter(job => job.status === status).length\n const parts = [\n [count('running'), 'running'],\n [count('queued'), 'queued'],\n [count('awaiting_input'), 'waiting'],\n ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)\n return parts.length ? parts.join(' · ') : `${tray.jobs.value.length} total`\n})\nconst canClear = computed(() => (\n tray.deletableJobs.value.length > 0\n && tray.canClearFinished.value\n))\n\nasync function openTray(): Promise<void> {\n tray.open()\n await nextTick()\n panelRef.value?.focus()\n}\n\nasync function closeTray(): Promise<void> {\n tray.close()\n await nextTick()\n triggerRef.value?.focus()\n}\n\nasync function toggleTray(): Promise<void> {\n if (tray.isOpen.value) await closeTray()\n else await openTray()\n}\n\nfunction onPanelKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') {\n void closeTray()\n return\n }\n if (event.key !== 'Tab' || !panelRef.value) return\n const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(\n 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n ))\n if (focusable.length === 0) {\n event.preventDefault()\n panelRef.value.focus()\n return\n }\n const first = focusable[0]\n const last = focusable[focusable.length - 1]\n const activeElement = document.activeElement as HTMLElement | null\n if (!activeElement || !focusable.includes(activeElement)) {\n event.preventDefault()\n if (event.shiftKey) last?.focus()\n else first?.focus()\n } else if (event.shiftKey && activeElement === first) {\n event.preventDefault()\n last?.focus()\n } else if (!event.shiftKey && activeElement === last) {\n event.preventDefault()\n first?.focus()\n }\n}\n\nfunction progressText(job: JobState): string {\n if (job.status === 'awaiting_input') {\n return job.progress.message || 'Waiting for external input'\n }\n if (job.status === 'queued' && job.queue_position > 0) {\n return `Queue position ${job.queue_position}`\n }\n return job.progress.message || job.progress.stage || jobStatusLabel(job.status)\n}\n\nfunction formatTimestamp(value: string | null): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime())\n ? ''\n : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)\n}\n\nfunction statusClass(job: JobState): string {\n return `mint-jobs-tray__status--${job.status.replace('_', '-')}`\n}\n\nfunction jobPercent(job: JobState): number {\n return Math.round(normalizeJobPercent(job.progress.percent))\n}\n\nasync function cancelJob(job: JobState): Promise<void> {\n const succeeded = await tray.cancelJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function deleteJob(job: JobState): Promise<void> {\n const succeeded = await tray.deleteJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function clearFinished(): Promise<void> {\n const succeeded = await tray.clearFinished()\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTarget\" :disabled=\"teleportTo === false\">\n <div class=\"mint-jobs-tray\">\n <button\n ref=\"triggerRef\"\n type=\"button\"\n class=\"mint-jobs-tray__trigger\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"tray.isOpen.value\"\n :aria-controls=\"panelId\"\n @click=\"toggleTray\"\n >\n <svg\n class=\"mint-jobs-tray__trigger-icon\"\n :class=\"{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path d=\"M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2\" />\n <path d=\"M9 5a3 3 0 0 1 6 0v2H9V5Z\" />\n </svg>\n <span class=\"mint-jobs-tray__trigger-label\">\n {{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}\n </span>\n <span\n v-if=\"tray.activeJobs.value.length > 0\"\n class=\"mint-jobs-tray__count\"\n aria-live=\"polite\"\n >\n {{ tray.activeJobs.value.length }}\n </span>\n </button>\n\n <Transition name=\"mint-jobs-tray-fade\">\n <div\n v-if=\"tray.isOpen.value\"\n class=\"mint-jobs-tray__backdrop\"\n aria-hidden=\"true\"\n @click=\"closeTray\"\n />\n </Transition>\n\n <Transition name=\"mint-jobs-tray-slide\">\n <section\n v-if=\"tray.isOpen.value\"\n :id=\"panelId\"\n ref=\"panelRef\"\n class=\"mint-jobs-tray__panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-labelledby=\"titleId\"\n tabindex=\"-1\"\n @keydown=\"onPanelKeydown\"\n >\n <header class=\"mint-jobs-tray__header\">\n <div>\n <h2 :id=\"titleId\" class=\"mint-jobs-tray__title\">{{ title }}</h2>\n <p class=\"mint-jobs-tray__subtitle\" :title=\"subtitle\">{{ subtitle }}</p>\n </div>\n <div class=\"mint-jobs-tray__header-actions\">\n <button\n v-if=\"tray.canRefresh.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Refresh jobs\"\n :disabled=\"tray.isLoading.value\"\n @click=\"tray.refresh\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M20 6v5h-5M4 18v-5h5\" />\n <path d=\"M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Close jobs\"\n @click=\"closeTray\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m6 6 12 12M18 6 6 18\" />\n </svg>\n </button>\n </div>\n </header>\n\n <div class=\"mint-jobs-tray__tabs\" role=\"tablist\" aria-label=\"Job groups\">\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"active\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'active' }\"\n :aria-selected=\"tray.activeTab.value === 'active'\"\n @click=\"tray.activeTab.value = 'active'\"\n >\n Active\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.activeJobs.value.length }}</span>\n </button>\n <button\n type=\"button\"\n role=\"tab\"\n data-tab=\"history\"\n class=\"mint-jobs-tray__tab\"\n :class=\"{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'history' }\"\n :aria-selected=\"tray.activeTab.value === 'history'\"\n @click=\"tray.activeTab.value = 'history'\"\n >\n History\n <span class=\"mint-jobs-tray__tab-count\">{{ tray.historyJobs.value.length }}</span>\n </button>\n </div>\n\n <p v-if=\"tray.error.value\" class=\"mint-jobs-tray__error\" role=\"alert\">\n {{ tray.error.value }}\n </p>\n\n <div class=\"mint-jobs-tray__body\">\n <template v-if=\"tray.activeTab.value === 'active'\">\n <p v-if=\"tray.activeJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No active jobs\n </p>\n <article\n v-for=\"job in activeRows\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n :class=\"{ 'mint-jobs-tray__job--awaiting-input': job.status === 'awaiting_input' }\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n <span v-if=\"job.status === 'running'\" class=\"mint-jobs-tray__percent\">{{ jobPercent(job) }}%</span>\n </div>\n <div\n v-if=\"job.status === 'running'\"\n class=\"mint-jobs-tray__progress\"\n role=\"progressbar\"\n aria-label=\"Job progress\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"jobPercent(job)\"\n >\n <span :style=\"{ width: `${jobPercent(job)}%` }\" />\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ progressText(job) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_cancel && tray.canCancel.value\"\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n aria-label=\"Cancel job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"cancelJob(job)\"\n >\n Cancel\n </button>\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n\n <template v-else>\n <div v-if=\"canClear\" class=\"mint-jobs-tray__clear-row\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__action mint-jobs-tray__action--danger\"\n @click=\"clearFinished\"\n >\n Clear finished\n </button>\n </div>\n <p v-if=\"tray.historyJobs.value.length === 0\" class=\"mint-jobs-tray__empty\">\n No job history\n </p>\n <article\n v-for=\"job in tray.historyJobs.value\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__job\"\n >\n <div class=\"mint-jobs-tray__job-heading\">\n <div class=\"mint-jobs-tray__job-identity\">\n <button\n type=\"button\"\n class=\"mint-jobs-tray__job-select\"\n @click=\"emit('select', job)\"\n >\n {{ job.name }}\n </button>\n <span class=\"mint-jobs-tray__kind\">{{ job.kind }}</span>\n </div>\n <span class=\"mint-jobs-tray__status\" :class=\"statusClass(job)\">\n <span class=\"mint-jobs-tray__status-dot\" aria-hidden=\"true\" />\n {{ jobStatusLabel(job.status) }}\n </span>\n </div>\n <div class=\"mint-jobs-tray__job-footer\">\n <span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>\n <div class=\"mint-jobs-tray__job-actions\">\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n aria-label=\"Delete job\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5\" />\n </svg>\n </button>\n </div>\n </div>\n </article>\n </template>\n </div>\n </section>\n </Transition>\n </div>\n </Teleport>\n</template>\n","import {\n getCurrentScope,\n onScopeDispose,\n readonly,\n ref,\n toValue,\n watch,\n type MaybeRefOrGetter,\n type Ref,\n} from 'vue'\n\n/**\n * One event of the platform's realtime stream (`GET /api/events`).\n *\n * Platform events carry no entity data: refetch the entity through REST.\n * `type` is `resync` (refetch everything you show), the audit action of a\n * committed write, such as `experiment.update` or `analysis.artifact_create`,\n * or the `\"<plugin>.<event>\"` name of a plugin event, which carries `payload`.\n * Only `usePresence` receives `presence.changed`.\n */\nexport interface RealtimeEvent {\n type: string\n id?: string\n /**\n * `experiment:{id}`, `project:{id}`, `experiments`, `user:{id}`, or a plugin's\n * `plugin:{name}` / `plugin:{name}:experiment:{id}`; absent on `resync`.\n */\n topic?: string\n /** A plugin event: the JSON payload the plugin published (at most 16 KiB). */\n payload?: unknown\n /** Changed fields of an update, when the write names them. */\n fields?: string[] | null\n actor_id?: string | null\n request_id?: string | null\n at?: string\n /** `presence.changed`: the topic's viewers (at most 20, `editing` first). */\n viewers?: { user_id: string, activity: string }[]\n /** `presence.changed`: the number of viewers, also those beyond `viewers`. */\n count?: number\n}\n\nexport type RealtimeHandler = (event: RealtimeEvent) => void\n\nexport interface UseRealtimeReturn {\n /** True while the shared stream is open. */\n connected: Readonly<Ref<boolean>>\n /** Unsubscribe; also runs when the calling component or effect scope is disposed. */\n stop: () => void\n}\n\nconst EVENTS_URL = '/api/events'\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\n/** Merges the subscriptions of components that mount together into one connection. */\nconst TOPIC_DEBOUNCE_MS = 50\nconst RESYNC: RealtimeEvent = { type: 'resync' }\n\ninterface Listener {\n topics: () => string[]\n handler: RealtimeHandler\n /** Receives `presence.changed`; a write listener would refetch on each viewer change. */\n presence: boolean\n}\n\nconst listeners = new Set<Listener>()\n/** The last `presence.changed` of each topic of the open stream; the server sends one per topic at open. */\nconst presence = new Map<string, RealtimeEvent>()\nconst connected = ref(false)\nlet source: EventSource | null = null\nlet sourceTopics = ''\nlet failures = 0\nlet syncTimer: ReturnType<typeof setTimeout> | null = null\nlet retryTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction dispatch(event: RealtimeEvent): void {\n const isPresence = event.type === 'presence.changed'\n if (isPresence && event.topic) presence.set(event.topic, event)\n for (const listener of [...listeners]) {\n if (isPresence && !listener.presence) continue\n if (event.type === 'resync' || (event.topic && listener.topics().includes(event.topic))) {\n listener.handler(event)\n }\n }\n}\n\nfunction close(): void {\n source?.close()\n source = null\n sourceTopics = ''\n presence.clear()\n connected.value = false\n}\n\nfunction open(topics: string): void {\n const stream = new EventSource(`${EVENTS_URL}?topics=${encodeURIComponent(topics)}`)\n source = stream\n sourceTopics = topics\n stream.onopen = () => {\n connected.value = true\n // The server subscribes only now: a write between a caller's REST load (or\n // the close of the previous stream) and this point sent no event, so every caller refetches.\n dispatch(RESYNC)\n failures = 0\n }\n stream.onmessage = (message) => {\n dispatch(JSON.parse(message.data) as RealtimeEvent)\n }\n stream.onerror = () => {\n // EventSource retries a dropped connection itself but gives up after an\n // HTTP error, so one backoff path handles both.\n if (source !== stream) return\n close()\n // A topic change during the wait does not shorten it: the retry reads the topics when it runs.\n if (syncTimer !== null) clearTimeout(syncTimer)\n syncTimer = null\n retryTimer = setTimeout(() => {\n retryTimer = null\n sync()\n }, Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** failures))\n failures += 1\n }\n}\n\nfunction sync(): void {\n syncTimer = null\n const topics = [...new Set([...listeners].flatMap(listener => listener.topics()))].sort().join(',')\n if (source && topics === sourceTopics) return\n close()\n if (topics) open(topics)\n}\n\nfunction schedule(): void {\n if (retryTimer !== null) return\n if (syncTimer !== null) clearTimeout(syncTimer)\n syncTimer = setTimeout(sync, TOPIC_DEBOUNCE_MS)\n}\n\n/**\n * Subscribe to topics of the platform's realtime stream.\n *\n * Every caller in a tab shares one `EventSource`, opened on the union of\n * their topics and reopened when that union changes. `handler` receives the\n * write events of the caller's own topics (not `presence.changed`, which goes\n * to `usePresence`) plus every `resync`: one comes from the\n * server when this tab fell behind, and one follows each open of the stream:\n * the first open, each reconnect (backoff from 1 s to 30 s) and each reopen for\n * a changed topic union. On `resync`, refetch. The union of all callers' topics\n * in a tab may hold at most 50 topics; the platform refuses a larger stream.\n *\n * The stream authenticates with the session cookie, so it works in the\n * platform and in plugin frontends served by it. It exists only when the\n * platform's `realtime` feature flag is on; check that flag first.\n *\n * A plugin topic reaches a caller whose role may use the plugin; an\n * experiment plugin topic also needs a caller who may view the experiment.\n *\n * @example\n * useRealtime(() => [`experiment:${id.value}`], () => refetch())\n * @example\n * useRealtime(['plugin:drp'], (event) => {\n * if (event.type === 'drp.run.progress') progress.value = event.payload as RunProgress\n * })\n */\nexport function useRealtime(\n topics: MaybeRefOrGetter<string[]>,\n handler: RealtimeHandler,\n): UseRealtimeReturn {\n return subscribeRealtime(topics, handler, false)\n}\n\n/** The last `presence.changed` of `topic` while the open stream views it (for `usePresence`). */\nexport function currentPresence(topic: string): RealtimeEvent | undefined {\n return presence.get(topic)\n}\n\n/** `useRealtime`, and with `withPresence` also the `presence.changed` events (for `usePresence`). */\nexport function subscribeRealtime(\n topics: MaybeRefOrGetter<string[]>,\n handler: RealtimeHandler,\n withPresence: boolean,\n): UseRealtimeReturn {\n const listener: Listener = { topics: () => toValue(topics), handler, presence: withPresence }\n listeners.add(listener)\n const stopWatch = watch(() => listener.topics().join(','), () => schedule())\n schedule()\n\n function stop(): void {\n stopWatch()\n if (listeners.delete(listener)) schedule()\n }\n\n if (getCurrentScope()) onScopeDispose(stop)\n return { connected: readonly(connected), stop }\n}\n"],"mappings":";;;;;;;;;;AAgDA,SAAgB,wBACd,SAC+B;CAC/B,MAAM,OAAO,eAAyC;EACpD,MAAM,WAA4B,CAAC;EACnC,MAAM,yBAAS,IAAI,IAAY;EAC/B,KAAK,MAAM,UAAU,SACnB,KAAK,MAAM,OAAO,QAAQ,OAAO,IAAI,GAAG;GACtC,IAAI,OAAO,IAAI,IAAI,MAAM,GAAG,MAAM,qBAAqB,IAAI,MAAM;GACjE,OAAO,IAAI,IAAI,MAAM;GACrB,SAAS,KAAK,GAAG;EACnB;EAEF,OAAO;CACT,CAAC;CAED,SAAS,SAAS,OAAsC;EACtD,IAAI;EACJ,KAAK,MAAM,UAAU,SAAS;GAC5B,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,CAAC,MAAK,QAAO,IAAI,WAAW,KAAK,GAAG;GAC7D,IAAI,OAAO,MAAM,qBAAqB,KAAK;GAC3C,QAAQ;EACV;EACA,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,0BAA0B,OAAO;EAC7D,OAAO;CACT;CAEA,MAAM,WAA0C,EAAE,KAAK;CACvD,IAAI,QAAQ,MAAK,WAAU,OAAO,OAAO,GACvC,SAAS,UAAU,YAAY;EAC7B,MAAM,QAAQ,IAAI,QAAQ,KAAI,WAAU,OAAO,UAAU,CAAC,CAAC;EAC3D,OAAO,KAAK;CACd;CAEF,IAAI,QAAQ,MAAK,WAAU,OAAO,MAAM,GACtC,SAAS,SAAS,OAAO,UAAU;EACjC,MAAM,QAAQ,SAAS,KAAK;EAC5B,IAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,OAAO,MAAM,mCAAmC;EACnF,OAAO,MAAM,OAAO,KAAK;CAC3B;CAEF,IAAI,QAAQ,SAAS,KAAK,QAAQ,OAAM,WAAU,OAAO,MAAM,GAC7D,SAAS,SAAS,OAAO,UAAU;EACjC,MAAM,QAAQ,SAAS,KAAK;EAC5B,IAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,OAAO,MAAM,iCAAiC;EACjF,OAAO,MAAM,OAAO,KAAK;CAC3B;CAKF,IAAI,QAAQ,SAAS,KAAK,QAAQ,OAAM,WAAU,OAAO,KAAK,GAC5D,SAAS,QAAQ,YAAY;EAC3B,MAAM,UAAU,MAAM,QAAQ,IAC5B,QAAQ,KAAI,WAAU,OAAO,MAAO,CAAC,CACvC;EACA,OAAO,QAAQ,OAAO,WAA6B,OAAO,WAAW,QAAQ,IACzE,QAAQ,QAAQ,OAAO,WAAW,QAAQ,QAAQ,CAAC,IACnD,KAAA;CACN;CAEF,IAAI,QAAQ,MAAK,WAAU,OAAO,OAAO,GACvC,SAAS,gBAAgB;EACvB,MAAM,SAAoB,CAAC;EAC3B,KAAK,MAAM,UAAU,SACnB,IAAI;GACF,OAAO,UAAU;EACnB,SAAS,OAAO;GACd,OAAO,KAAK,KAAK;EACnB;EAEF,IAAI,OAAO,WAAW,GAAG,MAAM,OAAO;EACtC,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,4BAA4B,MAAM;CACrE;CAEF,OAAO;AACT;AAEA,IAAM,8BAAN,cAA0C,MAAM;CAC9C;CACA;CAEA,YAAY,QAA4B;EACtC,MAAM,sCAAsC;EAC5C,KAAK,OAAO;EACZ,KAAK,QAAQ,OAAO;EACpB,KAAK,SAAS,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC;CACzC;AACF;AAEA,SAAS,qBAAqB,OAAsB;CAClD,uBAAO,IAAI,MAAM,oDAAoD,OAAO;AAC9E;AAgCA,SAAS,aAAa,OAAgB,UAA0B;CAC9D,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAClD;;;;;;;AAQA,SAAgB,aACd,UAA+B,CAAC,GACZ;CACpB,MAAM,SAAS,IAAI,KAAK;CACxB,MAAM,YAAY,IAAuB,QAAQ;CACjD,MAAM,YAAY,IAAI,KAAK;CAC3B,MAAM,QAAQ,WAA0B,IAAI;CAC5C,MAAM,gBAAgB,oBAAI,IAAI,IAAY,CAAC;CAC3C,IAAI,oBAAoB;CACxB,IAAI,SAAS;CACb,MAAM,SAAS,eAAe,QAAQ,QAAQ,MAAM,CAAC;CAErD,MAAM,OAAO,eAA2B;EACtC,MAAM,gBAAgB,OAAO;EAC7B,OAAO,gBAAgB,CAAC,GAAG,QAAQ,cAAc,IAAI,CAAC,CAAC,CAAC,IAAI,iBAAiB,IAAI,CAAC;CACpF,CAAC;CACD,MAAM,aAAa,eAAe,KAAK,MAAM,QAAO,QAAO,kBAAkB,IAAI,MAAM,CAAC,CAAC;CACzF,MAAM,cAAc,eAAe,KAAK,MAAM,QAAO,QAAO,CAAC,kBAAkB,IAAI,MAAM,CAAC,CAAC;CAC3F,MAAM,gBAAgB,eAAe,YAAY,MAAM,QAAO,QAAO,IAAI,UAAU,CAAC;CACpF,MAAM,cAAc,eAAe,WAAW,MAAM,MAAM,YAAY,MAAM,MAAM,IAAI;CACtF,MAAM,aAAa,eAAe,QAAQ,OAAO,OAAO,OAAO,CAAC;CAChE,MAAM,YAAY,eAAe,QAAQ,OAAO,OAAO,MAAM,CAAC;CAC9D,MAAM,YAAY,eAAe,QAAQ,OAAO,OAAO,MAAM,CAAC;CAC9D,MAAM,mBAAmB,eAAe,QACtC,OAAO,OAAO,SAAS,OAAO,OAAO,MACvC,CAAC;CAED,SAAS,WAAW,OAAe,SAAwB;EACzD,MAAM,OAAO,IAAI,IAAI,cAAc,KAAK;EACxC,IAAI,SAAS,KAAK,IAAI,KAAK;OACtB,KAAK,OAAO,KAAK;EACtB,cAAc,QAAQ;CACxB;CAEA,eAAe,UAA4B;EACzC,MAAM,WAAW,OAAO,OAAO;EAC/B,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,aAAa,EAAE;EACrB,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,IAAI;GACF,MAAM,SAAS;GACf,OAAO,UAAU,eAAe;EAClC,SAAS,OAAO;GACd,IAAI,UAAU,eAAe,mBAC3B,MAAM,QAAQ,aAAa,OAAO,qBAAqB;GAEzD,OAAO;EACT,UAAU;GACR,IAAI,UAAU,eAAe,mBAAmB,UAAU,QAAQ;EACpE;CACF;CAEA,eAAe,UAAU,KAAiC;EACxD,MAAM,eAAe,OAAO,OAAO;EACnC,IAAI,CAAC,IAAI,cAAc,CAAC,gBAAgB,cAAc,MAAM,IAAI,IAAI,MAAM,GACxE,OAAO;EAET,WAAW,IAAI,QAAQ,IAAI;EAC3B,MAAM,QAAQ;EACd,IAAI;GACF,MAAM,aAAa,IAAI,MAAM;GAC7B,OAAO;EACT,SAAS,OAAO;GACd,IAAI,QAAQ,MAAM,QAAQ,aAAa,OAAO,sBAAsB;GACpE,OAAO;EACT,UAAU;GACR,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;EAC1C;CACF;CAEA,eAAe,UAAU,KAAiC;EACxD,MAAM,eAAe,OAAO,OAAO;EACnC,IAAI,CAAC,IAAI,cAAc,CAAC,gBAAgB,cAAc,MAAM,IAAI,IAAI,MAAM,GACxE,OAAO;EAET,WAAW,IAAI,QAAQ,IAAI;EAC3B,MAAM,QAAQ;EACd,IAAI;GACF,MAAM,UAAU,MAAM,aAAa,IAAI,MAAM;GAC7C,OAAO,UAAU,YAAY;EAC/B,SAAS,OAAO;GACd,IAAI,QAAQ,MAAM,QAAQ,aAAa,OAAO,sBAAsB;GACpE,OAAO;EACT,UAAU;GACR,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;EAC1C;CACF;CAEA,eAAe,gBAAkC;EAC/C,MAAM,UAAU,cAAc;EAC9B,IAAI,QAAQ,WAAW,GAAG,OAAO;EACjC,MAAM,QAAQ;EACd,IAAI;GACF,MAAM,gBAAgB,OAAO;GAC7B,IAAI,eAAe,OAAO;IACxB,MAAM,UAAU,MAAM,cAAc,MAAM;IAC1C,IAAI,CAAC,QAAQ,OAAO;IACpB,OAAO,OAAO,YAAY,YAAY,YAAY,QAAQ;GAC5D;GACA,IAAI,CAAC,eAAe,QAAQ,OAAO;GAEnC,QAAO,MADe,QAAQ,IAAI,QAAQ,KAAI,QAAO,UAAU,GAAG,CAAC,CAAC,EAAA,CACrD,MAAM,OAAO;EAC9B,SAAS,OAAO;GACd,IAAI,QAAQ,MAAM,QAAQ,aAAa,OAAO,sBAAsB;GACpE,OAAO;EACT;CACF;CAEA,gBAAgB;EACd,IAAI,QAAQ,gBAAgB,OAAO,QAAa;CAClD,CAAC;CACD,kBAAkB;EAChB,SAAS;EACT,qBAAqB;CACvB,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YAAY;GAAE,OAAO,QAAQ;EAAK;EAClC,aAAa;GAAE,OAAO,QAAQ;EAAM;EACpC,cAAc;GAAE,OAAO,QAAQ,CAAC,OAAO;EAAM;EAC7C;EACA;EACA;EACA;EACA,YAAW,QAAO,cAAc,MAAM,IAAI,IAAI,MAAM;CACtD;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECvSA,MAAM,QAAQ;EAMd,MAAM,OAAO;EAUb,MAAM,UAAU,kBAAkB,MAAM,CAAC,CAAC,QAAQ,MAAM,EAAE;EAC1D,MAAM,UAAU,GAAG,QAAQ;EAC3B,MAAM,aAAa,IAA8B,IAAI;EACrD,MAAM,WAAW,IAAwB,IAAI;EAC7C,MAAM,iBAAiB,eAAe,MAAM,cAAc,MAAM;EAChE,MAAM,OAAO,aAAa;GACxB,QAAQ,MAAM,OAAO,QAAQ;GAC7B,aAAa,MAAM;EACrB,CAAC;EAED,MAAM,aAAa,eAAe,CAChC,GAAG,KAAK,WAAW,MAAM,QAAO,QAAO,IAAI,WAAW,gBAAgB,GACtE,GAAG,KAAK,WAAW,MAAM,QAAO,QAAO,IAAI,WAAW,gBAAgB,CACxE,CAAC;EACD,MAAM,WAAW,eAAe;GAC9B,MAAM,SAAS,WAA+B,KAAK,WAAW,MAAM,QAAO,QAAO,IAAI,WAAW,MAAM,CAAC,CAAC;GACzG,MAAM,QAAQ;IACZ,CAAC,MAAM,SAAS,GAAG,SAAS;IAC5B,CAAC,MAAM,QAAQ,GAAG,QAAQ;IAC1B,CAAC,MAAM,gBAAgB,GAAG,SAAS;GACrC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,UAAU,GAAG,EAAE,GAAG,MAAM;GACtD,OAAO,MAAM,SAAS,MAAM,KAAK,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,OAAO;EACtE,CAAC;EACD,MAAM,WAAW,eACf,KAAK,cAAc,MAAM,SAAS,KAC/B,KAAK,iBAAiB,KAC1B;EAED,eAAe,WAA0B;GACvC,KAAK,KAAK;GACV,MAAM,SAAS;GACf,SAAS,OAAO,MAAM;EACxB;EAEA,eAAe,YAA2B;GACxC,KAAK,MAAM;GACX,MAAM,SAAS;GACf,WAAW,OAAO,MAAM;EAC1B;EAEA,eAAe,aAA4B;GACzC,IAAI,KAAK,OAAO,OAAO,MAAM,UAAU;QAClC,MAAM,SAAS;EACtB;EAEA,SAAS,eAAe,OAA4B;GAClD,IAAI,MAAM,QAAQ,UAAU;IAC1B,UAAe;IACf;GACF;GACA,IAAI,MAAM,QAAQ,SAAS,CAAC,SAAS,OAAO;GAC5C,MAAM,YAAY,MAAM,KAAK,SAAS,MAAM,iBAC1C,4IACF,CAAC;GACD,IAAI,UAAU,WAAW,GAAG;IAC1B,MAAM,eAAe;IACrB,SAAS,MAAM,MAAM;IACrB;GACF;GACA,MAAM,QAAQ,UAAU;GACxB,MAAM,OAAO,UAAU,UAAU,SAAS;GAC1C,MAAM,gBAAgB,SAAS;GAC/B,IAAI,CAAC,iBAAiB,CAAC,UAAU,SAAS,aAAa,GAAG;IACxD,MAAM,eAAe;IACrB,IAAI,MAAM,UAAU,MAAM,MAAM;SAC3B,OAAO,MAAM;GACpB,OAAO,IAAI,MAAM,YAAY,kBAAkB,OAAO;IACpD,MAAM,eAAe;IACrB,MAAM,MAAM;GACd,OAAO,IAAI,CAAC,MAAM,YAAY,kBAAkB,MAAM;IACpD,MAAM,eAAe;IACrB,OAAO,MAAM;GACf;EACF;EAEA,SAAS,aAAa,KAAuB;GAC3C,IAAI,IAAI,WAAW,kBACjB,OAAO,IAAI,SAAS,WAAW;GAEjC,IAAI,IAAI,WAAW,YAAY,IAAI,iBAAiB,GAClD,OAAO,kBAAkB,IAAI;GAE/B,OAAO,IAAI,SAAS,WAAW,IAAI,SAAS,SAAS,eAAe,IAAI,MAAM;EAChF;EAEA,SAAS,gBAAgB,OAA8B;GACrD,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,OAAO,IAAI,KAAK,KAAK;GAC3B,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAC9B,KACA,IAAI,KAAK,eAAe,KAAA,GAAW;IAAE,WAAW;IAAU,WAAW;GAAQ,CAAC,CAAC,CAAC,OAAO,IAAI;EACjG;EAEA,SAAS,YAAY,KAAuB;GAC1C,OAAO,2BAA2B,IAAI,OAAO,QAAQ,KAAK,GAAG;EAC/D;EAEA,SAAS,WAAW,KAAuB;GACzC,OAAO,KAAK,MAAM,oBAAoB,IAAI,SAAS,OAAO,CAAC;EAC7D;EAEA,eAAe,UAAU,KAA8B;GAErD,IAAI,CAAC,MADmB,KAAK,UAAU,GAAG,KACxB,KAAK,MAAM,OAAO,KAAK,SAAS,KAAK,MAAM,KAAK;EACpE;EAEA,eAAe,UAAU,KAA8B;GAErD,IAAI,CAAC,MADmB,KAAK,UAAU,GAAG,KACxB,KAAK,MAAM,OAAO,KAAK,SAAS,KAAK,MAAM,KAAK;EACpE;EAEA,eAAe,gBAA+B;GAE5C,IAAI,CAAC,MADmB,KAAK,cAAc,KACzB,KAAK,MAAM,OAAO,KAAK,SAAS,KAAK,MAAM,KAAK;EACpE;;GAIE,OAAA,UAAA,GAAA,YAsPW,UAAA;IAtPA,IAAI,eAAA;IAAiB,UAAU,QAAA,eAAU;GAClD,GAAA,CAAA,mBAoPM,OApPN,YAoPM;IAnPJ,mBA8BS,UAAA;KA7BH,SAAA;KAAJ,KAAI;KACJ,MAAK;KACL,OAAM;KACN,iBAAc;KACb,iBAAe,MAAA,IAAA,CAAI,CAAC,OAAO;KAC3B,iBAAe;KACf,SAAO;;MAER,UAAA,GAAA,mBAUM,OAAA;MATJ,OAAK,eAAA,CAAC,gCAA8B,EAAA,wCACc,MAAA,IAAA,CAAI,CAAC,WAAW,MAAM,SAAM,EAAA,CAAA,CAAA;MAC9E,SAAQ;MACR,MAAK;MACL,QAAO;MACP,eAAY;KAEZ,GAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,mBAAsF,QAAA,EAAhF,GAAE,4EAA2E,GAAA,MAAA,EAAA,GACnF,mBAAsC,QAAA,EAAhC,GAAE,4BAA2B,GAAA,MAAA,EAAA,CAAA,EAAA,GAAA,CAAA;KAErC,mBAEO,QAFP,YAEO,gBADF,MAAA,IAAA,CAAI,CAAC,YAAY,QAAQ,MAAA,cAAA,CAAc,CAAC,MAAA,IAAA,CAAI,CAAC,YAAY,MAAM,MAAM,IAAA,MAAA,GAAA,CAAA;KAGlE,MAAA,IAAA,CAAI,CAAC,WAAW,MAAM,SAAM,KADpC,UAAA,GAAA,mBAMO,QANP,YAMO,gBADF,MAAA,IAAA,CAAI,CAAC,WAAW,MAAM,MAAM,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;;IAInC,YAOa,YAAA,EAPD,MAAK,sBAAqB,GAAA;KACpC,SAAA,cAKE,CAJM,MAAA,IAAA,CAAI,CAAC,OAAO,SADpB,UAAA,GAAA,mBAKE,OAAA;;MAHA,OAAM;MACN,eAAY;MACX,SAAO;;;;IAIZ,YAyMa,YAAA,EAzMD,MAAK,uBAAsB,GAAA;KACrC,SAAA,cAuMU,CAtMF,MAAA,IAAA,CAAI,CAAC,OAAO,SADpB,UAAA,GAAA,mBAuMU,WAAA;;MArMP,IAAI;MACD,SAAA;MAAJ,KAAI;MACJ,OAAM;MACN,MAAK;MACL,cAAW;MACV,mBAAiB;MAClB,UAAS;MACR,WAAS;;MAEV,mBA8BS,UA9BT,YA8BS,CA7BP,mBAGM,OAAA,MAAA,CAFJ,mBAAgE,MAAA;OAA3D,IAAI;OAAS,OAAM;MAA2B,GAAA,gBAAA,QAAA,KAAK,GAAA,CAAA,GACxD,mBAAwE,KAAA;OAArE,OAAM;OAA4B,OAAO,SAAA;MAAa,GAAA,gBAAA,SAAA,KAAQ,GAAA,GAAA,UAAA,CAAA,CAAA,GAEnE,mBAwBM,OAxBN,YAwBM,CAtBI,MAAA,IAAA,CAAI,CAAC,WAAW,SADxB,UAAA,GAAA,mBAYS,UAAA;;OAVP,MAAK;OACL,OAAM;OACN,cAAW;OACV,UAAU,MAAA,IAAA,CAAI,CAAC,UAAU;OACzB,SAAK,OAAA,OAAA,OAAA,MAAE,GAAA,SAAA,MAAA,IAAA,CAAI,CAAC,WAAL,MAAA,IAAA,CAAI,CAAC,QAAO,GAAA,IAAA;MAEpB,GAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,mBAGM,OAAA;OAHD,SAAQ;OAAY,MAAK;OAAO,QAAO;OAAe,eAAY;MACrE,GAAA,CAAA,mBAAiC,QAAA,EAA3B,GAAE,uBAAsB,CAAA,GAC9B,mBAAuE,QAAA,EAAjE,GAAE,6DAA4D,CAAA,CAAA,GAAA,EAAA,CAAA,EAAA,GAAA,GAAA,UAAA,KAAA,mBAAA,IAAA,IAAA,GAGxE,mBASS,UAAA;OARP,MAAK;OACL,OAAM;OACN,cAAW;OACV,SAAO;MAER,GAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,mBAEM,OAAA;OAFD,SAAQ;OAAY,MAAK;OAAO,QAAO;OAAe,eAAY;MACrE,GAAA,CAAA,mBAAiC,QAAA,EAA3B,GAAE,uBAAsB,CAAA,CAAA,GAAA,EAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA;MAMtC,mBAyBM,OAzBN,YAyBM,CAxBJ,mBAWS,UAAA;OAVP,MAAK;OACL,MAAK;OACL,YAAS;OACT,OAAK,eAAA,CAAC,uBAAqB,EAAA,+BACc,MAAA,IAAA,CAAI,CAAC,UAAU,UAAK,SAAA,CAAA,CAAA;OAC5D,iBAAe,MAAA,IAAA,CAAI,CAAC,UAAU,UAAK;OACnC,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,MAAA,IAAA,CAAI,CAAC,UAAU,QAAK;MAC7B,GAAA,CAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,YAEC,EAAA,IAAA,mBAAiF,QAAjF,aAAiF,gBAAtC,MAAA,IAAA,CAAI,CAAC,WAAW,MAAM,MAAM,GAAA,CAAA,CAAA,GAAA,IAAA,WAAA,GAEzE,mBAWS,UAAA;OAVP,MAAK;OACL,MAAK;OACL,YAAS;OACT,OAAK,eAAA,CAAC,uBAAqB,EAAA,+BACc,MAAA,IAAA,CAAI,CAAC,UAAU,UAAK,UAAA,CAAA,CAAA;OAC5D,iBAAe,MAAA,IAAA,CAAI,CAAC,UAAU,UAAK;OACnC,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,MAAA,IAAA,CAAI,CAAC,UAAU,QAAK;MAC7B,GAAA,CAAA,OAAA,OAAA,OAAA,KAAA,gBAAA,aAEC,EAAA,IAAA,mBAAkF,QAAlF,aAAkF,gBAAvC,MAAA,IAAA,CAAI,CAAC,YAAY,MAAM,MAAM,GAAA,CAAA,CAAA,GAAA,IAAA,WAAA,CAAA,CAAA;MAInE,MAAA,IAAA,CAAI,CAAC,MAAM,SAApB,UAAA,GAAA,mBAEI,KAFJ,aAEI,gBADC,MAAA,IAAA,CAAI,CAAC,MAAM,KAAK,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;MAGrB,mBA4HM,OA5HN,aA4HM,CA3HY,MAAA,IAAA,CAAI,CAAC,UAAU,UAAK,YAApC,UAAA,GAAA,mBAmEW,UAAA,EAAA,KAAA,EAAA,GAAA,CAlEA,MAAA,IAAA,CAAI,CAAC,WAAW,MAAM,WAAM,KAArC,UAAA,GAAA,mBAEI,KAFJ,aAA2E,kBAE3E,KAAA,mBAAA,IAAA,IAAA,IACA,UAAA,IAAA,GAAA,mBA8DU,UAAA,MAAA,WA7DM,WAAA,QAAP,QAAG;OADZ,OAAA,UAAA,GAAA,mBA8DU,WAAA;QA5DP,KAAK,IAAI;QACV,OAAK,eAAA,CAAC,uBAAqB,EAAA,uCACsB,IAAI,WAAM,iBAAA,CAAA,CAAA;;QAE3D,mBAgBM,OAhBN,aAgBM;SAfJ,mBASM,OATN,aASM,CARJ,mBAMS,UAAA;UALP,MAAK;UACL,OAAM;UACL,UAAK,WAAE,KAAI,UAAW,GAAG;SAEvB,GAAA,gBAAA,IAAI,IAAI,GAAA,GAAA,WAAA,GAEb,mBAAwD,QAAxD,aAAwD,gBAAlB,IAAI,IAAI,GAAA,CAAA,CAAA,CAAA;SAEhD,mBAGO,QAAA,EAHD,OAAK,eAAA,CAAC,0BAAiC,YAAY,GAAG,CAAA,CAAA,EAAA,GAAA,CAC1D,OAAA,OAAA,OAAA,KAAA,mBAA8D,QAAA;UAAxD,OAAM;UAA6B,eAAY;SAAS,GAAA,MAAA,EAAA,IAAA,gBAAA,MAC9D,gBAAG,MAAA,cAAA,CAAc,CAAC,IAAI,MAAM,CAAA,GAAA,CAAA,CAAA,GAAA,CAAA;SAElB,IAAI,WAAM,aAAtB,UAAA,GAAA,mBAAmG,QAAnG,aAAmG,gBAA1B,WAAW,GAAG,CAAA,IAAI,KAAC,CAAA,KAAA,mBAAA,IAAA,IAAA;;QAGtF,IAAI,WAAM,aADlB,UAAA,GAAA,mBAUM,OAAA;;SARJ,OAAM;SACN,MAAK;SACL,cAAW;SACX,iBAAc;SACd,iBAAc;SACb,iBAAe,WAAW,GAAG;QAE9B,GAAA,CAAA,mBAAkD,QAAA,EAA3C,OAAK,eAAA,EAAA,OAAA,GAAc,WAAW,GAAG,EAAA,GAAA,CAAA,EAAA,GAAA,MAAA,CAAA,CAAA,GAAA,GAAA,WAAA,KAAA,mBAAA,IAAA,IAAA;QAE1C,mBA2BM,OA3BN,aA2BM,CA1BJ,mBAAoC,QAAA,MAAA,gBAA3B,aAAa,GAAG,CAAA,GAAA,CAAA,GACzB,mBAwBM,OAxBN,aAwBM;SAvBJ,WAAsC,KAAA,QAAA,eAAA,EAAP,IAAG,CAAA;SAE1B,IAAI,cAAc,MAAA,IAAA,CAAI,CAAC,UAAU,SADzC,UAAA,GAAA,mBASS,UAAA;;UAPP,MAAK;UACL,OAAM;UACN,cAAW;UACV,UAAU,MAAA,IAAA,CAAI,CAAC,UAAU,GAAG;UAC5B,UAAK,WAAE,UAAU,GAAG;SACtB,GAAA,YAED,GAAA,WAAA,KAAA,mBAAA,IAAA,IAAA;SAEQ,IAAI,cAAc,MAAA,IAAA,CAAI,CAAC,UAAU,SADzC,UAAA,GAAA,mBAWS,UAAA;;UATP,MAAK;UACL,OAAM;UACN,cAAW;UACV,UAAU,MAAA,IAAA,CAAI,CAAC,UAAU,GAAG;UAC5B,UAAK,WAAE,UAAU,GAAG;SAErB,GAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,mBAEM,OAAA;UAFD,SAAQ;UAAY,MAAK;UAAO,QAAO;UAAe,eAAY;SACrE,GAAA,CAAA,mBAA+D,QAAA,EAAzD,GAAE,qDAAoD,CAAA,CAAA,GAAA,EAAA,CAAA,EAAA,GAAA,GAAA,WAAA,KAAA,mBAAA,IAAA,IAAA;;;MAQxE,CAAA,GAAA,GAAA,EAAA,GAAA,EAAA,MAAA,UAAA,GAAA,mBAqDW,UAAA,EAAA,KAAA,EAAA,GAAA;OApDE,SAAA,SAAX,UAAA,GAAA,mBAQM,OARN,aAQM,CAPJ,mBAMS,UAAA;QALP,MAAK;QACL,OAAM;QACL,SAAO;OACT,GAAA,kBAED,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;OAEO,MAAA,IAAA,CAAI,CAAC,YAAY,MAAM,WAAM,KAAtC,UAAA,GAAA,mBAEI,KAFJ,aAA4E,kBAE5E,KAAA,mBAAA,IAAA,IAAA;QACA,UAAA,IAAA,GAAA,mBAuCU,UAAA,MAAA,WAtCM,MAAA,IAAA,CAAI,CAAC,YAAY,QAAxB,QAAG;QADZ,OAAA,UAAA,GAAA,mBAuCU,WAAA;SArCP,KAAK,IAAI;SACV,OAAM;QAEN,GAAA,CAAA,mBAeM,OAfN,aAeM,CAdJ,mBASM,OATN,aASM,CARJ,mBAMS,UAAA;SALP,MAAK;SACL,OAAM;SACL,UAAK,WAAE,KAAI,UAAW,GAAG;QAEvB,GAAA,gBAAA,IAAI,IAAI,GAAA,GAAA,WAAA,GAEb,mBAAwD,QAAxD,aAAwD,gBAAlB,IAAI,IAAI,GAAA,CAAA,CAAA,CAAA,GAEhD,mBAGO,QAAA,EAHD,OAAK,eAAA,CAAC,0BAAiC,YAAY,GAAG,CAAA,CAAA,EAAA,GAAA,CAC1D,OAAA,QAAA,OAAA,MAAA,mBAA8D,QAAA;SAAxD,OAAM;SAA6B,eAAY;QAAS,GAAA,MAAA,EAAA,IAAA,gBAAA,MAC9D,gBAAG,MAAA,cAAA,CAAc,CAAC,IAAI,MAAM,CAAA,GAAA,CAAA,CAAA,GAAA,CAAA,CAAA,CAAA,GAGhC,mBAiBM,OAjBN,aAiBM,CAhBJ,mBAAsE,QAAA,MAAA,gBAA7D,gBAAgB,IAAI,gBAAgB,IAAI,UAAU,CAAA,GAAA,CAAA,GAC3D,mBAcM,OAdN,aAcM,CAbJ,WAAsC,KAAA,QAAA,eAAA,EAAP,IAAG,CAAA,GAE1B,IAAI,cAAc,MAAA,IAAA,CAAI,CAAC,UAAU,SADzC,UAAA,GAAA,mBAWS,UAAA;;SATP,MAAK;SACL,OAAM;SACN,cAAW;SACV,UAAU,MAAA,IAAA,CAAI,CAAC,UAAU,GAAG;SAC5B,UAAK,WAAE,UAAU,GAAG;QAErB,GAAA,CAAA,GAAA,OAAA,QAAA,OAAA,MAAA,CAAA,mBAEM,OAAA;SAFD,SAAQ;SAAY,MAAK;SAAO,QAAO;SAAe,eAAY;QACrE,GAAA,CAAA,mBAA+D,QAAA,EAAzD,GAAE,qDAAoD,CAAA,CAAA,GAAA,EAAA,CAAA,EAAA,GAAA,GAAA,WAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;;;;;;;;;;;;AE3VpF,IAAM,aAAa;AACnB,IAAM,oBAAoB;AAC1B,IAAM,mBAAmB;;AAEzB,IAAM,oBAAoB;AAC1B,IAAM,SAAwB,EAAE,MAAM,SAAS;AAS/C,IAAM,4BAAY,IAAI,IAAc;;AAEpC,IAAM,2BAAW,IAAI,IAA2B;AAChD,IAAM,YAAY,IAAI,KAAK;AAC3B,IAAI,SAA6B;AACjC,IAAI,eAAe;AACnB,IAAI,WAAW;AACf,IAAI,YAAkD;AACtD,IAAI,aAAmD;AAEvD,SAAS,SAAS,OAA4B;CAC5C,MAAM,aAAa,MAAM,SAAS;CAClC,IAAI,cAAc,MAAM,OAAO,SAAS,IAAI,MAAM,OAAO,KAAK;CAC9D,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAAG;EACrC,IAAI,cAAc,CAAC,SAAS,UAAU;EACtC,IAAI,MAAM,SAAS,YAAa,MAAM,SAAS,SAAS,OAAO,CAAC,CAAC,SAAS,MAAM,KAAK,GACnF,SAAS,QAAQ,KAAK;CAE1B;AACF;AAEA,SAAS,QAAc;CACrB,QAAQ,MAAM;CACd,SAAS;CACT,eAAe;CACf,SAAS,MAAM;CACf,UAAU,QAAQ;AACpB;AAEA,SAAS,KAAK,QAAsB;CAClC,MAAM,SAAS,IAAI,YAAY,GAAG,WAAW,UAAU,mBAAmB,MAAM,GAAG;CACnF,SAAS;CACT,eAAe;CACf,OAAO,eAAe;EACpB,UAAU,QAAQ;EAGlB,SAAS,MAAM;EACf,WAAW;CACb;CACA,OAAO,aAAa,YAAY;EAC9B,SAAS,KAAK,MAAM,QAAQ,IAAI,CAAkB;CACpD;CACA,OAAO,gBAAgB;EAGrB,IAAI,WAAW,QAAQ;EACvB,MAAM;EAEN,IAAI,cAAc,MAAM,aAAa,SAAS;EAC9C,YAAY;EACZ,aAAa,iBAAiB;GAC5B,aAAa;GACb,KAAK;EACP,GAAG,KAAK,IAAI,kBAAkB,oBAAoB,KAAK,QAAQ,CAAC;EAChE,YAAY;CACd;AACF;AAEA,SAAS,OAAa;CACpB,YAAY;CACZ,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,SAAQ,aAAY,SAAS,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;CAClG,IAAI,UAAU,WAAW,cAAc;CACvC,MAAM;CACN,IAAI,QAAQ,KAAK,MAAM;AACzB;AAEA,SAAS,WAAiB;CACxB,IAAI,eAAe,MAAM;CACzB,IAAI,cAAc,MAAM,aAAa,SAAS;CAC9C,YAAY,WAAW,MAAM,iBAAiB;AAChD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,YACd,QACA,SACmB;CACnB,OAAO,kBAAkB,QAAQ,SAAS,KAAK;AACjD;;AAGA,SAAgB,gBAAgB,OAA0C;CACxE,OAAO,SAAS,IAAI,KAAK;AAC3B;;AAGA,SAAgB,kBACd,QACA,SACA,cACmB;CACnB,MAAM,WAAqB;EAAE,cAAc,QAAQ,MAAM;EAAG;EAAS,UAAU;CAAa;CAC5F,UAAU,IAAI,QAAQ;CACtB,MAAM,YAAY,YAAY,SAAS,OAAO,CAAC,CAAC,KAAK,GAAG,SAAS,SAAS,CAAC;CAC3E,SAAS;CAET,SAAS,OAAa;EACpB,UAAU;EACV,IAAI,UAAU,OAAO,QAAQ,GAAG,SAAS;CAC3C;CAEA,IAAI,gBAAgB,GAAG,eAAe,IAAI;CAC1C,OAAO;EAAE,WAAW,SAAS,SAAS;EAAG;CAAK;AAChD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as BaseButton_default, t as useDropdownState } from "./useDropdownState-
|
|
1
|
+
import { a as BaseButton_default, t as useDropdownState } from "./useDropdownState-Bb2N4m40.js";
|
|
2
2
|
import { r as readErrorMessage } from "./experiment-DyO54FQB.js";
|
|
3
|
-
import { E as useMenuKeyboard } from "./records-
|
|
3
|
+
import { E as useMenuKeyboard } from "./records-B4yl2oIu.js";
|
|
4
4
|
import { Fragment, Teleport, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref, watch } from "vue";
|
|
5
5
|
import { Check, Ellipsis } from "lucide-vue-next";
|
|
6
6
|
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
|
|
@@ -241,4 +241,4 @@ function useRequest(defaultErrorMessage = "Request failed.") {
|
|
|
241
241
|
//#endregion
|
|
242
242
|
export { ActionMenu_default as n, useRequest as t };
|
|
243
243
|
|
|
244
|
-
//# sourceMappingURL=useRequest-
|
|
244
|
+
//# sourceMappingURL=useRequest-C9uwM79q.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRequest-Dqw12k2s.js","names":[],"sources":["../src/components/ActionMenu.vue","../src/components/ActionMenu.vue","../src/composables/useRequest.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom'\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref, watch } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })\n\n// Disclosure state (outside-click, Escape, scroll/resize closing, focus return)\n// comes from the shared composable; only floating-ui placement stays local.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n closeOnScroll: true,\n insideRefs: [panelRef],\n onOpen: () => {\n panelStyle.value = { visibility: 'hidden' }\n emit('open')\n },\n onClose: () => emit('close'),\n})\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\nasync function positionPanel() {\n const trigger = triggerRef.value\n const panel = panelRef.value\n if (!trigger || !panel) return\n\n const position = await computePosition(trigger, panel, {\n placement: 'bottom-end',\n strategy: 'fixed',\n middleware: [\n offset(4),\n flip(),\n shift({ padding: 8 }),\n ],\n })\n panelStyle.value = {\n left: `${position.x}px`,\n top: `${position.y}px`,\n visibility: 'visible',\n }\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n\nwatch(isOpen, async (opened) => {\n if (!opened) return\n await nextTick()\n await positionPanel()\n})\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom'\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref, watch } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })\n\n// Disclosure state (outside-click, Escape, scroll/resize closing, focus return)\n// comes from the shared composable; only floating-ui placement stays local.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n closeOnScroll: true,\n insideRefs: [panelRef],\n onOpen: () => {\n panelStyle.value = { visibility: 'hidden' }\n emit('open')\n },\n onClose: () => emit('close'),\n})\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\nasync function positionPanel() {\n const trigger = triggerRef.value\n const panel = panelRef.value\n if (!trigger || !panel) return\n\n const position = await computePosition(trigger, panel, {\n placement: 'bottom-end',\n strategy: 'fixed',\n middleware: [\n offset(4),\n flip(),\n shift({ padding: 8 }),\n ],\n })\n panelStyle.value = {\n left: `${position.x}px`,\n top: `${position.y}px`,\n visibility: 'visible',\n }\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n\nwatch(isOpen, async (opened) => {\n if (!opened) return\n await nextTick()\n await positionPanel()\n})\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","import { ref, type Ref } from 'vue'\nimport { readErrorMessage as readSharedErrorMessage } from '../utils/errors'\n\nexport type RequestKind = 'load' | 'save' | 'run'\n\n/**\n * The unit of work `run()` drives.\n *\n * It receives an `AbortSignal` that fires when the run is superseded by a\n * newer `run()` or by `cancel()`. Zero-argument operations stay valid and\n * simply ignore it.\n */\nexport type RequestOperation<T> = (signal: AbortSignal) => Promise<T>\n\nexport interface RequestRunOptions {\n /** Which timestamp a successful run moves: `lastLoadedAt`, `lastSavedAt` or `lastRunAt`. */\n kind?: RequestKind\n errorMessage?: string\n /**\n * Abort the previously started run when this one starts. Default `true`;\n * pass `false` to let overlapping runs finish their own work (the stale\n * guard on shared state still applies either way).\n */\n abortPrevious?: boolean\n /** Caller-owned signal; aborting it aborts this run's signal too. */\n signal?: AbortSignal\n}\n\nexport interface UseRequestReturn {\n /** Whether a request is in progress. */\n isLoading: Ref<boolean>\n /** Message from the last failed request, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful load operation, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Timestamp of the last successful save operation, or null. */\n lastSavedAt: Ref<Date | null>\n /** Timestamp of the last successful run operation, or null. */\n lastRunAt: Ref<Date | null>\n /** Clear the current error without changing loading or timestamps. */\n clearError: () => void\n /** Convert thrown values into a developer-facing error message. */\n readErrorMessage: (value: unknown, fallback?: string) => string\n /** Store and return a normalized error message. */\n setError: (value: unknown, fallback?: string) => string\n /**\n * Run a request with shared loading/error handling and optional sync marking.\n *\n * ## Stale-response semantics\n *\n * Every call takes a monotonically increasing token. Starting a run\n * supersedes any run started before it, and a superseded run writes **no**\n * shared state when it finally settles: it does not clear `isLoading`, does not\n * set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.\n * Only the newest run owns those.\n *\n * The caller's own promise is untouched by the guard — a superseded run still\n * resolves with its value or rejects with its own error, exactly as the\n * operation did. The guard never invents a rejection, so a caller awaiting\n * `run()` never sees a \"superseded\" error it did not cause.\n *\n * The operation additionally receives an `AbortSignal` that fires when its\n * run is superseded or `cancel()` is called, so an operation that forwards\n * the signal (`fetch(url, { signal })`) stops work instead of racing.\n */\n run: <T>(operation: RequestOperation<T>, options?: RequestRunOptions) => Promise<T>\n /**\n * Abort the in-flight run and drop `isLoading`. The aborted run's completion\n * is treated as stale and writes no state.\n */\n cancel: () => void\n}\n\n/** Loading, error and timestamp state for one request at a time; `run()` drives it. */\nexport function useRequest(\n defaultErrorMessage = 'Request failed.',\n): UseRequestReturn {\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n const lastLoadedAt = ref<Date | null>(null)\n const lastSavedAt = ref<Date | null>(null)\n const lastRunAt = ref<Date | null>(null)\n\n function clearError(): void {\n error.value = null\n }\n\n function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {\n return readSharedErrorMessage(value, fallback)\n }\n\n function setError(value: unknown, fallback?: string): string {\n const message = readErrorMessage(value, fallback)\n error.value = message\n return message\n }\n\n // Monotonic token guarding every write to the shared loading/error/timestamp\n // state. Overlapping runs settle in any order, and a superseded one must not\n // clear a newer run's loading flag or overwrite its error.\n let runToken = 0\n let activeController: AbortController | null = null\n\n function cancel(): void {\n runToken += 1\n activeController?.abort()\n activeController = null\n isLoading.value = false\n }\n\n async function run<T>(\n operation: RequestOperation<T>,\n options: RequestRunOptions = {},\n ): Promise<T> {\n if (options.abortPrevious !== false) {\n activeController?.abort()\n }\n\n const token = (runToken += 1)\n const controller = new AbortController()\n activeController = controller\n\n const callerSignal = options.signal\n if (callerSignal) {\n if (callerSignal.aborted) controller.abort(callerSignal.reason)\n else callerSignal.addEventListener(\n 'abort',\n () => controller.abort(callerSignal.reason),\n { once: true },\n )\n }\n\n const isCurrent = (): boolean => token === runToken\n\n isLoading.value = true\n clearError()\n try {\n const response = await operation(controller.signal)\n if (isCurrent() && options.kind) {\n const settledAt = new Date()\n if (options.kind === 'load') lastLoadedAt.value = settledAt\n else if (options.kind === 'save') lastSavedAt.value = settledAt\n else lastRunAt.value = settledAt\n }\n return response\n } catch (err) {\n if (isCurrent()) {\n setError(err, options.errorMessage)\n }\n throw err\n } finally {\n if (isCurrent()) {\n isLoading.value = false\n }\n if (activeController === controller) {\n activeController = null\n }\n }\n }\n\n return {\n isLoading,\n error,\n lastLoadedAt,\n lastSavedAt,\n lastRunAt,\n clearError,\n readErrorMessage,\n setError,\n run,\n cancel,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useRequest-C9uwM79q.js","names":[],"sources":["../src/components/ActionMenu.vue","../src/components/ActionMenu.vue","../src/composables/useRequest.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom'\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref, watch } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })\n\n// Disclosure state (outside-click, Escape, scroll/resize closing, focus return)\n// comes from the shared composable; only floating-ui placement stays local.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n closeOnScroll: true,\n insideRefs: [panelRef],\n onOpen: () => {\n panelStyle.value = { visibility: 'hidden' }\n emit('open')\n },\n onClose: () => emit('close'),\n})\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\nasync function positionPanel() {\n const trigger = triggerRef.value\n const panel = panelRef.value\n if (!trigger || !panel) return\n\n const position = await computePosition(trigger, panel, {\n placement: 'bottom-end',\n strategy: 'fixed',\n middleware: [\n offset(4),\n flip(),\n shift({ padding: 8 }),\n ],\n })\n panelStyle.value = {\n left: `${position.x}px`,\n top: `${position.y}px`,\n visibility: 'visible',\n }\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n\nwatch(isOpen, async (opened) => {\n if (!opened) return\n await nextTick()\n await positionPanel()\n})\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom'\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref, watch } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })\n\n// Disclosure state (outside-click, Escape, scroll/resize closing, focus return)\n// comes from the shared composable; only floating-ui placement stays local.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n closeOnScroll: true,\n insideRefs: [panelRef],\n onOpen: () => {\n panelStyle.value = { visibility: 'hidden' }\n emit('open')\n },\n onClose: () => emit('close'),\n})\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\nasync function positionPanel() {\n const trigger = triggerRef.value\n const panel = panelRef.value\n if (!trigger || !panel) return\n\n const position = await computePosition(trigger, panel, {\n placement: 'bottom-end',\n strategy: 'fixed',\n middleware: [\n offset(4),\n flip(),\n shift({ padding: 8 }),\n ],\n })\n panelStyle.value = {\n left: `${position.x}px`,\n top: `${position.y}px`,\n visibility: 'visible',\n }\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n\nwatch(isOpen, async (opened) => {\n if (!opened) return\n await nextTick()\n await positionPanel()\n})\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","import { ref, type Ref } from 'vue'\nimport { readErrorMessage as readSharedErrorMessage } from '../utils/errors'\n\nexport type RequestKind = 'load' | 'save' | 'run'\n\n/**\n * The unit of work `run()` drives.\n *\n * It receives an `AbortSignal` that fires when the run is superseded by a\n * newer `run()` or by `cancel()`. Zero-argument operations stay valid and\n * simply ignore it.\n */\nexport type RequestOperation<T> = (signal: AbortSignal) => Promise<T>\n\nexport interface RequestRunOptions {\n /** Which timestamp a successful run moves: `lastLoadedAt`, `lastSavedAt` or `lastRunAt`. */\n kind?: RequestKind\n errorMessage?: string\n /**\n * Abort the previously started run when this one starts. Default `true`;\n * pass `false` to let overlapping runs finish their own work (the stale\n * guard on shared state still applies either way).\n */\n abortPrevious?: boolean\n /** Caller-owned signal; aborting it aborts this run's signal too. */\n signal?: AbortSignal\n}\n\nexport interface UseRequestReturn {\n /** Whether a request is in progress. */\n isLoading: Ref<boolean>\n /** Message from the last failed request, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful load operation, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Timestamp of the last successful save operation, or null. */\n lastSavedAt: Ref<Date | null>\n /** Timestamp of the last successful run operation, or null. */\n lastRunAt: Ref<Date | null>\n /** Clear the current error without changing loading or timestamps. */\n clearError: () => void\n /** Convert thrown values into a developer-facing error message. */\n readErrorMessage: (value: unknown, fallback?: string) => string\n /** Store and return a normalized error message. */\n setError: (value: unknown, fallback?: string) => string\n /**\n * Run a request with shared loading/error handling and optional sync marking.\n *\n * ## Stale-response semantics\n *\n * Every call takes a monotonically increasing token. Starting a run\n * supersedes any run started before it, and a superseded run writes **no**\n * shared state when it finally settles: it does not clear `isLoading`, does not\n * set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.\n * Only the newest run owns those.\n *\n * The caller's own promise is untouched by the guard — a superseded run still\n * resolves with its value or rejects with its own error, exactly as the\n * operation did. The guard never invents a rejection, so a caller awaiting\n * `run()` never sees a \"superseded\" error it did not cause.\n *\n * The operation additionally receives an `AbortSignal` that fires when its\n * run is superseded or `cancel()` is called, so an operation that forwards\n * the signal (`fetch(url, { signal })`) stops work instead of racing.\n */\n run: <T>(operation: RequestOperation<T>, options?: RequestRunOptions) => Promise<T>\n /**\n * Abort the in-flight run and drop `isLoading`. The aborted run's completion\n * is treated as stale and writes no state.\n */\n cancel: () => void\n}\n\n/** Loading, error and timestamp state for one request at a time; `run()` drives it. */\nexport function useRequest(\n defaultErrorMessage = 'Request failed.',\n): UseRequestReturn {\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n const lastLoadedAt = ref<Date | null>(null)\n const lastSavedAt = ref<Date | null>(null)\n const lastRunAt = ref<Date | null>(null)\n\n function clearError(): void {\n error.value = null\n }\n\n function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {\n return readSharedErrorMessage(value, fallback)\n }\n\n function setError(value: unknown, fallback?: string): string {\n const message = readErrorMessage(value, fallback)\n error.value = message\n return message\n }\n\n // Monotonic token guarding every write to the shared loading/error/timestamp\n // state. Overlapping runs settle in any order, and a superseded one must not\n // clear a newer run's loading flag or overwrite its error.\n let runToken = 0\n let activeController: AbortController | null = null\n\n function cancel(): void {\n runToken += 1\n activeController?.abort()\n activeController = null\n isLoading.value = false\n }\n\n async function run<T>(\n operation: RequestOperation<T>,\n options: RequestRunOptions = {},\n ): Promise<T> {\n if (options.abortPrevious !== false) {\n activeController?.abort()\n }\n\n const token = (runToken += 1)\n const controller = new AbortController()\n activeController = controller\n\n const callerSignal = options.signal\n if (callerSignal) {\n if (callerSignal.aborted) controller.abort(callerSignal.reason)\n else callerSignal.addEventListener(\n 'abort',\n () => controller.abort(callerSignal.reason),\n { once: true },\n )\n }\n\n const isCurrent = (): boolean => token === runToken\n\n isLoading.value = true\n clearError()\n try {\n const response = await operation(controller.signal)\n if (isCurrent() && options.kind) {\n const settledAt = new Date()\n if (options.kind === 'load') lastLoadedAt.value = settledAt\n else if (options.kind === 'save') lastSavedAt.value = settledAt\n else lastRunAt.value = settledAt\n }\n return response\n } catch (err) {\n if (isCurrent()) {\n setError(err, options.errorMessage)\n }\n throw err\n } finally {\n if (isCurrent()) {\n isLoading.value = false\n }\n if (activeController === controller) {\n activeController = null\n }\n }\n }\n\n return {\n isLoading,\n error,\n lastLoadedAt,\n lastSavedAt,\n lastRunAt,\n clearError,\n readErrorMessage,\n setError,\n run,\n cancel,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.4.0-beta.
|
|
3
|
+
"version": "1.4.0-beta.14",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,6 +35,25 @@ describe('BaseButton icon', () => {
|
|
|
35
35
|
})
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
// Text-only labels get a span so the stylesheet can trim the text box to the cap
|
|
39
|
+
// height (Geist sits ~1.6px high in a flex-centred button otherwise). Element
|
|
40
|
+
// children keep their flex layout, so they are not wrapped.
|
|
41
|
+
describe('BaseButton label', () => {
|
|
42
|
+
it.each([
|
|
43
|
+
['plain text', 'Cancel', 'Cancel'],
|
|
44
|
+
['element children', '<svg /><span>Export</span>', null],
|
|
45
|
+
])('should wrap the label in a span only for a %s slot', (_case, slot, label) => {
|
|
46
|
+
const wrapper = mount(BaseButton, { slots: { default: slot } })
|
|
47
|
+
const span = wrapper.find('.mint-button__label')
|
|
48
|
+
expect(span.exists() ? span.text() : null).toBe(label)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should trim the label text box to the cap height', () => {
|
|
52
|
+
const css = readFileSync(resolve(__dirname, '../../styles/components/button.css'), 'utf8')
|
|
53
|
+
expect(css).toMatch(/\.mint-button__label \{\s*text-box: trim-both cap alphabetic;\s*\}/)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
38
57
|
// jsdom has no layout, so this pins the stylesheet contract instead: a bare
|
|
39
58
|
// <svg> rendered by an icon-only BaseButton must be sized by the icon box, otherwise it
|
|
40
59
|
// collapses to zero width as a flex item (FilePicker source toggles, beta.14).
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* <BaseButton variant="ghost" :icon="Trash2" label="Delete" />
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
|
-
import { computed, useSlots, type Component } from 'vue'
|
|
13
|
+
import { Comment, Fragment, Text, computed, useSlots, type Component, type VNode } from 'vue'
|
|
14
14
|
import type { ButtonVariant, ButtonSize } from '../types'
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
@@ -45,6 +45,19 @@ const slots = useSlots()
|
|
|
45
45
|
const iconOnly = computed(() => Boolean(props.icon) && !slots.default)
|
|
46
46
|
const block = computed(() => (iconOnly.value ? 'mint-icon-button' : 'mint-button'))
|
|
47
47
|
|
|
48
|
+
// A text-only label gets its own span so the stylesheet can centre the cap height.
|
|
49
|
+
// Element children are left as flex items, so their gap and alignment do not change.
|
|
50
|
+
function isTextNode(node: VNode): boolean {
|
|
51
|
+
if (node.type === Text || node.type === Comment) return true
|
|
52
|
+
return node.type === Fragment && Array.isArray(node.children)
|
|
53
|
+
&& (node.children as VNode[]).every(isTextNode)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasTextLabel(): boolean {
|
|
57
|
+
const nodes = slots.default?.()
|
|
58
|
+
return !!nodes && nodes.length > 0 && nodes.every(isTextNode)
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function handleClick(event: MouseEvent) {
|
|
49
62
|
if (!props.disabled && !props.loading) {
|
|
50
63
|
emit('click', event)
|
|
@@ -93,7 +106,8 @@ function handleClick(event: MouseEvent) {
|
|
|
93
106
|
<template v-if="!iconOnly">
|
|
94
107
|
<component :is="icon" v-if="icon" class="mint-button__icon" aria-hidden="true" />
|
|
95
108
|
<slot name="icon-left" />
|
|
96
|
-
<slot
|
|
109
|
+
<span v-if="hasTextLabel()" class="mint-button__label"><slot /></span>
|
|
110
|
+
<slot v-else />
|
|
97
111
|
<slot name="icon-right" />
|
|
98
112
|
</template>
|
|
99
113
|
</button>
|
|
@@ -150,6 +150,14 @@
|
|
|
150
150
|
height: 1.25rem;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/* Flex centring centres the line box, and Geist's ascender puts the caps
|
|
154
|
+
about 1.6px above the middle. Trim the text box to cap height and baseline
|
|
155
|
+
so the caps sit on the centre line. Browsers without text-box keep the
|
|
156
|
+
untrimmed box. */
|
|
157
|
+
.mint-button__label {
|
|
158
|
+
text-box: trim-both cap alphabetic;
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
/* Spinner */
|
|
154
162
|
.mint-button__spinner {
|
|
155
163
|
animation: mint-button-spin 1s linear infinite;
|