@morscherlab/mint-sdk 1.1.5 → 1.1.7
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/{EmptyState-LMPS3keb.js → EmptyState-p90quq8Z.js} +2 -2
- package/dist/{EmptyState-LMPS3keb.js.map → EmptyState-p90quq8Z.js.map} +1 -1
- package/dist/{ExperimentPopover-D6_RGzTY.js → ExperimentPopover-tLwJ7XF4.js} +3 -3
- package/dist/{ExperimentPopover-D6_RGzTY.js.map → ExperimentPopover-tLwJ7XF4.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-DcQ38zua.js → ExperimentSelectorModal-B6P2msT4.js} +12 -5
- package/dist/ExperimentSelectorModal-B6P2msT4.js.map +1 -0
- package/dist/{SettingsModal-Ccq2z-Ms.js → SettingsModal-D7LFUokT.js} +100 -156
- package/dist/SettingsModal-D7LFUokT.js.map +1 -0
- package/dist/__tests__/components/ActionMenu.test.d.ts +1 -0
- package/dist/__tests__/components/BaseModal.test.d.ts +1 -0
- package/dist/components/ActionMenu.vue.d.ts +21 -0
- package/dist/components/BaseModal.vue.d.ts +17 -1
- package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
- package/dist/components/GeneratedPluginView.vue.d.ts +3 -3
- package/dist/components/GeneratedResultRenderer.vue.d.ts +3 -3
- package/dist/components/RackEditor.vue.d.ts +1 -1
- package/dist/components/SampleLegend.vue.d.ts +1 -1
- package/dist/components/SmartGroupManual.vue.d.ts +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +8 -8
- package/dist/{components-CV5zuWa8.js → components-CDomhGSj.js} +477 -71
- package/dist/components-CDomhGSj.js.map +1 -0
- package/dist/composables/controlSchemaDoseDesign.d.ts +8 -1
- package/dist/composables/controlSchemaModel.d.ts +10 -1
- package/dist/composables/index.js +8 -8
- package/dist/composables/useAnalysisArtifacts.d.ts +9 -1
- package/dist/composables/useAppExperiment.d.ts +7 -0
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +9 -1
- package/dist/composables/useControlWorkspace.d.ts +9 -1
- package/dist/composables/useGeneratedAnalysis.d.ts +9 -0
- package/dist/{composables-CW8NRXLB.js → composables-CRR_JxQD.js} +4 -4
- package/dist/{composables-CW8NRXLB.js.map → composables-CRR_JxQD.js.map} +1 -1
- package/dist/{experiment-utils-7Yw-h0tD.js → experiment-utils-CLnzPPq5.js} +170 -13
- package/dist/experiment-utils-CLnzPPq5.js.map +1 -0
- package/dist/index.js +13 -13
- package/dist/install.js +7 -7
- package/dist/styles.css +764 -610
- package/dist/templates/index.js +4 -4
- package/dist/{templates-CJRFiWUX.js → templates-Cr-48-u2.js} +2 -2
- package/dist/{templates-CJRFiWUX.js.map → templates-Cr-48-u2.js.map} +1 -1
- package/dist/types/components.d.ts +32 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/{useControlSchema-BEAgLyyx.js → useControlSchema-BMVb2XIQ.js} +58 -10
- package/dist/useControlSchema-BMVb2XIQ.js.map +1 -0
- package/dist/{useDropdownState-Be-ug_ci.js → useDropdownState-DSb3BC3N.js} +2 -2
- package/dist/{useDropdownState-Be-ug_ci.js.map → useDropdownState-DSb3BC3N.js.map} +1 -1
- package/dist/{useExperimentSelector-DFg_iEHI.js → useExperimentSelector-D0pjj9WH.js} +3 -3
- package/dist/{useExperimentSelector-DFg_iEHI.js.map → useExperimentSelector-D0pjj9WH.js.map} +1 -1
- package/dist/{useFormBuilder-CR9pBApW.js → useFormBuilder-DG3HOFGA.js} +2 -2
- package/dist/{useFormBuilder-CR9pBApW.js.map → useFormBuilder-DG3HOFGA.js.map} +1 -1
- package/dist/{useProtocolTemplates-DXeIHMwV.js → useProtocolTemplates-BMt0LlXf.js} +21 -6
- package/dist/useProtocolTemplates-BMt0LlXf.js.map +1 -0
- package/package.json +2 -1
- package/src/__tests__/components/ActionMenu.test.ts +120 -0
- package/src/__tests__/components/BaseModal.test.ts +133 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -7
- package/src/__tests__/docs/extractDocsComponents.test.ts +15 -1
- package/src/__tests__/docs/extractDocsParsing.test.ts +22 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +80 -0
- package/src/components/ActionMenu.story.vue +26 -0
- package/src/components/ActionMenu.vue +209 -0
- package/src/components/AppSidebar.vue +2 -13
- package/src/components/ArtifactSelectorModal.vue +8 -1
- package/src/components/BaseModal.story.vue +107 -1
- package/src/components/BaseModal.vue +152 -4
- package/src/components/BioTemplateExperimentWorkspaceView.vue +8 -1
- package/src/components/BioTemplatePackWorkspaceView.vue +8 -1
- package/src/components/BioTemplatePresetWorkspaceView.vue +8 -1
- package/src/components/BioTemplateRenderer.vue +8 -1
- package/src/components/ComponentBindingRenderer.vue +8 -1
- package/src/components/ControlWorkspaceView.vue +8 -1
- package/src/components/DoseDesignWorkspaceView.vue +8 -1
- package/src/components/ExperimentSelectorModal.vue +8 -1
- package/src/components/FormBuilder.vue +5 -0
- package/src/components/GeneratedPluginView.vue +12 -2
- package/src/components/GeneratedResultRenderer.vue +12 -2
- package/src/components/JobsStatusTray.vue +8 -1
- package/src/components/PluginWorkspaceView.vue +10 -1
- package/src/components/SettingsModal.vue +102 -149
- package/src/components/index.ts +1 -0
- package/src/composables/controlSchemaDoseDesign.ts +8 -1
- package/src/composables/controlSchemaModel.ts +10 -1
- package/src/composables/useAnalysisArtifacts.ts +9 -1
- package/src/composables/useAppExperiment.ts +7 -0
- package/src/composables/useBioTemplatePackWorkspace.ts +9 -1
- package/src/composables/useControlWorkspace.ts +9 -1
- package/src/composables/useGeneratedAnalysis.ts +9 -0
- package/src/styles/components/action-menu.css +59 -0
- package/src/styles/components/icon-button.css +7 -3
- package/src/styles/components/modal.css +293 -0
- package/src/styles/components/settings-modal.css +3 -273
- package/src/styles/index.css +1 -0
- package/src/types/components.ts +38 -0
- package/src/types/index.ts +3 -0
- package/dist/ExperimentSelectorModal-DcQ38zua.js.map +0 -1
- package/dist/SettingsModal-Ccq2z-Ms.js.map +0 -1
- package/dist/components-CV5zuWa8.js.map +0 -1
- package/dist/experiment-utils-7Yw-h0tD.js.map +0 -1
- package/dist/useControlSchema-BEAgLyyx.js.map +0 -1
- package/dist/useProtocolTemplates-DXeIHMwV.js.map +0 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Compact overflow actions with an IconButton trigger and a viewport-safe teleported menu. */
|
|
3
|
+
import { computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
4
|
+
import { Ellipsis } from 'lucide-vue-next'
|
|
5
|
+
import { nextTick, onBeforeUnmount, ref } from 'vue'
|
|
6
|
+
import type { ActionMenuItem } from '../types/components'
|
|
7
|
+
import IconButton from './IconButton.vue'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string
|
|
11
|
+
items: ActionMenuItem[]
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
16
|
+
disabled: false,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
select: [item: ActionMenuItem]
|
|
21
|
+
open: []
|
|
22
|
+
close: []
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const isOpen = ref(false)
|
|
26
|
+
const triggerRootRef = ref<HTMLElement | null>(null)
|
|
27
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
28
|
+
const panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })
|
|
29
|
+
|
|
30
|
+
function triggerElement(): HTMLButtonElement | null {
|
|
31
|
+
return triggerRootRef.value?.querySelector('button') ?? null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function menuItems(): HTMLElement[] {
|
|
35
|
+
if (!panelRef.value) return []
|
|
36
|
+
return Array.from(
|
|
37
|
+
panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function positionPanel() {
|
|
42
|
+
const trigger = triggerElement()
|
|
43
|
+
const panel = panelRef.value
|
|
44
|
+
if (!trigger || !panel) return
|
|
45
|
+
|
|
46
|
+
const position = await computePosition(trigger, panel, {
|
|
47
|
+
placement: 'bottom-end',
|
|
48
|
+
strategy: 'fixed',
|
|
49
|
+
middleware: [
|
|
50
|
+
offset(4),
|
|
51
|
+
flip(),
|
|
52
|
+
shift({ padding: 8 }),
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
panelStyle.value = {
|
|
56
|
+
left: `${position.x}px`,
|
|
57
|
+
top: `${position.y}px`,
|
|
58
|
+
visibility: 'visible',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addGlobalListeners() {
|
|
63
|
+
document.addEventListener('pointerdown', handleDocumentPointerDown, true)
|
|
64
|
+
window.addEventListener('resize', handleViewportChange)
|
|
65
|
+
window.addEventListener('scroll', handleViewportChange, true)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function removeGlobalListeners() {
|
|
69
|
+
document.removeEventListener('pointerdown', handleDocumentPointerDown, true)
|
|
70
|
+
window.removeEventListener('resize', handleViewportChange)
|
|
71
|
+
window.removeEventListener('scroll', handleViewportChange, true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function openMenu() {
|
|
75
|
+
if (props.disabled || isOpen.value) return
|
|
76
|
+
panelStyle.value = { visibility: 'hidden' }
|
|
77
|
+
isOpen.value = true
|
|
78
|
+
emit('open')
|
|
79
|
+
addGlobalListeners()
|
|
80
|
+
await nextTick()
|
|
81
|
+
await positionPanel()
|
|
82
|
+
menuItems()[0]?.focus()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function closeMenu(returnFocus = false) {
|
|
86
|
+
if (!isOpen.value) return
|
|
87
|
+
isOpen.value = false
|
|
88
|
+
removeGlobalListeners()
|
|
89
|
+
emit('close')
|
|
90
|
+
if (returnFocus) {
|
|
91
|
+
nextTick(() => triggerElement()?.focus())
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toggleMenu() {
|
|
96
|
+
if (isOpen.value) closeMenu()
|
|
97
|
+
else void openMenu()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleSelect(item: ActionMenuItem) {
|
|
101
|
+
if (item.disabled) return
|
|
102
|
+
emit('select', item)
|
|
103
|
+
closeMenu()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleDocumentPointerDown(event: PointerEvent) {
|
|
107
|
+
const target = event.target
|
|
108
|
+
if (!(target instanceof Node)) return
|
|
109
|
+
if (triggerRootRef.value?.contains(target) || panelRef.value?.contains(target)) return
|
|
110
|
+
closeMenu()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function handleViewportChange() {
|
|
114
|
+
closeMenu()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleMenuKeydown(event: KeyboardEvent) {
|
|
118
|
+
const items = menuItems()
|
|
119
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement)
|
|
120
|
+
|
|
121
|
+
if (event.key === 'Escape') {
|
|
122
|
+
event.preventDefault()
|
|
123
|
+
closeMenu(true)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (event.key === 'Tab') {
|
|
127
|
+
closeMenu()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (event.key === 'Home') {
|
|
131
|
+
event.preventDefault()
|
|
132
|
+
items[0]?.focus()
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if (event.key === 'End') {
|
|
136
|
+
event.preventDefault()
|
|
137
|
+
items[items.length - 1]?.focus()
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
|
141
|
+
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
const direction = event.key === 'ArrowDown' ? 1 : -1
|
|
144
|
+
const nextIndex = (Math.max(currentIndex, 0) + direction + items.length) % items.length
|
|
145
|
+
items[nextIndex]?.focus()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
onBeforeUnmount(removeGlobalListeners)
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<span ref="triggerRootRef" class="mint-action-menu">
|
|
153
|
+
<IconButton
|
|
154
|
+
variant="secondary"
|
|
155
|
+
size="sm"
|
|
156
|
+
:label="label"
|
|
157
|
+
:disabled="disabled"
|
|
158
|
+
aria-haspopup="menu"
|
|
159
|
+
:aria-expanded="isOpen"
|
|
160
|
+
@click="toggleMenu"
|
|
161
|
+
>
|
|
162
|
+
<Ellipsis
|
|
163
|
+
class="mint-action-menu__trigger-icon"
|
|
164
|
+
:size="16"
|
|
165
|
+
:stroke-width="2"
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
/>
|
|
168
|
+
</IconButton>
|
|
169
|
+
</span>
|
|
170
|
+
|
|
171
|
+
<Teleport to="body">
|
|
172
|
+
<div
|
|
173
|
+
v-if="isOpen"
|
|
174
|
+
ref="panelRef"
|
|
175
|
+
class="mint-action-menu__panel"
|
|
176
|
+
:style="panelStyle"
|
|
177
|
+
role="menu"
|
|
178
|
+
@keydown="handleMenuKeydown"
|
|
179
|
+
>
|
|
180
|
+
<template v-for="item in items" :key="item.id">
|
|
181
|
+
<a
|
|
182
|
+
v-if="item.href !== undefined && !item.disabled"
|
|
183
|
+
:href="item.href"
|
|
184
|
+
:target="item.target"
|
|
185
|
+
:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
|
|
186
|
+
:class="['mint-action-menu__item', { 'mint-action-menu__item--danger': item.danger }]"
|
|
187
|
+
role="menuitem"
|
|
188
|
+
@click="handleSelect(item)"
|
|
189
|
+
>
|
|
190
|
+
{{ item.label }}
|
|
191
|
+
</a>
|
|
192
|
+
<button
|
|
193
|
+
v-else
|
|
194
|
+
type="button"
|
|
195
|
+
:class="['mint-action-menu__item', { 'mint-action-menu__item--danger': item.danger }]"
|
|
196
|
+
role="menuitem"
|
|
197
|
+
:disabled="item.disabled"
|
|
198
|
+
@click="handleSelect(item)"
|
|
199
|
+
>
|
|
200
|
+
{{ item.label }}
|
|
201
|
+
</button>
|
|
202
|
+
</template>
|
|
203
|
+
</div>
|
|
204
|
+
</Teleport>
|
|
205
|
+
</template>
|
|
206
|
+
|
|
207
|
+
<style>
|
|
208
|
+
@import '../styles/components/action-menu.css';
|
|
209
|
+
</style>
|
|
@@ -9,20 +9,9 @@
|
|
|
9
9
|
* When activeView is omitted, the first non-empty panel view is selected.
|
|
10
10
|
* When no view has matching panels, the sidebar hides entirely.
|
|
11
11
|
*
|
|
12
|
-
* @example
|
|
12
|
+
* @example Recommended
|
|
13
13
|
* ```vue
|
|
14
|
-
* <AppSidebar
|
|
15
|
-
* <template #section-parameters>
|
|
16
|
-
* <BaseSlider v-model="threshold" label="Threshold" />
|
|
17
|
-
* </template>
|
|
18
|
-
* <template #section-display>
|
|
19
|
-
* <BaseToggle v-model="showOutliers" label="Show outliers" />
|
|
20
|
-
* </template>
|
|
21
|
-
* <template #header>Plugin Tools</template>
|
|
22
|
-
* </AppSidebar>
|
|
23
|
-
*
|
|
24
|
-
* <!-- Direct schema-driven controls -->
|
|
25
|
-
* <AppSidebar :controls="controls" :active-view="activeTab" v-model="values" />
|
|
14
|
+
* <AppSidebar v-model="values" :model="sidebarModel" />
|
|
26
15
|
* ```
|
|
27
16
|
*/
|
|
28
17
|
import { computed, ref, useSlots, type Slots } from 'vue'
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Modal for browsing and selecting an analysis artifact of an experiment, with filters and keyboard nav.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <ArtifactSelectorModal v-model="open" :experiment-id="experimentId" @select="selectArtifact" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed, ref, watch, nextTick } from 'vue'
|
|
4
11
|
import type { ModalSize } from '../types'
|
|
5
12
|
import type {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import BaseModal from './BaseModal.vue'
|
|
4
4
|
import BaseButton from './BaseButton.vue'
|
|
5
|
-
import type { ModalSize } from '../types'
|
|
5
|
+
import type { ModalSize, ModalTab } from '../types'
|
|
6
6
|
|
|
7
7
|
const modalSizes: ModalSize[] = ['sm', 'md', 'lg', 'xl', 'full']
|
|
8
8
|
const galleryOpen = ref<Record<string, boolean>>({
|
|
@@ -19,6 +19,28 @@ const refreshOpen = ref(false)
|
|
|
19
19
|
const subtitleOpen = ref(false)
|
|
20
20
|
const drawerOpen = ref(false)
|
|
21
21
|
const sheetOpen = ref(false)
|
|
22
|
+
|
|
23
|
+
// ---- layout="tabs": shell-level tab strip (2–4 sections) ----
|
|
24
|
+
const tabsOpen = ref(false)
|
|
25
|
+
const tabsActive = ref('processing')
|
|
26
|
+
const stripTabs: ModalTab[] = [
|
|
27
|
+
{ id: 'processing', label: 'Processing', meta: '6' },
|
|
28
|
+
{ id: 'peaks', label: 'Peak detection', meta: '9' },
|
|
29
|
+
{ id: 'correction', label: 'Correction', meta: '4' },
|
|
30
|
+
{ id: 'appearance', label: 'Appearance' },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// ---- layout="rail": shell-level rail (5+ sections) ----
|
|
34
|
+
const railOpen = ref(false)
|
|
35
|
+
const railActive = ref('security')
|
|
36
|
+
const railTabs: ModalTab[] = [
|
|
37
|
+
{ id: 'profile', label: 'Profile', description: 'Name, display name, and email', icon: 'M20 21a8 8 0 0 0-16 0M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8' },
|
|
38
|
+
{ id: 'password', label: 'Password', description: 'Change your sign-in password', icon: 'M6 11h12v10H6zM9 11V7a3 3 0 0 1 6 0v4' },
|
|
39
|
+
{ id: 'security', label: 'Security', meta: '2', description: 'Passkeys and institutional sign-in', icon: 'm12 3 8 4.5v9L12 21l-8-4.5v-9L12 3Z' },
|
|
40
|
+
{ id: 'notifications', label: 'Notifications', description: 'Email and in-app alerts', icon: 'M18 8a6 6 0 1 0-12 0c0 7-3 8-3 8h18s-3-1-3-8M13.7 21a2 2 0 0 1-3.4 0' },
|
|
41
|
+
{ id: 'tokens', label: 'API tokens', meta: '3', description: 'Programmatic access keys', icon: 'M15 7h3a4 4 0 0 1 0 8h-3M9 15H6a4 4 0 0 1 0-8h3M8 11h8' },
|
|
42
|
+
{ id: 'appearance', label: 'Appearance', description: 'Theme and density', icon: 'M12 3a9 9 0 1 0 0 18 3 3 0 0 0 0-6 3 3 0 0 1 0-6 3 3 0 0 0 0-6M7 10h.01M12 7h.01M17 10h.01' },
|
|
43
|
+
]
|
|
22
44
|
</script>
|
|
23
45
|
|
|
24
46
|
<template>
|
|
@@ -225,5 +247,89 @@ const sheetOpen = ref(false)
|
|
|
225
247
|
</template>
|
|
226
248
|
</BaseModal>
|
|
227
249
|
</Variant>
|
|
250
|
+
|
|
251
|
+
<Variant title="Tabs layout — shell-level strip">
|
|
252
|
+
<div style="padding: 2rem;">
|
|
253
|
+
<BaseButton variant="primary" @click="tabsOpen = true">
|
|
254
|
+
Open tabbed modal
|
|
255
|
+
</BaseButton>
|
|
256
|
+
<p style="margin: 1rem 0 0; font-size: 0.8125rem; color: var(--text-muted);">
|
|
257
|
+
The strip is a shell region between header and body: flush with the modal edges,
|
|
258
|
+
it never scrolls with the content, and the body keeps one fixed height across tabs.
|
|
259
|
+
Arrow keys move between tabs. For 2–4 sections; use the rail beyond that.
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<BaseModal
|
|
264
|
+
v-model="tabsOpen"
|
|
265
|
+
v-model:active-tab="tabsActive"
|
|
266
|
+
layout="tabs"
|
|
267
|
+
:tabs="stripTabs"
|
|
268
|
+
size="lg"
|
|
269
|
+
title="Isotope Tracer Analysis — settings"
|
|
270
|
+
subtitle="Plugin 2.4 · applies to this workspace"
|
|
271
|
+
>
|
|
272
|
+
<div style="display: flex; flex-direction: column; gap: 0.875rem;">
|
|
273
|
+
<p style="margin: 0; font-size: 0.8125rem; color: var(--text-secondary);">
|
|
274
|
+
Active tab: <strong style="color: var(--text-primary);">{{ tabsActive }}</strong> —
|
|
275
|
+
pane content swaps here while the card height stays put.
|
|
276
|
+
</p>
|
|
277
|
+
<div v-for="n in 4" :key="n" style="display: flex; align-items: center; gap: 0.875rem; padding: 0.6875rem 0; border-bottom: 1px solid var(--border-light);">
|
|
278
|
+
<span style="width: 9.5rem; flex-shrink: 0; font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);">Setting {{ n }}</span>
|
|
279
|
+
<span style="flex: 1; padding: 0.375rem 0.625rem; border: 1px solid var(--border-color); border-radius: 0.4375rem; background: var(--bg-secondary); font-family: var(--font-mono); font-size: 0.8125rem;">value {{ n }}</span>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<template #footer>
|
|
283
|
+
<BaseButton variant="secondary" @click="tabsOpen = false">Cancel</BaseButton>
|
|
284
|
+
<BaseButton variant="primary" @click="tabsOpen = false">Save settings</BaseButton>
|
|
285
|
+
</template>
|
|
286
|
+
</BaseModal>
|
|
287
|
+
</Variant>
|
|
288
|
+
|
|
289
|
+
<Variant title="Rail layout — shell-level sidebar">
|
|
290
|
+
<div style="padding: 2rem;">
|
|
291
|
+
<BaseButton variant="primary" @click="railOpen = true">
|
|
292
|
+
Open rail modal
|
|
293
|
+
</BaseButton>
|
|
294
|
+
<p style="margin: 1rem 0 0; font-size: 0.8125rem; color: var(--text-muted);">
|
|
295
|
+
For 5+ sections, or sections needing descriptions/counts. The rail is a real shell
|
|
296
|
+
column (no negative-margin bleed), scrolls independently of the pane, and the pane
|
|
297
|
+
names the active section in its own header. Collapses to the strip under 640px.
|
|
298
|
+
</p>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<BaseModal
|
|
302
|
+
v-model="railOpen"
|
|
303
|
+
v-model:active-tab="railActive"
|
|
304
|
+
layout="rail"
|
|
305
|
+
:tabs="railTabs"
|
|
306
|
+
size="lg"
|
|
307
|
+
title="Your account"
|
|
308
|
+
subtitle="R. Keller · rkeller · Admin"
|
|
309
|
+
close-on-overlay
|
|
310
|
+
>
|
|
311
|
+
<template #rail-footer>
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
style="display: flex; align-items: center; gap: 0.5rem; margin-top: auto; padding: 0.4375rem 0.625rem; border: none; border-radius: 0.4375rem; background: transparent; color: var(--mint-error); font-family: inherit; font-size: 0.78125rem; font-weight: 500; text-align: left; cursor: pointer;"
|
|
315
|
+
@click="railOpen = false"
|
|
316
|
+
>
|
|
317
|
+
Log out
|
|
318
|
+
</button>
|
|
319
|
+
</template>
|
|
320
|
+
|
|
321
|
+
<div style="display: flex; flex-direction: column;">
|
|
322
|
+
<div v-for="n in 3" :key="n" style="display: flex; align-items: center; gap: 0.6875rem; padding: 0.5625rem 0; border-bottom: 1px solid var(--border-light);">
|
|
323
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary);">{{ railActive }} row {{ n }}</span>
|
|
324
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted);">detail</span>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
<template #footer>
|
|
328
|
+
<span style="font-size: 0.75rem; color: var(--text-muted);">Changes apply immediately</span>
|
|
329
|
+
<div style="flex: 1;" />
|
|
330
|
+
<BaseButton variant="secondary" @click="railOpen = false">Close</BaseButton>
|
|
331
|
+
</template>
|
|
332
|
+
</BaseModal>
|
|
333
|
+
</Variant>
|
|
228
334
|
</Story>
|
|
229
335
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/** Modal dialog with backdrop, focus trap, Escape-to-close, and configurable size/variant. */
|
|
3
|
-
import { ref, watch, nextTick, onUnmounted } from 'vue'
|
|
4
|
-
import type { ModalSize, ModalVariant } from '../types'
|
|
2
|
+
/** Modal dialog with backdrop, focus trap, Escape-to-close, and configurable size/variant/layout. */
|
|
3
|
+
import { computed, ref, watch, nextTick, onUnmounted, type ComponentPublicInstance } from 'vue'
|
|
4
|
+
import type { ModalLayout, ModalSize, ModalTab, ModalVariant } from '../types'
|
|
5
5
|
import { useEventListener } from '../composables/useEventListener'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
@@ -13,6 +13,16 @@ interface Props {
|
|
|
13
13
|
closable?: boolean
|
|
14
14
|
closeOnOverlay?: boolean
|
|
15
15
|
closeOnEscape?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Shell layout. 'tabs' renders a tab strip between header and body;
|
|
18
|
+
* 'rail' renders a sidebar rail + pane as the shell's middle row.
|
|
19
|
+
* Both are shell regions — never part of the scrolling body content.
|
|
20
|
+
*/
|
|
21
|
+
layout?: ModalLayout
|
|
22
|
+
/** Sections for the tab strip / rail. The strip renders only with 2+ tabs. */
|
|
23
|
+
tabs?: ModalTab[]
|
|
24
|
+
/** Id of the selected tab. Two-way bindable via update:activeTab. */
|
|
25
|
+
activeTab?: string
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -21,13 +31,49 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
21
31
|
closable: true,
|
|
22
32
|
closeOnOverlay: false,
|
|
23
33
|
closeOnEscape: true,
|
|
34
|
+
layout: 'plain',
|
|
35
|
+
tabs: () => [],
|
|
24
36
|
})
|
|
25
37
|
|
|
26
38
|
const emit = defineEmits<{
|
|
27
39
|
'update:modelValue': [value: boolean]
|
|
40
|
+
'update:activeTab': [id: string]
|
|
28
41
|
close: []
|
|
29
42
|
}>()
|
|
30
43
|
|
|
44
|
+
const activeTabMeta = computed(() => props.tabs.find((tab) => tab.id === props.activeTab))
|
|
45
|
+
|
|
46
|
+
const tabButtonRefs = new Map<string, HTMLElement>()
|
|
47
|
+
|
|
48
|
+
function setTabButtonRef(id: string, el: Element | ComponentPublicInstance | null) {
|
|
49
|
+
if (el instanceof HTMLElement) {
|
|
50
|
+
tabButtonRefs.set(id, el)
|
|
51
|
+
} else {
|
|
52
|
+
tabButtonRefs.delete(id)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function selectTab(tab: ModalTab) {
|
|
57
|
+
if (tab.disabled || tab.id === props.activeTab) return
|
|
58
|
+
emit('update:activeTab', tab.id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** ARIA tablist keyboard contract: arrows move selection to the next non-disabled tab, wrapping. */
|
|
62
|
+
function focusTab(delta: number) {
|
|
63
|
+
const enabled = props.tabs.filter((tab) => !tab.disabled)
|
|
64
|
+
if (enabled.length === 0) return
|
|
65
|
+
const currentIndex = enabled.findIndex((tab) => tab.id === props.activeTab)
|
|
66
|
+
const next = enabled[(currentIndex + delta + enabled.length) % enabled.length]
|
|
67
|
+
if (!next || next.id === props.activeTab) return
|
|
68
|
+
emit('update:activeTab', next.id)
|
|
69
|
+
tabButtonRefs.get(next.id)?.focus()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isPathIcon(icon: ModalTab['icon']): icon is string | string[] {
|
|
73
|
+
if (!icon) return false
|
|
74
|
+
return Array.isArray(icon) || icon.trim().startsWith('M') || icon.trim().startsWith('m')
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
const containerRef = ref<HTMLElement | null>(null)
|
|
32
78
|
let previouslyFocused: HTMLElement | null = null
|
|
33
79
|
|
|
@@ -43,6 +89,8 @@ const FOCUSABLE_SELECTOR = [
|
|
|
43
89
|
function getFocusableElements(): HTMLElement[] {
|
|
44
90
|
if (!containerRef.value) return []
|
|
45
91
|
return Array.from(containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
|
|
92
|
+
// Roving tabindex (inactive tabs/rail items) stays out of the Tab cycle.
|
|
93
|
+
.filter((el) => el.tabIndex !== -1)
|
|
46
94
|
}
|
|
47
95
|
|
|
48
96
|
function handleKeydown(event: KeyboardEvent) {
|
|
@@ -127,6 +175,7 @@ onUnmounted(() => {
|
|
|
127
175
|
'mint-modal__container',
|
|
128
176
|
`mint-modal__container--${size}`,
|
|
129
177
|
`mint-modal__container--${variant}`,
|
|
178
|
+
`mint-modal__container--${layout}`,
|
|
130
179
|
]"
|
|
131
180
|
role="dialog"
|
|
132
181
|
aria-modal="true"
|
|
@@ -160,8 +209,107 @@ onUnmounted(() => {
|
|
|
160
209
|
</button>
|
|
161
210
|
</div>
|
|
162
211
|
|
|
212
|
+
<!-- Tab strip — a shell region between header and body, flush with the modal edges -->
|
|
213
|
+
<div
|
|
214
|
+
v-if="layout === 'tabs' && tabs.length > 1"
|
|
215
|
+
class="mint-modal__tabs"
|
|
216
|
+
role="tablist"
|
|
217
|
+
>
|
|
218
|
+
<button
|
|
219
|
+
v-for="tab in tabs"
|
|
220
|
+
:key="tab.id"
|
|
221
|
+
:ref="(el) => setTabButtonRef(tab.id, el)"
|
|
222
|
+
type="button"
|
|
223
|
+
role="tab"
|
|
224
|
+
:aria-selected="activeTab === tab.id"
|
|
225
|
+
:aria-disabled="tab.disabled || undefined"
|
|
226
|
+
:tabindex="activeTab === tab.id ? 0 : -1"
|
|
227
|
+
:class="['mint-modal__tab', {
|
|
228
|
+
'mint-modal__tab--active': activeTab === tab.id,
|
|
229
|
+
'mint-modal__tab--disabled': tab.disabled,
|
|
230
|
+
}]"
|
|
231
|
+
@click="selectTab(tab)"
|
|
232
|
+
@keydown.left.prevent="focusTab(-1)"
|
|
233
|
+
@keydown.right.prevent="focusTab(1)"
|
|
234
|
+
>
|
|
235
|
+
{{ tab.label }}
|
|
236
|
+
<span v-if="tab.meta" class="mint-modal__tab-meta">{{ tab.meta }}</span>
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Rail layout: rail column + pane are the shell's middle row -->
|
|
241
|
+
<div v-if="layout === 'rail'" class="mint-modal__split">
|
|
242
|
+
<div class="mint-modal__rail" role="tablist" aria-orientation="vertical">
|
|
243
|
+
<div class="mint-modal__rail-items">
|
|
244
|
+
<button
|
|
245
|
+
v-for="tab in tabs"
|
|
246
|
+
:key="tab.id"
|
|
247
|
+
:ref="(el) => setTabButtonRef(tab.id, el)"
|
|
248
|
+
type="button"
|
|
249
|
+
role="tab"
|
|
250
|
+
:aria-selected="activeTab === tab.id"
|
|
251
|
+
:aria-disabled="tab.disabled || undefined"
|
|
252
|
+
:tabindex="activeTab === tab.id ? 0 : -1"
|
|
253
|
+
:class="['mint-modal__rail-item', {
|
|
254
|
+
'mint-modal__rail-item--active': activeTab === tab.id,
|
|
255
|
+
'mint-modal__rail-item--disabled': tab.disabled,
|
|
256
|
+
}]"
|
|
257
|
+
@click="selectTab(tab)"
|
|
258
|
+
@keydown.up.prevent="focusTab(-1)"
|
|
259
|
+
@keydown.down.prevent="focusTab(1)"
|
|
260
|
+
>
|
|
261
|
+
<span v-if="tab.icon" class="mint-modal__rail-item-icon" aria-hidden="true">
|
|
262
|
+
<svg
|
|
263
|
+
v-if="isPathIcon(tab.icon)"
|
|
264
|
+
viewBox="0 0 24 24"
|
|
265
|
+
fill="none"
|
|
266
|
+
stroke="currentColor"
|
|
267
|
+
stroke-width="1.8"
|
|
268
|
+
stroke-linecap="round"
|
|
269
|
+
stroke-linejoin="round"
|
|
270
|
+
>
|
|
271
|
+
<template v-if="Array.isArray(tab.icon)">
|
|
272
|
+
<path v-for="(d, index) in tab.icon" :key="index" :d="d" />
|
|
273
|
+
</template>
|
|
274
|
+
<path v-else :d="tab.icon" />
|
|
275
|
+
</svg>
|
|
276
|
+
<!-- Trusted, author-controlled markup only — same contract as SettingsTab.icon. -->
|
|
277
|
+
<span v-else v-html="tab.icon" />
|
|
278
|
+
</span>
|
|
279
|
+
<span class="mint-modal__rail-item-text">
|
|
280
|
+
<span class="mint-modal__rail-item-label">{{ tab.label }}</span>
|
|
281
|
+
<span v-if="tab.description" class="mint-modal__rail-item-description">{{ tab.description }}</span>
|
|
282
|
+
</span>
|
|
283
|
+
<span v-if="tab.meta" class="mint-modal__rail-item-meta">{{ tab.meta }}</span>
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
<!-- Quiet rail-bottom action (e.g. Log out) -->
|
|
287
|
+
<slot name="rail-footer" />
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<section
|
|
291
|
+
class="mint-modal__pane"
|
|
292
|
+
role="tabpanel"
|
|
293
|
+
:aria-label="activeTabMeta?.label"
|
|
294
|
+
tabindex="0"
|
|
295
|
+
>
|
|
296
|
+
<header v-if="activeTabMeta?.label" class="mint-modal__pane-header">
|
|
297
|
+
<h4 class="mint-modal__pane-title">{{ activeTabMeta.label }}</h4>
|
|
298
|
+
<p v-if="activeTabMeta.description" class="mint-modal__pane-subtitle">
|
|
299
|
+
{{ activeTabMeta.description }}
|
|
300
|
+
</p>
|
|
301
|
+
</header>
|
|
302
|
+
<div class="mint-modal__pane-body">
|
|
303
|
+
<slot />
|
|
304
|
+
</div>
|
|
305
|
+
</section>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
163
308
|
<!-- Body -->
|
|
164
|
-
<div
|
|
309
|
+
<div
|
|
310
|
+
v-else
|
|
311
|
+
:class="['mint-modal__body', layout === 'tabs' ? 'mint-modal__body--tabbed' : null]"
|
|
312
|
+
>
|
|
165
313
|
<slot />
|
|
166
314
|
</div>
|
|
167
315
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Current-experiment template workspace with load/save/reset controls and WellPlate/DataFrame/DoseCalculator previews.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <BioTemplateExperimentWorkspaceView :target="collection" label="Assay" kind="collection" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed } from 'vue'
|
|
4
11
|
import {
|
|
5
12
|
extractTemplateCollection,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Complete current-experiment workspace for a curated biology template pack with save/load controls and component-prop slots.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <BioTemplatePackWorkspaceView pack="cell-culture-screen" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed, effectScope, onScopeDispose, shallowRef, unref, useSlots, watch, type EffectScope } from 'vue'
|
|
4
11
|
import type { BioTemplateEnvelope, TemplateCollectionEnvelope, TemplatePackId } from '../templates'
|
|
5
12
|
import type {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Complete editable biology template preset workspace for WellPlate/DoseCalculator, LC-MS, qPCR, generated controls, preview components, and current-experiment save wiring.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <BioTemplatePresetWorkspaceView v-model="values" preset="wellplate-screen" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed, effectScope, onScopeDispose, shallowRef, toRaw, unref, watch, type EffectScope } from 'vue'
|
|
4
11
|
import type { BioTemplateControlValues, TemplatePresetId } from '../templates'
|
|
5
12
|
import type {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Render biology template envelopes with matching SDK components such as WellPlate, DoseCalculator, and DataFrame.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <BioTemplateRenderer :target="collection" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed } from 'vue'
|
|
4
11
|
import {
|
|
5
12
|
toBioTemplateComponentBindings,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Render generated SDK component bindings such as WellPlate, DoseCalculator, DataFrame, and PlateMapEditor.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <ComponentBindingRenderer :bindings="workspace.componentBindings.value" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed, type Component } from 'vue'
|
|
4
11
|
import DataFrame from './DataFrame.vue'
|
|
5
12
|
import DoseCalculator from './DoseCalculator.vue'
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Complete control workspace component that turns a simple controls data model into AppTopBar, AppSidebar, and FormBuilder forms.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <ControlWorkspaceView v-model="values" :model="workspaceModel" title="Analysis" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed, effectScope, onScopeDispose, shallowRef, toRaw, watch, type EffectScope } from 'vue'
|
|
4
11
|
import type { TopBarVariant } from '../types'
|
|
5
12
|
import type { FormEnhancements } from '../types/form-builder'
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Complete ControlWorkspaceView page shell for WellPlate + DoseCalculator dose design.
|
|
4
|
+
*
|
|
5
|
+
* @example Recommended
|
|
6
|
+
* ```vue
|
|
7
|
+
* <DoseDesignWorkspaceView v-model="values" title="Dose Design" />
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
3
10
|
import { computed } from 'vue'
|
|
4
11
|
import type { TopBarVariant } from '../types'
|
|
5
12
|
import type { FormEnhancements } from '../types/form-builder'
|