Developer Documentation
The Veltrix portal and community apps render from one shared component kit, shipped as the @veltrixsecops/app-sdk/ui subpath of the SDK. App authors get the same buttons, tables, and form controls the portal uses, themed automatically to each tenant's brand and light/dark preference — because theming is driven by design tokens the host owns.
The kit is live in @veltrixsecops/app-sdk (v3.5.x) at the /ui subpath — 24 components and 2 hooks you can import today. At packaging time the CLI compiles your client/index.tsx into a hermetic ESM bundle where react, react-dom, and every @veltrixsecops/app-sdk import (including /ui) are resolved from the host at runtime via globalThis.__VELTRIX_APP_RUNTIME__.
The declarative App UI & Navigation contract — an app's client.pages[], the platform-owned chrome (breadcrumb, app header, navigation, permission gating, loading + error states), page layouts, and the token-driven theme — is documented in App UI & Navigation.
A component kit that apps import has to be publicly installable, so it ships as the @veltrixsecops/app-sdk/ui subpath of the public SDK. But rather than bundle a second copy of the components, the kit copies the platform's prop types and delegates at render time to the host portal's own components (via globalThis.__VELTRIX_APP_RUNTIME__.ui). One implementation, no drift: app authors see exactly what the portal renders, and outside the platform (local dev, tests) each component falls back to a minimal, unstyled, accessible version so nothing crashes.
| Import | Contains | React? |
|---|---|---|
@veltrixsecops/app-sdk |
Handler contracts, manifest types, ctx.platform, layout helpers |
No (server-safe) |
@veltrixsecops/app-sdk/ui |
The shared component kit (24 components + useToast/useConfirmDialog), delegating to the host at runtime |
Yes |
@veltrixsecops/app-sdk/hooks |
useAppContext, usePipelineStatus, useAppBranding, usePermissions |
Yes |
@veltrixsecops/cli |
init / validate / package / login / sandbox / dev / deploy / config / drift | No |
Predictable shell, flexible body. The platform renders the chrome for every app page — breadcrumb, app header, navigation, permission gating, loading skeletons, and error boundary — identically across every vendor's app. Your component renders only the page body and composes it from this library. Apps get full freedom inside the frame but cannot alter the chrome or the brand palette, so the portal never becomes a patchwork of vendor themes. (The declarative side of this — client.pages[] — is documented in App UI & Navigation.)
Tokens are CSS custom properties owned by the host. The portal sets the semantic scales — --vx-color-primary, surface / border / content, and their dark variants — on :root; components only ever read var(--vx-*). The consequence is powerful: an app's UI automatically follows the tenant's light/dark theme (and future tenant branding) with zero work from the app author. Apps may define their own app-scoped --app-<id>-* variables for their body content, but cannot ship global CSS or override the host --vx-* palette. React hooks like useAppContext and usePipelineStatus live at the sibling @veltrixsecops/app-sdk/hooks subpath — kept off the SDK root entry so pipeline handlers stay React-free.
The kit ships inside the SDK — install it as a dev dependency (no separate UI package, Tailwind preset, or tokens stylesheet to wire up; theming comes from the host at runtime):
npm install --save-dev @veltrixsecops/app-sdk
Then compose your page body from the /ui subpath:
import { Card, CardHeader, CardBody, Button, Badge } from '@veltrixsecops/app-sdk/ui'
export function IndexesPage() {
return (
<Card>
<CardHeader>Indexes <Badge variant="success">Deployed</Badge></CardHeader>
<CardBody>
<Button variant="primary">New index</Button>
</CardBody>
</Card>
)
}
These render in the platform design system and pick up the tenant theme (light/dark) and your app's branding automatically via the host's CSS-token bridge — no styling needed.
There must be exactly one copy of React on the page. The CLI's client bundler handles this for you: it marks react, react-dom, react/jsx-runtime, and every @veltrixsecops/app-sdk import (root and subpaths, including /ui and /hooks) as externals, shimming them to the host portal's runtime at load time — so your components render inside the host React tree and never bundle their own copy.
If an app bundles its own React, hooks throw "Invalid hook call" and context (theme, app context) silently fails to cross the boundary. The client-bundle tooling enforces the externals contract so the host's singleton is always used.
// what the CLI bundler externalizes (handled for you at `veltrix package` time)
external: [
'react', 'react-dom', 'react/jsx-runtime',
'@veltrixsecops/app-sdk', '@veltrixsecops/app-sdk/ui', '@veltrixsecops/app-sdk/hooks',
]
// → all resolved from the host portal at runtime via globalThis.__VELTRIX_APP_RUNTIME__
The shared components, all exported from @veltrixsecops/app-sdk/ui (prop types are exported alongside each). useToast and useConfirmDialog are hooks, backed by providers the platform mounts around every app page.
| Component | Purpose | Key props |
|---|---|---|
Button |
Actions with variants, sizes, loading state | variant (primary/secondary/danger/success/warning/ghost/link), size (sm/md/lg), isLoading, loadingText, leftIcon |
Badge |
Status indicators and labels | variant, size (sm/md/lg), dot |
Card |
Container with header/body/footer | variant (default/bordered/elevated); CardHeader, CardBody, CardFooter |
Input |
Form input with label, error, icons | label, error, size (sm/md/lg), variant (default/error/success), leftIcon |
EmptyState |
Consistent empty-state messaging | icon, title, description, action |
Skeleton |
Loading placeholders | variant (text/circular/rectangular), width, height; also SkeletonText, SkeletonCard |
Textarea / Checkbox |
Multi-line input and checkbox, same label/error pattern as Input |
label, error, plus native props |
Select / MultiSelect |
Accessible single/multi dropdown select | options, value, onChange, label, error |
FormField / FormDialog / Modal |
Labelled field wrapper, a form-in-a-dialog, and a general modal | isOpen, onClose, title, onSubmit |
Tabs |
Tabbed content switcher | tabs ({ key, label, content }[]) |
DataTable |
Sortable, columned data table | columns, rows, sort/empty state |
SearchBox / FilterBar / SortSelect / Pagination |
List controls — search, faceted filtering, sort, and paging | value/onChange; filters; page/total |
Tooltip / Spinner / StatsCard / Alert |
Hover hint, loading spinner, KPI stat tile, inline alert banner | content/placement; label/value; variant |
useToast / useConfirmDialog hooks |
Toasts (instead of alert()) and confirm dialogs (instead of confirm()) |
useToast() → .success()/.error(); useConfirmDialog().confirm({ title, message, variant }) |
Heavy composite widgets (a configuration canvas, version-control, and pipeline visualizer) stay out of scope for the app kit — the platform renders those itself. Every export here also works outside the platform (local dev, tests): components degrade to a minimal accessible fallback, useToast logs to the console, and useConfirmDialog().confirm() resolves to false.
Color is defined once as semantic design tokens — space-separated RGB triples so Tailwind can apply opacity modifiers. Components consume them through utility classes (bg-primary, text-content-secondary, border-border), never hardcoded hex. Dark mode flips the "soft" tokens (subtle tints, surfaces, borders, text); solid brand bases stay constant.
| Token group | Variables | Tailwind utility |
|---|---|---|
| Brand / semantic | --vx-color-primary, -hover, -active, -foreground, -subtle (also success, warning, danger, info) |
bg-primary, text-primary-foreground, bg-primary-subtle |
| Surfaces | --vx-color-surface, -raised, -overlay, -hover, -sunken |
bg-surface, bg-surface-raised |
| Borders | --vx-color-border, --vx-color-border-strong |
border-border, border-border-strong |
| Content (text) | --vx-color-content-primary, -secondary, -tertiary, -disabled, -inverse |
text-content-primary, text-content-secondary |
Dark mode is triggered by either a .dark class or [data-theme="dark"] on the root element — a consumer can use whichever convention it prefers. The host owns these --vx-* values; tenant branding is applied there, and every component follows automatically (values are space-separated RGB triples so Tailwind opacity modifiers keep working):
/* host-owned — the portal sets these; apps read them, they do not redefine them */
:root {
--vx-color-primary: 79 70 229; /* indigo-600 instead of the blue default */
--vx-color-primary-hover: 67 56 202;
}
.dark, [data-theme='dark'] {
--vx-color-surface: 17 24 39; /* dark page background */
}
Apps do not ship global CSS and cannot override the host --vx-* palette or the chrome — that is what keeps the portal coherent across every vendor's app. For their own body content, apps may declare app-scoped variables namespaced --app-<id>-*:
/* inside an app's own page body — app-scoped, never global */
.app-splunk-enterprise .chart {
--app-splunk-enterprise-accent: var(--vx-color-primary); /* stay on-theme */
border-color: var(--vx-color-border);
}
You don't ship these tokens yourself — the host portal defines the --vx-* palette on :root, and the /ui components read them at runtime. Your app only opts in to the theme (and may add its own --app-<id>-* variables for body content).
client.pages contributes navigation today.