@morscherlab/mint-sdk

MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.
31,056
Install
npm install @morscherlab/mint-sdk
Latest Version:1.3.8
Vue:3
License:MIT
Last Updated:Oct 21, 2026
Links: npm
Maintainer: x.pan

@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 MINT SDK docs for detailed API reference and guides.

Installation

bun add @morscherlab/mint-sdk

Requirements

  • 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.

Required. dist/styles.css is the SDK's single stylesheet: components no longer @import their own CSS from their <style> block, so a plugin that skips this import renders unstyled components. src/styles/index.css is the sole aggregator — every component stylesheet is imported there exactly once.

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 } from '@morscherlab/mint-sdk'
import type { ButtonVariant } from '@morscherlab/mint-sdk'

const { success } = useToast()

function handleSubmit() {
  success('Saved!')
}
</script>

<template>
  <FormField label="Username" required>
    <BaseInput v-model="username" placeholder="Enter username" />
  </FormField>

  <BaseButton variant="primary" @click="handleSubmit">
    Save
  </BaseButton>
</template>

Native Plotly charts

PlotlyChart accepts native Plotly data, layout, and config props and loads Plotly lazily. It shares MINT's chart header, loading/empty states, and toolbar/legend slots. Explicit layout colors override the MINT defaults.

<PlotlyChart
  title="QC overview"
  :data="[{ type: 'scatter', x: [1, 2, 3], y: [3, 4, 2] }]"
  :layout="{ xaxis: { title: { text: 'Sample' } } }"
  :config="{ toImageButtonOptions: { filename: 'qc-overview', format: 'png' } }"
/>

Replace data/layout/config references to redraw; nested in-place mutations do not schedule a render. The component isolates Plotly's mutations of data and layout, observes container size and the root theme, and cleans up on unmount. Use loading, empty, and emptyMessage for explicit states, and the error slot for a custom failure presentation. Generated Plotly results reuse this renderer and retain their static-image fallback.

4. Use Generated Plugin Clients

For plugin frontends scaffolded by mint init, run this from the plugin root:

mint sdk generate

Use 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 requests (including uploads and downloads) throw MintApiError from @morscherlab/mint-sdk for HTTP failures. It exposes code, message, status, requestId, details, and the original response body; the status comes from HTTP, and the request ID falls back to X-Request-ID. Legacy FastAPI detail and SDK plugin error responses are supported. Direct useApi() calls retain Axios errors unless configured with { typedErrors: true }; cancellation and authentication retry behavior are unchanged.

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.settingsConfig

Pass 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. Biology Data Templates (deprecated)

Deprecated in MINT 1.3 and removed in 1.4, with no SDK replacement: the biology data templates (@morscherlab/mint-sdk/templates, mint_sdk.templates, mint add data-template*), the BioTemplate* views, the useBioTemplate* composables, useTemplateCollection(), useExperimentSave(), and the template-only components ReagentList, ReagentEditor, ExperimentTimeline, SampleLegend and FitPanel.

6. Build Control Workspaces

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>

Components

Base Components

  • BaseButton - Button with variants (primary, secondary, cta, danger, success, ghost)
  • BaseInput - Text input with validation states
  • BaseSelect - Dropdown select with generic typing
  • BaseTabs - Tab navigation with underline and pills variants
  • BaseModal - Modal dialog with Teleport

Form Components

  • FormField - Form field wrapper with label, error, and hint
  • FormBuilder - Schema-driven forms from full schema objects or compact controls

Contributor rule: SDK components compose SDK fields (FormField + BaseInput, BaseSelect, BaseTextarea, UnitInput, SearchableSelect, BaseButton) and never restyle them. A component stylesheet may size and place a field; it does not define its own input, select, textarea, unit or button looks.

Feedback Components

  • AlertBox - Alert messages (success, error, warning, info)
  • AppToastContainer - Toast notification container for the global useToast() queue

Action Components

  • IconButton - Icon-only button with accessibility
  • ThemeToggle - Light/dark mode toggle

