@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.
Files changed (89) hide show
  1. package/README.md +18 -4
  2. package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
  3. package/dist/EmptyState-DgBRoYAy.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
  5. package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
  6. package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
  7. package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
  8. package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
  9. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  10. package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
  11. package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
  12. package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
  13. package/dist/components/BaseSelect.vue.d.ts +1 -1
  14. package/dist/components/BaseToggle.vue.d.ts +2 -0
  15. package/dist/components/FitPanel.vue.d.ts +1 -1
  16. package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
  17. package/dist/components/SectionCard.vue.d.ts +47 -0
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/components/index.js +6 -6
  20. package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
  21. package/dist/components-Ci_BTx00.js.map +1 -0
  22. package/dist/composables/analysisArtifactTarget.d.ts +5 -0
  23. package/dist/composables/index.d.ts +3 -1
  24. package/dist/composables/index.js +8 -8
  25. package/dist/composables/platformContextHelpers.d.ts +2 -0
  26. package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
  27. package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
  28. package/dist/composables/usePluginClient.d.ts +11 -5
  29. package/dist/{composables-B512bWJP.js → composables-C0VEJ73O.js} +40 -5
  30. package/dist/composables-C0VEJ73O.js.map +1 -0
  31. package/dist/index.js +11 -11
  32. package/dist/install.js +5 -5
  33. package/dist/styles.css +314 -17
  34. package/dist/tailwind.preset.d.ts +5 -0
  35. package/dist/tailwind.preset.js +7 -2
  36. package/dist/tailwind.preset.js.map +1 -1
  37. package/dist/templates/index.js +3 -3
  38. package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
  39. package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
  40. package/dist/types/analysisArtifactTypes.d.ts +8 -0
  41. package/dist/types/components.d.ts +2 -0
  42. package/dist/types/generated-ui.d.ts +13 -0
  43. package/dist/types/index.d.ts +2 -2
  44. package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
  45. package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
  46. package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
  47. package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
  48. package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
  49. package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
  50. package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
  51. package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
  52. package/package.json +1 -1
  53. package/src/__tests__/components/BaseSelect.test.ts +52 -0
  54. package/src/__tests__/components/BaseToggle.test.ts +40 -0
  55. package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
  56. package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
  57. package/src/__tests__/components/SectionCard.test.ts +181 -0
  58. package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
  59. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
  60. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
  61. package/src/__tests__/composables/usePluginClient.test.ts +36 -1
  62. package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
  63. package/src/components/BaseSelect.vue +16 -2
  64. package/src/components/BaseToggle.vue +3 -1
  65. package/src/components/GeneratedPluginView.vue +1 -1
  66. package/src/components/PluginWorkspaceView.props.ts +1 -1
  67. package/src/components/PluginWorkspaceView.vue +1 -5
  68. package/src/components/SectionCard.story.vue +163 -0
  69. package/src/components/SectionCard.vue +98 -0
  70. package/src/components/index.ts +1 -0
  71. package/src/composables/analysisArtifactTarget.ts +53 -0
  72. package/src/composables/index.ts +6 -0
  73. package/src/composables/platformContextHelpers.ts +10 -3
  74. package/src/composables/useAnalysisArtifacts.ts +48 -0
  75. package/src/composables/useGeneratedAnalysis.ts +30 -12
  76. package/src/composables/usePluginClient.ts +12 -5
  77. package/src/styles/components/button.css +1 -3
  78. package/src/styles/components/section-card.css +161 -0
  79. package/src/styles/index.css +1 -0
  80. package/src/styles/variables.css +25 -0
  81. package/src/tailwind.preset.ts +5 -0
  82. package/src/types/analysisArtifactTypes.ts +9 -0
  83. package/src/types/components.ts +12 -0
  84. package/src/types/generated-ui.ts +13 -0
  85. package/src/types/index.ts +3 -0
  86. package/dist/EmptyState-D1c4RdC7.js.map +0 -1
  87. package/dist/components-UfkPN1bK.js.map +0 -1
  88. package/dist/composables-B512bWJP.js.map +0 -1
  89. 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 `${definition.cpu} CPU`
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="visiblePageSelector"
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>
@@ -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
+ }
@@ -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 params = new URLSearchParams(window.location.search)
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(window.location.pathname)
44
- ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, '/'))
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,