# @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](https://github.com/MorscherLab/MINT-docs/blob/main/sdk/index.md) for detailed API reference and guides.
> - [API Reference](https://github.com/MorscherLab/MINT-docs/blob/main/sdk/api/frontend.md)
> - [Component Catalog](https://github.com/MorscherLab/MINT-docs/blob/main/sdk/components/index.md) - All public components with props/events
> - [Composables Reference](https://github.com/MorscherLab/MINT-docs/blob/main/sdk/frontend/composables.md) - All public composables with examples
> - [Theming Guide](https://github.com/MorscherLab/MINT-docs/blob/main/sdk/frontend/theming.md) - CSS variables and customization

## Installation

```bash
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:

```typescript
// 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:

```typescript
import mintPreset from '@morscherlab/mint-sdk/tailwind.preset'

export default {
  presets: [mintPreset],
  content: [
    './src/**/*.vue',
    './node_modules/@morscherlab/mint-sdk/**/*.vue',
  ],
}
```

### 3. Use Components

```vue
<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.

```vue
<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:

```bash
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:

```typescript
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)`:

```typescript
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:

```typescript
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:

```typescript
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:

```vue
<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:

```vue
<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()`:

```typescript
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:

```vue
<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.

```typescript
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.

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

toggleTheme()
setTheme(true) // dark mode
```

### `useToast()`
Toast notification management.

```typescript
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.

```typescript
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.

```typescript
const { samples, sampleOptions, fetch } = useExperimentSamples({ experimentId: 42 })
await fetch()
```

## Stores

### `useAuthStore`
Pinia store for authentication state.

```typescript
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.

```typescript
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`:

```typescript
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](https://lucide.dev) is the icon standard for this SDK.** New
components should import the icon component they need rather than inlining an
SVG:

```vue
<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:

```css
: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