Layout Components

  • CollapsibleCard - Expandable card sections with optional icon badge and toggle switch
  • AppTopBar - Application header with slots
  • AppSidebar - Context sidebar that accepts panels/forms or a compact controls schema

File & Data Components

  • FileUploader - Drag-and-drop file upload with file/folder mode support

Sample Management Components

Deprecated in MINT 1.3 and removed in 1.4, with no SDK replacement: sample grouping as a whole (SampleSelector, SmartGroupModal, SmartGroupFieldRecipe, SmartGroupManual, AutoGroupModal, useAutoGroup, GroupAssigner, useGroupAssignment).

Removed Compatibility APIs

The public SDK no longer exports ToastNotification, GroupingModal, SettingsButton, FormSection, FormFieldRenderer, usePluginApi(), or usePluginConfig(). The identity-only usePluginClient() and the legacy useJobsStatusTray() job adapter API remain deprecated until MINT 1.4. Use AppToastContainer, AppTopBar settings or SettingsModal, FormBuilder, usePluginSettings(), generated useGeneratedPluginClient() helpers, and source-driven usePluginJobCenter() instead.

MINT 1.3 also removed ColorSlider (pass colorStops, thresholds, showLabels, minLabel and maxLabel to BaseSlider), DropdownButton (BaseSelect or SearchableSelect for values, ActionMenu for actions), InstrumentAlertLog, InstrumentStatusCard, LcmsSequenceTable, useAsync(), useOptimisticMutation() (use useRequestSyncState()), useResourceCrud() (use useGeneratedPluginClient()), useCommandHistory(), useWellPlateAdapter(), useWellPlateValidation() (no SDK replacement; keep them in the plugin), useWellPainting() and useWellPlateEditor() (use useRackEditor() with PlateEditor). mint doctor still detects old imports and reports the replacement path.

MINT 1.3 removed AppPluginSwitcher and its PluginSwitcherPlugin / PluginSwitcherInfo types, with the pluginSwitcher prop and plugin-switcher-* events on AppTopBar and PluginWorkspaceView: integrated plugins now show their icon, name and version as a static identity in AppTopBar. ProgressBar lost its segmented variant (variant, steps, currentStep); use StepWizard for step progress and the rail for percentages.

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>

JobsStatusTray accepts only the runtime source. Polling, SSE updates, and job actions therefore 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.

useTheme()

Theme switching.

const { isDark, toggleTheme, setTheme } = useTheme()

toggleTheme()
setTheme(true) // dark mode

useToast()

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,

  // 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

Icons

lucide-vue-next is the icon standard for this SDK. New components should import the icon component they need rather than inlining an SVG:

<script setup lang="ts">
import { RefreshCw } from 'lucide-vue-next'
</script>

<template>
  <RefreshCw :size="16" />
</template>

lucide-vue-next is a declared dependency and is external to the built bundle (like axios and @floating-ui/dom), so the consumer resolves one shared copy instead of the SDK shipping its own.

A number of older components still carry hand-written inline <svg> markup — usually annotated with the Lucide icon name they were traced from. Those are converted opportunistically, not in bulk; leave one alone unless you are already editing the component.

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

Related Packages

@gameap/plugin-sdk

SDK for developing GameAP frontend plugins

2,036
@morscherlab/mld-sdk

MLD Platform SDK - Vue 3 components, composables, and types for plugin developme...

13,654
briefing-plugin-sdk

Build plugins for Briefing, the secure video chat: plugin API, Vite preset and t...

409 1
@21cnfe/onekit-mp-cli

小程序框架开发工具

1,063
duplicate-dependencies-webpack-plugin

webpack plugin to find duplicated dependencies from your bundle

332,104 6

Version History

Version Released License
1.3.8 MIT
1.3.7 MIT
1.3.6 MIT
1.3.5 MIT
1.3.4 MIT
Pre-releases (76)
Version Released License
1.3.0-beta.11 MIT
1.3.0-beta.10 MIT
1.3.0-beta.9 MIT
1.3.0-beta.8 MIT
1.3.0-beta.7 MIT