@morscherlab/mint-sdk
v1.1.24
Published
MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.
Maintainers
Readme
@morscherlab/mint-sdk (Frontend)
Vue 3 SDK for MINT Platform plugin development. Provides reusable components, composables, stores, and types for building consistent, well-integrated plugins.
Full Documentation: See the comprehensive docs for detailed API reference and guides.
- API Reference
- Component Catalog - All public components with props/events
- Composables Reference - All public composables with examples
- Theming Guide - CSS variables and customization
Installation
bun add @morscherlab/mint-sdkRequirements
- Vue 3.4+
- Pinia 2.1+
- Tailwind CSS 4.0+ (optional - SDK includes pre-built utility classes)
Quick Start
1. Import Styles
Import the SDK styles in your main entry point:
// main.ts
import '@morscherlab/mint-sdk/styles'The SDK includes all necessary CSS utility classes pre-built, so Tailwind CSS is optional. The styles work standalone or alongside Tailwind.
2. (Optional) Configure Tailwind
If you're using Tailwind CSS and want to extend the MINT theme:
import mintPreset from '@morscherlab/mint-sdk/tailwind.preset'
export default {
presets: [mintPreset],
content: [
'./src/**/*.vue',
'./node_modules/@morscherlab/mint-sdk/**/*.vue',
],
}3. Use Components
<script setup lang="ts">
import { BaseButton, BaseInput, FormField } from '@morscherlab/mint-sdk'
import { useToast, useAuth } from '@morscherlab/mint-sdk'
import type { ButtonVariant } from '@morscherlab/mint-sdk'
const { success } = useToast()
const { login } = useAuth()
async function handleSubmit() {
const result = await login(username, password)
if (result) {
success('Login successful!')
}
}
</script>
<template>
<FormField label="Username" required>
<BaseInput v-model="username" placeholder="Enter username" />
</FormField>
<BaseButton variant="primary" @click="handleSubmit">
Login
</BaseButton>
</template>4. Use Generated Plugin Clients
For plugin frontends scaffolded by mint init, run this from the plugin root:
mint sdk generateUse mint sdk generate --check --json when CI or editor tasks need machine-readable contract drift status.
Then call backend routes through the generated client:
import { useGeneratedPluginClient } from './generated/mint-plugin'
const pluginClient = useGeneratedPluginClient()
const health = await pluginClient.health()
// Cancel a generated request without bypassing SDK authentication.
const controller = new AbortController()
const result = await pluginClient.analyze(
{ body: { parameters: { threshold: '0.05' } } },
{ signal: controller.signal },
)Generated files also expose typed plugin settings when the backend declares
@mint_plugin(config=PluginConfig):
import {
pluginSettingsFormSchema,
pluginSettingsValueSchema,
useGeneratedPluginSettings,
} from './generated/mint-plugin'
const pluginSettings = useGeneratedPluginSettings()
const threshold = pluginSettings.settings.value?.threshold
const settingsConfig = pluginSettings.settingsConfigPass settingsConfig to PluginWorkspaceView or AppTopBar.
pluginSettingsValueSchema is the Pydantic JSON Schema used for generated
TypeScript values. pluginSettingsFormSchema is the SDK
SettingsModalSchema used to render controls; they are deliberately separate.
When .env, process-environment, or startup values override a field, saving
still updates its backing value. The effective override remains active, while
the saved value is returned in stored_settings and marked inactive until
that higher-priority override is removed.
The generated client is built from backend plugin metadata, FastAPI routes, and Pydantic schemas, and uses the frontend SDK to resolve the API prefix automatically.
FastAPI request or response content declared as standard
application/octet-stream is generated as a raw binary transport: request
bodies accept Blob | ArrayBuffer | Uint8Array, responses resolve to Blob,
and the SDK does not JSON-encode the payload. Multipart
type: string, format: binary fields remain typed as File | Blob.
Routes with an experimentId path parameter can omit it inside an experiment page; the client reads the current platform experiment from injected context or the URL:
await pluginClient.analyze({ body: { parameters: { threshold: '0.05' } } })Use useCurrentExperiment() when a plugin view needs the platform experiment selected in the URL or injected context:
import { useCurrentExperiment } from '@morscherlab/mint-sdk'
const {
experimentId,
hasExperiment,
requireExperimentId,
experiment,
isLoading,
refresh,
} = useCurrentExperiment()
async function saveCurrentExperiment() {
if (!hasExperiment.value) return
const id = requireExperimentId()
// Use id for platform APIs or explicit cross-experiment overrides.
}5. Use Biology Data Templates
Plugin frontends can import standard biology template builders and adapters instead of inventing ad-hoc JSON shapes:
import {
createWellPlateScreenCollection,
createTemplateCollection,
createPlateMapTemplate,
createSampleSheetTemplate,
ensureTemplateFromCollection,
listBioTemplateCatalog,
listBioTemplatePresets,
toPlateMapEditorState,
toTemplateDataFrame,
} from '@morscherlab/mint-sdk/templates'
const catalog = listBioTemplateCatalog()
const presets = listBioTemplatePresets()
const template = createPlateMapTemplate({
samples: ['Control', 'Treatment'],
})
const plateEditorState = toPlateMapEditorState(template)
const sampleFrame = toTemplateDataFrame(createSampleSheetTemplate({
samples: [{ sampleId: 'S001', name: 'Control 1' }],
}))
// <DataFrame v-bind="sampleFrame" />
const collection = createTemplateCollection([template])
const loadedPlate = ensureTemplateFromCollection(collection, 'plate-map')
const screenCollection = createWellPlateScreenCollection({
samples: ['Control', 'Treatment'],
compounds: { 'Drug A': [10, 1, 0.1] },
unit: 'uM',
})For frontend wiring, useBioTemplateWorkspace() combines the generated
controls, sidebar/forms, renderer props, and SDK component bindings:
import { useBioTemplateWorkspace } from '@morscherlab/mint-sdk'
import { createWellPlateScreenCollection } from '@morscherlab/mint-sdk/templates'
const collection = createWellPlateScreenCollection({
samples: ['Control', 'Treatment'],
compounds: { 'Drug A': [10, 1, 0.1] },
})
const workspace = useBioTemplateWorkspace(collection)
// <AppSidebar v-bind="workspace.sidebar" />
// <FormBuilder v-bind="workspace.form" />
// <BioTemplateRenderer v-if="workspace.renderer" v-bind="workspace.renderer" />For editable built-in presets, useBioTemplatePresetWorkspace() also manages
the generated control values, collection updates, and current-experiment save:
import { BioTemplatePresetWorkspaceView, useBioTemplatePresetWorkspace } from '@morscherlab/mint-sdk'
const workspace = useBioTemplatePresetWorkspace('wellplate-screen')
// <BioTemplatePresetWorkspaceView :workspace="workspace" />For custom plugin tools, pass one workspace model to ControlWorkspaceView to
generate AppTopBar, AppSidebar, FormBuilder, component prop bindings, and
their shared values from the same source:
<script setup lang="ts">
import { ref } from 'vue'
import { ControlWorkspaceView, defineControlModel } from '@morscherlab/mint-sdk'
const workspaceModel = defineControlModel({
views: {
analysis: {
label: 'Analysis',
sections: {
parameters: {
label: 'Parameters',
controls: {
threshold: { type: 'number', default: 0.05, min: 0, max: 1 },
method: ['linear', 'logistic'],
saveResults: true,
},
},
},
},
},
})
const values = ref({})
</script>
<template>
<ControlWorkspaceView v-model="values" :model="workspaceModel" title="Analysis" />
</template>Use defineControls() for flat forms, and use useControlWorkspace()
directly only when a plugin needs full manual control over the generated
AppTopBar, AppSidebar, or FormBuilder bindings.
For WellPlate + DoseCalculator pages, start from the dose-design preset instead of writing the sidebar controls and component prop mappings yourself:
<script setup lang="ts">
import { ControlWorkspaceView, DoseCalculator, WellPlate, defineDoseDesignControlModel } from '@morscherlab/mint-sdk'
const workspaceModel = defineDoseDesignControlModel({
selectedWells: ['A1', 'A2'],
includeMolecularWeight: true,
})
</script>
<template>
<ControlWorkspaceView :model="workspaceModel" title="Dose Design" v-slot="{ componentPropsById }">
<WellPlate v-bind="componentPropsById.plate" />
<DoseCalculator v-bind="componentPropsById.dose" />
</ControlWorkspaceView>
</template>Built-in templates include plate-map, sample-sheet, sample-prep,
dose-response, calibration-curve, time-course, protocol-steps, assay-matrix,
reagent-list, flow-cytometry-panel, instrument-run, and qpcr-plate. In
plugin projects, mint add data-template <name> --page generates the
matching backend route, typed frontend composable, and starter view.
Generated composables use ensureTemplateEnvelope() to validate backend
template payloads before assigning typed frontend state. Use
createTemplateCollection(), extractTemplateCollection(), and
ensureTemplateFromCollection() when a plugin view reads or writes the shared
design_data.templates layout. mint add data-template-pack <name> --page
generates a pack-level composable that uses these helpers for curated
experiment scaffolds.
Use mint add data-template-pack <name> --page for curated sets such as
cell-culture-screen, omics-assay, longitudinal-study, and
molecular-assay.
For no-scaffold defaults that can be saved directly to design_data, use
template presets such as createWellPlateScreenCollection(),
createQpcrExpressionCollection(), createLcmsBatchCollection(), and
createWesternBlotAssayCollection().
Components
Base Components
BaseButton- Button with variants (primary, secondary, cta, danger, success, ghost)BaseInput- Text input with validation statesBaseSelect- Dropdown select with generic typingBaseTabs- Tab navigation with underline and pills variantsBaseModal- Modal dialog with Teleport
Form Components
FormField- Form field wrapper with label, error, and hintFormBuilder- Schema-driven forms from fullschemaobjects or compactcontrols
Feedback Components
AlertBox- Alert messages (success, error, warning, info)AppToastContainer- Toast notification container for the globaluseToast()queue
Action Components
IconButton- Icon-only button with accessibilityThemeToggle- Light/dark mode toggle
Layout Components
CollapsibleCard- Expandable card sections with optional icon badge and toggle switchAppTopBar- Application header with slotsAppSidebar- Context sidebar that accepts panels/forms or a compactcontrolsschema
Biological Template Components
BioTemplateRenderer- Render template envelopes or collections with matching SDK componentsBioTemplateExperimentWorkspaceView- Current-experiment load/save/reset workspace for one template or collectionBioTemplatePresetWorkspaceView- Full editable preset workspace fromuseBioTemplatePresetWorkspace()
File & Data Components
FileUploader- Drag-and-drop file upload with file/folder mode support
Sample Management Components
SampleSelector- Hierarchical sample grouping with auto-group, CSV metadata import, and optional experimentdesign_datasample loadingAutoGroupModal- Smart sample grouping wizard for names and design metadataGroupAssigner- Drag-and-drop group assignment for comparisons (e.g., Control vs Treatment)
Removed Compatibility APIs
The public SDK no longer exports ToastNotification, GroupingModal,
SettingsButton, FormSection, FormFieldRenderer, or usePluginApi().
Use AppToastContainer, AutoGroupModal, AppTopBar settings or
SettingsModal, FormBuilder, and generated useGeneratedPluginClient()
helpers instead. mint doctor still detects old imports and reports the
replacement path.
Composables
usePluginJobs(options)
Generated plugin contracts wrap this composable with definition-specific
input, job, and output types. For a component dedicated to one generated job,
bind the definition once with forDefinition():
import { usePluginJobs } from './generated/mint-plugin'
const jobs = usePluginJobs()
await jobs.ready()
const analyzeJobs = jobs.forDefinition('analyze')
const job = await analyzeJobs.submit({
source: new File(['peak\n1'], 'peaks.csv'),
threshold: 0.1,
})
const result = await analyzeJobs.loadResult(job)
const canonicalJob = await analyzeJobs.getJob(job.job_id)
const completedJob = await analyzeJobs.waitForJob(canonicalJob, {
signal: abortController.signal,
rejectOnFailure: true,
})
await analyzeJobs.cancelJob(canonicalJob)
// Releases only finished `analyze` jobs and their retained results.
await analyzeJobs.clearFinishedJobs()The returned scope exposes a typed jobs getter, isJob() type guard,
submit(), getJob(), waitForJob(), loadResult(), cancelJob(), deleteJob(), and
definition-scoped clearFinishedJobs(). It also exposes the generated
lifecycle and profile literals for that definition without adding runtime
requests. Passing a job from another definition
fails instead of crossing the scope. Capability fields are presentation hints;
the server remains authoritative when scoped cancel/delete methods are called.
The root wrapper still supports dynamic definition IDs through submit() and
jobsFor(). Its no-argument clearFinishedJobs() intentionally clears
terminal jobs across every definition.
The underlying frontend composable accepts an optional definition ID in
clearFinishedJobs(definitionId); generated clients expose the scoped method
so static definition IDs retain their generated input, job, and output types.
getJob() fetches one fresh canonical job without refreshing the list.
waitForJob() settles from the same SSE/poll-backed canonical source and
cleans up on abort, deletion, or disposal, so callers do not need a second
event stream or Promise registry. A call made while the runtime is paused
rejects immediately. Waiting on a known active job tolerates a transient direct
lookup failure and continues through SSE/poll updates; pausing an existing wait
suspends those updates until resume().
cancelJob() updates active work, while deleteJob() releases one terminal
job and its retained result. Background work can be pause()d and resume()d,
while dispose() is idempotent and also runs automatically with the owning Vue
effect scope.
Both ordinary uploadPath({ ..., signal }) and a staged scope's
uploadPart(job, { ..., signal }) forward the supplied AbortSignal to the
authenticated multipart request. Abort stops the client-side transfer, but the
caller should refresh job/staging state before retrying because the server may
already have committed the upload.
Automatic submission binds ordinary Path uploads to its job definition. When
uploading manually for an administrator-protected definition, call
uploadPath(input, definitionId) so the server can authorize and bind the
upload before reading its body.
cancelJob() and deleteJob() accept either a canonical job object or its ID.
Pass the runtime's canonical source directly to standard job UI. The job center derives its groups from the same jobs ref and does not mirror it with a watcher:
<script setup lang="ts">
import { JobsStatusTray } from '@morscherlab/mint-sdk'
import { usePluginJobs } from './generated/mint-plugin'
const jobs = usePluginJobs()
</script>
<template>
<JobsStatusTray :source="jobs" />
</template>The older jobs plus adapter tray inputs remain available for compatibility.
New integrations should pass the runtime itself so polling, SSE updates, and
job actions continue to have exactly one state owner.
When an incremental migration temporarily has more than one owner, use
combinePluginJobSources([sdkJobs, legacyJobs]). It preserves each source's
live jobs getter, fans out refresh/clear operations, routes per-job actions to
the owning source, and rejects duplicate IDs instead of guessing.
Job state exposes definition_id, result_status, and result_expires_at.
Managed file results carry a short-lived, result-scoped download_url instead
of embedded base64 data or the user's bearer token, so regular browser
downloads work through an integrated plugin proxy.
useApi(options?)
Generic API client with auth interceptor.
const api = useApi()
// GET request
const data = await api.get<User[]>('/users')
// POST with body
const created = await api.post<User>('/users', { name: 'John' })
// File upload
const result = await api.upload<UploadResponse>('/upload', file)
// Build URLs for external use
const downloadUrl = api.buildUrl('/files/123/download')Authenticated requests that receive HTTP 401 clear the rejected SDK token and
throw the public AuthenticationRequiredError. Requests are never replayed;
callers can present a single sign-in prompt without risking duplicate writes.
Clients created with withAuth: false retain their existing auth state and
receive the original HTTP error.
useAuth()
Authentication methods.
const { login, logout, initializeAuth, getCurrentUser } = useAuth()
// Initialize on app mount
await initializeAuth()
// Login
const success = await login(username, password)
// Get current user
const user = await getCurrentUser()usePasskey()
WebAuthn/FIDO2 passkey support.
const { isSupported, registerPasskey, loginWithPasskey } = usePasskey()
if (isSupported()) {
await registerPasskey('My Device')
// or
await loginWithPasskey()
}useTheme()
Theme switching.
const { isDark, toggleTheme, setTheme } = useTheme()
toggleTheme()
setTheme(true) // dark modeuseToast()
Toast notification management.
const { success, error, warning, info } = useToast()
success('Operation completed!')
error('Something went wrong')
warning('Please review your input')
info('New update available')usePlatformContext()
Detect and interact with MINT Platform when running as a plugin.
const { isIntegrated, plugin, user, navigate, notify } = usePlatformContext()
if (isIntegrated.value) {
// Running under MINT Platform
console.log('Plugin:', plugin.value?.name)
notify('Hello from plugin!', 'info')
}useExperimentSamples(options?)
Load experiment design_data and derive SampleSelector-compatible sample values.
const { samples, sampleOptions, fetch } = useExperimentSamples({ experimentId: 42 })
await fetch()Stores
useAuthStore
Pinia store for authentication state.
const authStore = useAuthStore()
// State
authStore.token
authStore.userInfo
authStore.isAuthenticated
authStore.isAdmin
// Actions
authStore.initialize()
authStore.setToken(token, expiresIn)
authStore.logout()useSettingsStore
Pinia store for app settings.
const settingsStore = useSettingsStore()
// State
settingsStore.theme // 'light' | 'dark' | 'system'
settingsStore.colorPalette
// Methods
settingsStore.getApiBaseUrl()
settingsStore.getWsBaseUrl()
settingsStore.isDark()TypeScript Types
All types are exported from @morscherlab/mint-sdk:
import type {
// Components
ButtonVariant,
ButtonSize,
ModalSize,
AlertType,
Toast,
TabItem,
SelectOption,
// Sample Management
SampleGroup, // { name, color, samples[] }
GroupItem, // { name, color, count }
FileUploaderMode, // 'file' | 'folder'
// Auth
AuthConfig,
UserInfo,
LoginResponse,
// Platform
PluginInfo,
PlatformContext,
ThemeMode,
} from '@morscherlab/mint-sdk'CSS Utility Classes
The SDK provides ready-to-use CSS classes:
Buttons
.btn-primary- Blue primary button.btn-secondary- Outlined/bordered button.btn-cta- Orange call-to-action button.btn-success- Green success button.btn-danger- Red danger button
Inputs
.input-modern- Styled text input.select-modern- Styled dropdown select.label-modern- Uppercase label
Cards & Containers
.card- Card with border and shadow.card-hover- Card with hover effect.floating-card- Floating container pattern
Alerts
.alert-success- Green success alert.alert-error- Red error alert.alert-warning- Yellow warning alert.alert-info- Blue info alert
Form Controls
.toggle-switch- Toggle switch.checkbox-modern- Styled checkbox
Theming
The SDK uses CSS variables for theming. Override them to customize:
:root {
--bg-primary: #F8FAFC;
--bg-secondary: #FFFFFF;
--bg-tertiary: #F1F5F9;
--text-primary: #1E293B;
--text-secondary: #64748B;
--border-color: #E5E7EB;
--color-primary: #3B82F6;
}
html.dark {
--bg-primary: #0F172A;
--bg-secondary: #1E293B;
--bg-tertiary: #334155;
--text-primary: #F8FAFC;
--text-secondary: #94A3B8;
--border-color: #334155;
}License
MIT
