@morscherlab/mint-sdk 1.1.0-beta.2 → 1.1.0-beta.20
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/README.md +18 -4
- package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
- package/dist/EmptyState-DgBRoYAy.js.map +1 -0
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
- package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
- package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
- package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
- package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -0
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
- package/dist/components/SectionCard.vue.d.ts +47 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +6 -6
- package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
- package/dist/components-Ci_BTx00.js.map +1 -0
- package/dist/composables/analysisArtifactTarget.d.ts +5 -0
- package/dist/composables/index.d.ts +3 -1
- package/dist/composables/index.js +8 -8
- package/dist/composables/platformContextHelpers.d.ts +2 -0
- package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
- package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
- package/dist/composables/usePluginClient.d.ts +11 -5
- package/dist/{composables-B512bWJP.js → composables-C0VEJ73O.js} +40 -5
- package/dist/composables-C0VEJ73O.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +5 -5
- package/dist/styles.css +314 -17
- package/dist/tailwind.preset.d.ts +5 -0
- package/dist/tailwind.preset.js +7 -2
- package/dist/tailwind.preset.js.map +1 -1
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
- package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +8 -0
- package/dist/types/components.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +13 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
- package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
- package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
- package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
- package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
- package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
- package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
- package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/BaseSelect.test.ts +52 -0
- package/src/__tests__/components/BaseToggle.test.ts +40 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
- package/src/__tests__/components/SectionCard.test.ts +181 -0
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
- package/src/__tests__/composables/usePluginClient.test.ts +36 -1
- package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
- package/src/components/BaseSelect.vue +16 -2
- package/src/components/BaseToggle.vue +3 -1
- package/src/components/GeneratedPluginView.vue +1 -1
- package/src/components/PluginWorkspaceView.props.ts +1 -1
- package/src/components/PluginWorkspaceView.vue +1 -5
- package/src/components/SectionCard.story.vue +163 -0
- package/src/components/SectionCard.vue +98 -0
- package/src/components/index.ts +1 -0
- package/src/composables/analysisArtifactTarget.ts +53 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/platformContextHelpers.ts +10 -3
- package/src/composables/useAnalysisArtifacts.ts +48 -0
- package/src/composables/useGeneratedAnalysis.ts +30 -12
- package/src/composables/usePluginClient.ts +12 -5
- package/src/styles/components/button.css +1 -3
- package/src/styles/components/section-card.css +161 -0
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +25 -0
- package/src/tailwind.preset.ts +5 -0
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/components.ts +12 -0
- package/src/types/generated-ui.ts +13 -0
- package/src/types/index.ts +3 -0
- package/dist/EmptyState-D1c4RdC7.js.map +0 -1
- package/dist/components-UfkPN1bK.js.map +0 -1
- package/dist/composables-B512bWJP.js.map +0 -1
- package/dist/useControlSchema-Dallso-L.js.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { GeneratedJsonSchema } from '../../types/generated-ui'
|
|
4
|
+
|
|
5
|
+
describe('GeneratedJsonSchema', () => {
|
|
6
|
+
it('should accept minimum array size constraints emitted by Pydantic', () => {
|
|
7
|
+
const schema: GeneratedJsonSchema = { minItems: 1 }
|
|
8
|
+
|
|
9
|
+
expect(schema.minItems).toBe(1)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should accept array size constraints emitted by Pydantic', () => {
|
|
13
|
+
const schema: GeneratedJsonSchema = { maxItems: 100 }
|
|
14
|
+
|
|
15
|
+
expect(schema.maxItems).toBe(100)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should accept write-only fields emitted by Pydantic', () => {
|
|
19
|
+
const schema: GeneratedJsonSchema = { writeOnly: true }
|
|
20
|
+
|
|
21
|
+
expect(schema.writeOnly).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should accept tuple item schemas emitted by Pydantic', () => {
|
|
25
|
+
const schema: GeneratedJsonSchema = {
|
|
26
|
+
prefixItems: [{ type: 'string' }, { type: 'number' }],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(schema.prefixItems).toHaveLength(2)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should accept exclusive numeric bounds emitted by Pydantic', () => {
|
|
33
|
+
const schema: GeneratedJsonSchema = { exclusiveMinimum: 0 }
|
|
34
|
+
|
|
35
|
+
expect(schema.exclusiveMinimum).toBe(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should accept string patterns emitted by Pydantic', () => {
|
|
39
|
+
const schema: GeneratedJsonSchema = { pattern: '^[0-9a-f]{64}$' }
|
|
40
|
+
|
|
41
|
+
expect(schema.pattern).toBe('^[0-9a-f]{64}$')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should accept discriminators emitted for Pydantic unions', () => {
|
|
45
|
+
const schema: GeneratedJsonSchema = {
|
|
46
|
+
discriminator: {
|
|
47
|
+
propertyName: 'kind',
|
|
48
|
+
mapping: { targeted: '#/$defs/TargetedResult' },
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
expect(schema.discriminator?.propertyName).toBe('kind')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should accept exclusive union branches emitted by Pydantic', () => {
|
|
56
|
+
const schema: GeneratedJsonSchema = {
|
|
57
|
+
oneOf: [{ $ref: '#/$defs/TargetedResult' }],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expect(schema.oneOf).toHaveLength(1)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Renders a native `<select>` element with size, error, and disabled states. */
|
|
3
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, useAttrs } from 'vue'
|
|
4
|
+
import type { HTMLAttributes } from 'vue'
|
|
4
5
|
import type { SelectOption, SelectOptionInput } from '../types'
|
|
5
6
|
import { findOptionByDomValue, normalizeOptionInput } from '../utils/options'
|
|
6
7
|
|
|
8
|
+
defineOptions({ inheritAttrs: false })
|
|
9
|
+
|
|
7
10
|
interface Props {
|
|
8
11
|
modelValue?: string | number
|
|
9
12
|
options: SelectOptionInput<string | number>[]
|
|
@@ -19,6 +22,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
19
22
|
error: false,
|
|
20
23
|
size: 'md',
|
|
21
24
|
})
|
|
25
|
+
const attrs = useAttrs()
|
|
26
|
+
|
|
27
|
+
const controlAttrs = computed(() => {
|
|
28
|
+
const { class: _class, style: _style, ...nativeAttrs } = attrs
|
|
29
|
+
return nativeAttrs
|
|
30
|
+
})
|
|
31
|
+
const wrapperAttrs = computed<Pick<HTMLAttributes, 'class' | 'style'>>(() => ({
|
|
32
|
+
class: attrs.class as HTMLAttributes['class'],
|
|
33
|
+
style: attrs.style as HTMLAttributes['style'],
|
|
34
|
+
}))
|
|
22
35
|
|
|
23
36
|
const emit = defineEmits<{
|
|
24
37
|
'update:modelValue': [value: string | number]
|
|
@@ -36,8 +49,9 @@ function handleChange(event: Event) {
|
|
|
36
49
|
</script>
|
|
37
50
|
|
|
38
51
|
<template>
|
|
39
|
-
<div class="mint-select">
|
|
52
|
+
<div v-bind="wrapperAttrs" class="mint-select">
|
|
40
53
|
<select
|
|
54
|
+
v-bind="controlAttrs"
|
|
41
55
|
:value="modelValue"
|
|
42
56
|
:disabled="disabled"
|
|
43
57
|
:aria-invalid="error || undefined"
|
|
@@ -5,6 +5,8 @@ import Tooltip from './Tooltip.vue'
|
|
|
5
5
|
interface Props {
|
|
6
6
|
modelValue?: boolean
|
|
7
7
|
label?: string
|
|
8
|
+
/** Accessible switch name; defaults to the visual label, then "Toggle". */
|
|
9
|
+
ariaLabel?: string
|
|
8
10
|
description?: string
|
|
9
11
|
icon?: string | string[]
|
|
10
12
|
iconColor?: string
|
|
@@ -111,7 +113,7 @@ function iconStyle(): Record<string, string> {
|
|
|
111
113
|
role="switch"
|
|
112
114
|
:tabindex="disabled ? -1 : 0"
|
|
113
115
|
:aria-checked="modelValue"
|
|
114
|
-
:aria-label="label || 'Toggle'"
|
|
116
|
+
:aria-label="ariaLabel || label || 'Toggle'"
|
|
115
117
|
:class="[
|
|
116
118
|
'mint-toggle__track',
|
|
117
119
|
`mint-toggle__track--${size}`,
|
|
@@ -116,7 +116,7 @@ function resourceSummary(definition: GeneratedJobDefinition): string {
|
|
|
116
116
|
if (definition.lifecycle === 'staged' || definition.profile?.kind === 'staged') {
|
|
117
117
|
return 'Staged upload'
|
|
118
118
|
}
|
|
119
|
-
return
|
|
119
|
+
return `Up to ${definition.cpu} CPU`
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
onMounted(() => {
|
|
@@ -34,7 +34,7 @@ export interface PluginWorkspaceViewProps {
|
|
|
34
34
|
homePath?: string
|
|
35
35
|
/** Show the default MINT logo when no icon/logo slot is provided. */
|
|
36
36
|
showLogo?: boolean
|
|
37
|
-
/** Preferred route-level page selector entries. */
|
|
37
|
+
/** Preferred route-level page selector entries. Pass [] to suppress integrated platform fallback. */
|
|
38
38
|
pageSelector?: PageSelectorItemInput[]
|
|
39
39
|
/** Active page selector id. Defaults to activeView. */
|
|
40
40
|
currentPageSelectorId?: string
|
|
@@ -208,10 +208,6 @@ const {
|
|
|
208
208
|
emitPillSelect: (item) => emit('pill-select', item),
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
-
const visiblePageSelector = computed(() =>
|
|
212
|
-
(props.pageSelector?.length ?? 0) > 1 ? props.pageSelector : undefined,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
211
|
const hasSidebarSurface = computed(() => {
|
|
216
212
|
return hasPluginWorkspaceSidebarSurface({
|
|
217
213
|
showSidebar: props.showSidebar,
|
|
@@ -274,7 +270,7 @@ function handleFormCancel(sectionId: string) {
|
|
|
274
270
|
:variant="topBarVariant"
|
|
275
271
|
:home-path="homePath"
|
|
276
272
|
:show-logo="showLogo"
|
|
277
|
-
:page-selector="
|
|
273
|
+
:page-selector="pageSelector"
|
|
278
274
|
:current-page-selector-id="resolvedCurrentPageSelectorId"
|
|
279
275
|
:plugin-switcher="pluginSwitcher"
|
|
280
276
|
:pill-nav="resolvedPillNav"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import SectionCard from './SectionCard.vue'
|
|
3
|
+
import type { SectionCardAccent, SectionCardDivider } from '../types'
|
|
4
|
+
|
|
5
|
+
const flaskIcon =
|
|
6
|
+
'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4'
|
|
7
|
+
const folderIcon = 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z'
|
|
8
|
+
|
|
9
|
+
const experiments = [
|
|
10
|
+
{ code: 'MM-2026-014', name: '13C glucose tracing — HeLa, 24h', updated: '2h ago' },
|
|
11
|
+
{ code: 'MM-2026-013', name: 'Hypoxia timecourse — passage 6', updated: '5h ago' },
|
|
12
|
+
{ code: 'MM-2026-011', name: 'Glutamine dropout, triplicate', updated: 'yesterday' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function initPlayground() {
|
|
16
|
+
return {
|
|
17
|
+
title: 'Experiments',
|
|
18
|
+
count: '14 ongoing · 42 total',
|
|
19
|
+
accent: 'experiment' as SectionCardAccent,
|
|
20
|
+
uppercase: true,
|
|
21
|
+
divider: 'strong' as SectionCardDivider,
|
|
22
|
+
showIcon: true,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<Story title="Layout/SectionCard">
|
|
29
|
+
<Variant title="Playground" :init-state="initPlayground">
|
|
30
|
+
<template #default="{ state }">
|
|
31
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
32
|
+
<SectionCard
|
|
33
|
+
:title="state.title"
|
|
34
|
+
:count="state.count || undefined"
|
|
35
|
+
:icon="state.showIcon ? flaskIcon : undefined"
|
|
36
|
+
:accent="state.accent"
|
|
37
|
+
:uppercase="state.uppercase"
|
|
38
|
+
:divider="state.divider"
|
|
39
|
+
>
|
|
40
|
+
<div class="mint-section-card__rows">
|
|
41
|
+
<a
|
|
42
|
+
v-for="e in experiments"
|
|
43
|
+
:key="e.code"
|
|
44
|
+
href="#"
|
|
45
|
+
class="mint-section-card__row"
|
|
46
|
+
>
|
|
47
|
+
<span style="flex: 1; min-width: 0">
|
|
48
|
+
<span style="display: block; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">{{ e.name }}</span>
|
|
49
|
+
<span style="display: block; font-family: 'Fira Code', monospace; font-size: 0.65625rem; color: var(--text-muted)">{{ e.code }}</span>
|
|
50
|
+
</span>
|
|
51
|
+
<span style="flex-shrink: 0; font-size: 0.71875rem; color: var(--text-muted)">{{ e.updated }}</span>
|
|
52
|
+
</a>
|
|
53
|
+
</div>
|
|
54
|
+
</SectionCard>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #controls="{ state }">
|
|
59
|
+
<HstText v-model="state.title" title="Title" />
|
|
60
|
+
<HstText v-model="state.count" title="Count" />
|
|
61
|
+
<HstCheckbox v-model="state.showIcon" title="Show icon chip" />
|
|
62
|
+
<HstCheckbox v-model="state.uppercase" title="Uppercase title" />
|
|
63
|
+
<HstSelect
|
|
64
|
+
v-model="state.accent"
|
|
65
|
+
title="Accent"
|
|
66
|
+
:options="{
|
|
67
|
+
primary: 'primary',
|
|
68
|
+
experiment: 'experiment',
|
|
69
|
+
project: 'project',
|
|
70
|
+
success: 'success',
|
|
71
|
+
error: 'error',
|
|
72
|
+
warning: 'warning',
|
|
73
|
+
info: 'info',
|
|
74
|
+
}"
|
|
75
|
+
/>
|
|
76
|
+
<HstSelect
|
|
77
|
+
v-model="state.divider"
|
|
78
|
+
title="Divider"
|
|
79
|
+
:options="{ strong: 'strong', light: 'light' }"
|
|
80
|
+
/>
|
|
81
|
+
</template>
|
|
82
|
+
</Variant>
|
|
83
|
+
|
|
84
|
+
<Variant title="Home category card">
|
|
85
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
86
|
+
<SectionCard
|
|
87
|
+
title="Projects"
|
|
88
|
+
count="6 active · 9 total"
|
|
89
|
+
:icon="folderIcon"
|
|
90
|
+
accent="project"
|
|
91
|
+
uppercase
|
|
92
|
+
>
|
|
93
|
+
<template #actions>
|
|
94
|
+
<a href="#" style="font-size: 0.75rem; font-weight: 500; color: var(--color-primary-hover)">All →</a>
|
|
95
|
+
</template>
|
|
96
|
+
<div class="mint-section-card__rows">
|
|
97
|
+
<a href="#" class="mint-section-card__row">
|
|
98
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Hypoxia metabolic rewiring</span>
|
|
99
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">12 experiments</span>
|
|
100
|
+
</a>
|
|
101
|
+
<a href="#" class="mint-section-card__row">
|
|
102
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Serine one-carbon flux</span>
|
|
103
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">7 experiments</span>
|
|
104
|
+
</a>
|
|
105
|
+
</div>
|
|
106
|
+
</SectionCard>
|
|
107
|
+
</div>
|
|
108
|
+
</Variant>
|
|
109
|
+
|
|
110
|
+
<Variant title="Detail card with footer">
|
|
111
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
112
|
+
<SectionCard title="Analysis artifacts" :count="4" divider="light">
|
|
113
|
+
<div class="mint-section-card__rows">
|
|
114
|
+
<div class="mint-section-card__row">
|
|
115
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">isotopologue_matrix.csv</span>
|
|
116
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="mint-section-card__row">
|
|
119
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">natural_abundance_corrected.parquet</span>
|
|
120
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<template #footer>
|
|
124
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–2 of 4</span>
|
|
125
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 2</span>
|
|
126
|
+
</template>
|
|
127
|
+
</SectionCard>
|
|
128
|
+
</div>
|
|
129
|
+
</Variant>
|
|
130
|
+
|
|
131
|
+
<Variant title="Fill with pinned toolbar">
|
|
132
|
+
<div style="padding: 2rem; height: 420px; display: flex;">
|
|
133
|
+
<SectionCard title="All experiments" :count="42" divider="light" fill>
|
|
134
|
+
<template #toolbar>
|
|
135
|
+
<div style="display: flex; gap: 0.5rem; padding: 0.6875rem 1rem; border-bottom: 1px solid var(--border-color)">
|
|
136
|
+
<span style="font-size: 0.78125rem; color: var(--text-secondary)">Toolbar stays put while the rows scroll</span>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
139
|
+
<div class="mint-section-card__rows">
|
|
140
|
+
<div v-for="n in 20" :key="n" class="mint-section-card__row">
|
|
141
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">Row {{ n }}</span>
|
|
142
|
+
<span style="font-family: 'Fira Code', monospace; font-size: 0.71875rem; color: var(--text-muted)">MM-2026-{{ String(n).padStart(3, '0') }}</span>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<template #footer>
|
|
146
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–20 of 42</span>
|
|
147
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 3</span>
|
|
148
|
+
</template>
|
|
149
|
+
</SectionCard>
|
|
150
|
+
</div>
|
|
151
|
+
</Variant>
|
|
152
|
+
|
|
153
|
+
<Variant title="No icon, plain title">
|
|
154
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
155
|
+
<SectionCard title="Design data" count="96 samples · 4 timepoints" divider="light">
|
|
156
|
+
<div style="padding: 1rem; font-size: 0.8125rem; color: var(--text-secondary)">
|
|
157
|
+
Card body content sits directly on the card surface — no nested card.
|
|
158
|
+
</div>
|
|
159
|
+
</SectionCard>
|
|
160
|
+
</div>
|
|
161
|
+
</Variant>
|
|
162
|
+
</Story>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** One-card page region from the MINT 1.1 card system: 12px radius, the
|
|
3
|
+
* single --card-shadow token, a header strip (icon chip, title, mono count,
|
|
4
|
+
* actions), and hairline-separated content — no card-in-card. */
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
import type { SectionCardAccent, SectionCardDivider } from '../types'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string
|
|
10
|
+
/** Monospace meta shown next to the title, e.g. "14 ongoing · 42 total". */
|
|
11
|
+
count?: string | number
|
|
12
|
+
/** SVG path(s) for the 22px icon chip; omit to render no chip. */
|
|
13
|
+
icon?: string | string[]
|
|
14
|
+
/** Tint of the icon chip. */
|
|
15
|
+
accent?: SectionCardAccent
|
|
16
|
+
/** Uppercase category heading (home cards) vs plain heading (detail cards). */
|
|
17
|
+
uppercase?: boolean
|
|
18
|
+
/** Header hairline: 'strong' = --border-color, 'light' = --border-light. */
|
|
19
|
+
divider?: SectionCardDivider
|
|
20
|
+
/**
|
|
21
|
+
* Stretch to fill the height its parent offers, scrolling the body and
|
|
22
|
+
* pinning the header and footer. Requires a parent that gives the card a
|
|
23
|
+
* definite height (a flex or grid track).
|
|
24
|
+
*/
|
|
25
|
+
fill?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
count: undefined,
|
|
30
|
+
icon: undefined,
|
|
31
|
+
accent: 'primary',
|
|
32
|
+
uppercase: false,
|
|
33
|
+
divider: 'strong',
|
|
34
|
+
fill: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const iconPaths = computed(() =>
|
|
38
|
+
props.icon === undefined ? [] : Array.isArray(props.icon) ? props.icon : [props.icon],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const headerClasses = computed(() => [
|
|
42
|
+
'mint-section-card__header',
|
|
43
|
+
props.divider === 'light' ? 'mint-section-card__header--divider-light' : '',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const titleClasses = computed(() => [
|
|
47
|
+
'mint-section-card__title',
|
|
48
|
+
props.uppercase ? 'mint-section-card__title--uppercase' : '',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const cardClasses = computed(() => [
|
|
52
|
+
'mint-section-card',
|
|
53
|
+
props.fill ? 'mint-section-card--fill' : '',
|
|
54
|
+
])
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<section :class="cardClasses">
|
|
59
|
+
<div :class="headerClasses">
|
|
60
|
+
<span
|
|
61
|
+
v-if="iconPaths.length"
|
|
62
|
+
:class="['mint-section-card__icon-chip', `mint-section-card__icon-chip--${accent}`]"
|
|
63
|
+
>
|
|
64
|
+
<svg
|
|
65
|
+
class="mint-section-card__icon"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
fill="none"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="2"
|
|
70
|
+
stroke-linecap="round"
|
|
71
|
+
stroke-linejoin="round"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path v-for="(d, i) in iconPaths" :key="i" :d="d" />
|
|
75
|
+
</svg>
|
|
76
|
+
</span>
|
|
77
|
+
<h2 :class="titleClasses">{{ title }}</h2>
|
|
78
|
+
<span v-if="count !== undefined" class="mint-section-card__count">{{ count }}</span>
|
|
79
|
+
<div class="mint-section-card__spacer"></div>
|
|
80
|
+
<slot name="actions" />
|
|
81
|
+
</div>
|
|
82
|
+
<!-- Controls belong to the card's chrome, so they sit outside the body and
|
|
83
|
+
stay put while the body scrolls. -->
|
|
84
|
+
<div v-if="$slots.toolbar" class="mint-section-card__toolbar">
|
|
85
|
+
<slot name="toolbar" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="mint-section-card__body">
|
|
88
|
+
<slot />
|
|
89
|
+
</div>
|
|
90
|
+
<div v-if="$slots.footer" class="mint-section-card__footer">
|
|
91
|
+
<slot name="footer" />
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
@import '../styles/components/section-card.css';
|
|
98
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -126,6 +126,7 @@ export { default as DateTimePicker } from './DateTimePicker.vue'
|
|
|
126
126
|
export { default as TimeRangeInput } from './TimeRangeInput.vue'
|
|
127
127
|
export { default as ScheduleCalendar } from './ScheduleCalendar.vue'
|
|
128
128
|
export { default as ResourceCard } from './ResourceCard.vue'
|
|
129
|
+
export { default as SectionCard } from './SectionCard.vue'
|
|
129
130
|
|
|
130
131
|
// Experiment / analysis components
|
|
131
132
|
export { default as ExperimentSelectorModal } from './ExperimentSelectorModal.vue'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AnalysisArtifactTarget } from '../types/analysisArtifactTypes'
|
|
2
|
+
|
|
3
|
+
const EXPERIMENT_ID = 'experiment_id'
|
|
4
|
+
const ARTIFACT_PLUGIN_ID = 'artifact_plugin_id'
|
|
5
|
+
const ARTIFACT_KEY = 'artifact_key'
|
|
6
|
+
const ARTIFACT_ID = 'artifact_id'
|
|
7
|
+
|
|
8
|
+
function parsePositiveInteger(value: string | null): number | undefined {
|
|
9
|
+
if (value === null || !/^\d+$/.test(value)) return undefined
|
|
10
|
+
const parsed = Number(value)
|
|
11
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Build the canonical query used to open one analysis artifact in a plugin. */
|
|
15
|
+
export function buildAnalysisArtifactTargetQuery(
|
|
16
|
+
target: AnalysisArtifactTarget,
|
|
17
|
+
): URLSearchParams {
|
|
18
|
+
const query = new URLSearchParams()
|
|
19
|
+
query.set(EXPERIMENT_ID, String(target.experimentId))
|
|
20
|
+
query.set(ARTIFACT_PLUGIN_ID, target.artifactPluginId)
|
|
21
|
+
query.set(ARTIFACT_KEY, target.artifactKey)
|
|
22
|
+
if (target.artifactId !== undefined) {
|
|
23
|
+
query.set(ARTIFACT_ID, String(target.artifactId))
|
|
24
|
+
}
|
|
25
|
+
return query
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse a complete canonical target. Incomplete or invalid input is not guessed. */
|
|
29
|
+
export function parseAnalysisArtifactTarget(
|
|
30
|
+
query: URLSearchParams,
|
|
31
|
+
): AnalysisArtifactTarget | null {
|
|
32
|
+
const experimentId = parsePositiveInteger(query.get(EXPERIMENT_ID))
|
|
33
|
+
const artifactPluginId = query.get(ARTIFACT_PLUGIN_ID)?.trim()
|
|
34
|
+
const artifactKey = query.get(ARTIFACT_KEY)?.trim()
|
|
35
|
+
const rawArtifactId = query.get(ARTIFACT_ID)
|
|
36
|
+
const artifactId = parsePositiveInteger(rawArtifactId)
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
experimentId === undefined
|
|
40
|
+
|| !artifactPluginId
|
|
41
|
+
|| !artifactKey
|
|
42
|
+
|| (rawArtifactId !== null && artifactId === undefined)
|
|
43
|
+
) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
experimentId,
|
|
49
|
+
artifactPluginId,
|
|
50
|
+
artifactKey,
|
|
51
|
+
...(artifactId === undefined ? {} : { artifactId }),
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -159,6 +159,11 @@ export {
|
|
|
159
159
|
type UseAnalysisArtifactsOptions,
|
|
160
160
|
type UseAnalysisArtifactsReturn,
|
|
161
161
|
} from './useAnalysisArtifacts'
|
|
162
|
+
export {
|
|
163
|
+
buildAnalysisArtifactTargetQuery,
|
|
164
|
+
parseAnalysisArtifactTarget,
|
|
165
|
+
} from './analysisArtifactTarget'
|
|
166
|
+
export { resolveCurrentPluginId } from './platformContextHelpers'
|
|
162
167
|
export {
|
|
163
168
|
formatExperimentDate,
|
|
164
169
|
formatExperimentStatus,
|
|
@@ -476,6 +481,7 @@ export {
|
|
|
476
481
|
type DownloadBlobOptions,
|
|
477
482
|
type DownloadPluginEndpointOptions,
|
|
478
483
|
type PluginContract,
|
|
484
|
+
type PluginContractSettings,
|
|
479
485
|
type PluginClientRequestOptions,
|
|
480
486
|
type PluginEndpointContract,
|
|
481
487
|
type PluginEndpointDefinition,
|
|
@@ -12,6 +12,12 @@ export function getInjectedPlatformContext(): PlatformContext | undefined {
|
|
|
12
12
|
return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Return the platform's canonical plugin identity for the mounted frontend. */
|
|
16
|
+
export function resolveCurrentPluginId(): string | undefined {
|
|
17
|
+
const pluginId = getInjectedPlatformContext()?.plugin?.id
|
|
18
|
+
return typeof pluginId === 'string' && pluginId ? pluginId : undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export function getInjectedExperimentContext(): ExperimentAwarePlatformContext | undefined {
|
|
16
22
|
return getInjectedPlatformContext() as ExperimentAwarePlatformContext | undefined
|
|
17
23
|
}
|
|
@@ -38,10 +44,11 @@ export function currentExperimentIdFromContext(
|
|
|
38
44
|
|
|
39
45
|
export function currentExperimentIdFromUrl(): number | undefined {
|
|
40
46
|
if (typeof window === 'undefined') return undefined
|
|
41
|
-
const
|
|
47
|
+
const location = window.location
|
|
48
|
+
const params = new URLSearchParams(location?.search ?? '')
|
|
42
49
|
return parseExperimentId(params.get('experimentId') ?? params.get('experiment_id'))
|
|
43
|
-
?? experimentIdFromPath(
|
|
44
|
-
?? experimentIdFromPath(
|
|
50
|
+
?? experimentIdFromPath(location?.pathname ?? '')
|
|
51
|
+
?? experimentIdFromPath((location?.hash ?? '').replace(/^#\/?/, '/'))
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export function resolveCurrentExperimentId(
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
AnalysisArtifactMetadataUpdate,
|
|
20
20
|
AnalysisArtifactStatusFilter,
|
|
21
21
|
AnalysisArtifactSummary,
|
|
22
|
+
AnalysisArtifactTarget,
|
|
22
23
|
} from '../types/analysisArtifactTypes'
|
|
23
24
|
|
|
24
25
|
export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
|
|
@@ -67,6 +68,8 @@ export interface UseAnalysisArtifactsReturn {
|
|
|
67
68
|
refresh: () => Promise<AnalysisArtifactSummary[]>
|
|
68
69
|
/** Fetch one artifact with its result payload; null when it fails or is superseded. */
|
|
69
70
|
getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
|
|
71
|
+
/** Resolve and fetch the exact stable artifact identity. */
|
|
72
|
+
resolveTarget: (target: AnalysisArtifactTarget) => Promise<AnalysisArtifactDetail | null>
|
|
70
73
|
/** Update display_name and/or note; resolves to the updated summary or null. */
|
|
71
74
|
updateMetadata: (
|
|
72
75
|
artifactId: number,
|
|
@@ -206,6 +209,50 @@ export function useAnalysisArtifacts(
|
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
|
|
212
|
+
async function resolveTarget(
|
|
213
|
+
target: AnalysisArtifactTarget,
|
|
214
|
+
): Promise<AnalysisArtifactDetail | null> {
|
|
215
|
+
const experimentId = currentExperimentId.value
|
|
216
|
+
if (experimentId !== target.experimentId) {
|
|
217
|
+
request.setError(
|
|
218
|
+
`Analysis artifact target belongs to experiment ${target.experimentId}, not `
|
|
219
|
+
+ `${experimentId ?? 'the current selection'}.`,
|
|
220
|
+
)
|
|
221
|
+
return null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const available = await list()
|
|
225
|
+
const summary = available.find((artifact) => (
|
|
226
|
+
artifact.plugin_id === target.artifactPluginId
|
|
227
|
+
&& artifact.artifact_key === target.artifactKey
|
|
228
|
+
&& artifact.status === 'active'
|
|
229
|
+
))
|
|
230
|
+
if (!summary) {
|
|
231
|
+
request.setError(
|
|
232
|
+
`Analysis artifact ${target.artifactPluginId}/${target.artifactKey} is not available.`,
|
|
233
|
+
)
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const detail = await getDetail(summary.id)
|
|
238
|
+
if (
|
|
239
|
+
detail
|
|
240
|
+
&& (
|
|
241
|
+
detail.experiment_id !== target.experimentId
|
|
242
|
+
|| detail.plugin_id !== target.artifactPluginId
|
|
243
|
+
|| detail.artifact_key !== target.artifactKey
|
|
244
|
+
)
|
|
245
|
+
) {
|
|
246
|
+
selectedDetail.value = null
|
|
247
|
+
request.setError(
|
|
248
|
+
`Analysis artifact ${target.artifactPluginId}/${target.artifactKey} did not `
|
|
249
|
+
+ 'match the requested identity.',
|
|
250
|
+
)
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
return detail
|
|
254
|
+
}
|
|
255
|
+
|
|
209
256
|
async function mutate(
|
|
210
257
|
action: string,
|
|
211
258
|
operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
|
|
@@ -296,6 +343,7 @@ export function useAnalysisArtifacts(
|
|
|
296
343
|
list,
|
|
297
344
|
refresh: list,
|
|
298
345
|
getDetail,
|
|
346
|
+
resolveTarget,
|
|
299
347
|
updateMetadata,
|
|
300
348
|
archive,
|
|
301
349
|
restore,
|