Architecture
Design studio system & UI conventions
The DataFlow AI Platform presents a deep, feature-rich ETL product — pipeline design, monitoring, governance, migration and administration — through one consistent visual system. This page documents the design language: the semantic token model, the dark amber/zinc theme, the persona-adaptive shell, the shared component library, and the accessibility and responsive conventions every screen follows.
Design principles
The interface is built for telecom data engineers, analysts, administrators and stewards working long sessions inside dense operational screens. Four principles shape every component decision.
| Principle | What it means in practice |
|---|---|
| Semantic, not literal | Components reference token classes (bg-surface-1, text-heading) rather than raw colours, so a theme swap never touches component code. |
| Dark-first | The default theme is dark; light is a fully supported alternative. Both resolve from the same token set. |
| Persona-adaptive | The shell — chiefly the sidebar — shows only the features relevant to the active persona. |
| Code-split and resilient | Every page is lazy-loaded and wrapped in an error boundary; a failure in one screen never breaks the shell. |
Token model
Styling uses Tailwind CSS 4, wired through @tailwindcss/vite. The platform does not scatter hex values across components. Instead it defines a layer of semantic design tokens — named roles such as "surface" or "heading" — and components consume only those.
Surface tokens
Surfaces are layered. Higher-numbered surfaces sit visually closer to the user and are used for raised elements such as cards, panels and modals.
| Token class | Role |
|---|---|
bg-surface-0 | Page background — the deepest layer |
bg-surface-1 | Cards, panels, the sidebar |
bg-surface-2 | Raised elements — popovers, modals, dropdowns |
Text tokens
Text tokens express hierarchy, not colour. A heading is text-heading regardless of theme.
| Token class | Role |
|---|---|
text-heading | Page and section titles |
text-body | Primary reading text |
text-secondary | Supporting text, labels |
text-muted | De-emphasised text, hints, placeholders |
Border and accent
| Token class | Role |
|---|---|
border-primary | Default separators and component outlines |
amber-500 | The brand accent — primary actions, active navigation, focus highlights |
SURFACE LAYERING (dark theme)
┌─ bg-surface-0 (page) ────────────────────────────────┐
│ │
│ ┌─ bg-surface-1 (card / panel) ─────────────────┐ │
│ │ text-heading Pipeline summary │ │
│ │ text-body 14 runs in the last 24 hours │ │
│ │ text-muted last updated 2 minutes ago │ │
│ │ ┌──────────────┐ │ │
│ │ │ bg-surface-2 │ │ │
│ │ │ (popover) │ │ │
│ │ └──────────────┘ │ │
│ │ [ amber-500 primary action ] │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
Why semantic tokens
Because components reference token classes rather than literal colours, switching between dark and light — or adjusting the palette later — is a token-layer change. No component file is edited.
Colour palette reference
Behind the semantic tokens sits a concrete palette. The platform is a dark-first product and the canonical accent is gold/amber #ffd200. The tables below give the literal values each token resolves to.
Dark theme — backgrounds
| Role | Hex | Tailwind | Usage |
|---|---|---|---|
| Base | #09090b | zinc-950 | Primary page background — the deepest layer |
| Surface | #0c0c0f | custom | Alternating section background, cards |
| Elevated | #18181b | zinc-900 | Sidebar, panels, modals |
| Overlay | rgba(255,255,255,0.03) | white/3 | Subtle card overlays |
| Overlay hover | rgba(255,255,255,0.05) | white/5 | Card hover state |
| Overlay active | rgba(255,255,255,0.06) | white/6 | Active / selected state |
Dark theme — text
| Role | Hex | Tailwind | Usage |
|---|---|---|---|
| Primary | #ffffff | white | Headings, emphasis |
| Secondary | #f1f5f9 | slate-100 | Body text |
| Muted | #d1d5dc | gray-300 | Secondary labels |
| Subtle | #6a7282 | gray-500 | Placeholders, hints |
| Disabled | #4a5565 | gray-600 | Disabled state |
Dark theme — borders
| Role | Hex | Tailwind | Usage |
|---|---|---|---|
| Default | rgba(255,255,255,0.08) | white/8 | Card borders, dividers |
| Subtle | rgba(255,255,255,0.06) | white/6 | Subtle separators |
| Strong | rgba(255,255,255,0.15) | white/15 | Secondary button border |
Accent — primary (gold/amber)
| Role | Hex | Usage |
|---|---|---|
| Accent primary | #ffd200 | Primary brand colour, CTA |
| Accent primary hover | #e6bd00 | Hover state |
| Accent primary text | #101828 | Text on an accent background |
Accent — semantic palette
The amber accent is reserved for action and brand. Status and category meaning is carried by a fixed semantic palette.
| Colour | Hex | Tailwind | Usage |
|---|---|---|---|
| Emerald | #10b981 | emerald-500 | Success, online, healthy |
| Blue | #3b82f6 | blue-500 | Info, links, secondary actions |
| Cyan | #06b6d4 | cyan-500 | Data, metrics |
| Violet | #8b5cf6 | violet-500 | AI features, special |
| Purple | #c084fc | purple-400 | Creative, design |
| Amber | #f59e0b | amber-500 | Warnings |
| Red | #ef4444 | red-500 | Errors, destructive, critical |
| Orange | #f97316 | orange-500 | Attention, alerts |
| Pink | #ec4899 | pink-500 | Highlights |
The dark amber/zinc theme
The signature look is a dark amber/zinc theme: deep neutral zinc surfaces carrying a warm amber accent. Zinc gives long-session screens a low-glare, low-fatigue base; amber (amber-500) marks the few things the user should act on — primary buttons, the active navigation item, focus rings and key status highlights.
The theme is the default. It is the canonical expression of the design language; the light theme is the same token set re-mapped for bright environments.
| Theme | Base surfaces | Accent | Default |
|---|---|---|---|
| Dark | Zinc neutrals, deep to raised | Amber (amber-500) | Yes |
| Light | Light neutrals, same layering | Amber (amber-500) | No |
Light and dark theming
Theme state is owned by the themeStore Zustand store, persisted to localStorage under the key dataflow-theme. Its value is 'dark' | 'light', defaulting to 'dark'.
The mechanism is a single class toggle. When the theme changes, the store sets or clears the dark class on document.documentElement (<html>). Tailwind CSS 4 resolves every semantic token against that class, so one class flip re-themes the whole application.
themeStore.theme ──────────────► <html class="dark">
│ │
persisted to Tailwind 4 resolves
localStorage every semantic token
'dataflow-theme' against the class
│ │
restored on load components re-render
with no code change
App.tsx reads theme from useThemeStore on mount and synchronises the <html> class immediately, so the correct theme is applied before the first paint and there is no flash of the wrong theme.
Theme mapping
The same semantic role resolves to a different concrete value per theme. The accent and CTA stay constant — only neutrals re-map.
| Element | Dark mode | Light mode |
|---|---|---|
| Page background | #09090b (zinc-950) | #f8fafc (slate-50) |
| Surface | #0c0c0f | #ffffff |
| Sidebar | #18181b (zinc-900) | #ffffff |
| Text primary | #ffffff | #101828 |
| Text secondary | #d1d5dc (gray-300) | #4a5565 (gray-600) |
| Text muted | #6a7282 (gray-500) | #6a7282 (gray-500) |
| Border | white/8 | gray-200 (#e5e7eb) |
| Accent | #ffd200 | #ffd200 |
| CTA button | Gold gradient | Gold gradient |
| CTA text | #101828 | #101828 |
The persona-adaptive shell
The application runs inside one persistent shell — components/shell/AppShell.tsx — and that shell adapts to the active persona. There are four personas: engineer, analyst, admin and steward.
How personas are resolved
In production the persona is derived from Keycloak roles via the ROLE_PERSONA_MAP in auth/keycloak.ts. In dev mode it comes from the personaStore (activePersona, default engineer, persisted under dataflow-persona).
How the shell adapts
The sidebar navigation model lives in data/navigation.ts as navigationItems. Each item declares id, label, an icon (a lucide icon name), a path, a personas[] visibility list and optional children[]. The sidebar filters items by activePersona, and additionally honours a MIGRATION_CENTER_ENABLED feature flag from config/env.ts.
ENGINEER persona ANALYST persona ADMIN persona
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ▸ Home │ │ ▸ Home │ │ ▸ Home │
│ ▸ Design │ │ ▸ Data │ │ ▸ Monitor │
│ Studio │ │ Browser │ │ ▸ Governance │
│ ▸ Monitor │ │ ▸ Monitor │ │ ▸ Migration │
│ ▸ My │ │ ▸ Marketplace│ │ ▸ Admin (6) │
│ Pipelines │ │ ▸ Templates │ │ ─ Users │
│ ▸ Connections│ │ │ │ ─ Security │
└──────────────┘ └──────────────┘ └──────────────┘
navigationItems filtered by activePersona + MIGRATION_CENTER_ENABLED flag
Beyond filtering, ProtectedRoute enforces persona access per route: it computes an effectivePersona and calls canAccess(persona, pathname) from data/permissions. If a persona reaches a path it should not see, <AccessDenied /> is rendered instead of the page. The shared PermissionGate UI primitive lets individual controls — buttons, menu actions — be shown or hidden by permission within an otherwise visible page.
Component library conventions
Components are organised by feature area under components/, with one shared, theme-agnostic UI kit under components/ui/.
The UI kit
The ui/ folder holds roughly forty presentation primitives. They are the only place colour and spacing decisions are made; feature components compose these.
| Category | Primitives |
|---|---|
| Data display | DataTable, DataGrid, Timeline, Accordion, TreeView, CodeBlock, Badge, Avatar |
| Forms | Input, Select, Checkbox, Radio, Switch, Textarea, FileInput |
| Navigation | Tabs, Pagination, Breadcrumbs |
| Feedback | Skeleton, EmptyState, AlertBanner, ProgressBar, Toast / ToastProvider, Popover, Tooltip |
| Charts | LineChart, BarChart, GaugeChart |
| Layout | Stack, Show, ResponsiveGrid, Divider, List |
| Access | SkipNav, PermissionGate |
Feature folders — design-studio/, monitor/, governance/, admin/, migration/, ai-copilot/, data-browser/, pipelines/, connections/ — build screen-specific components on top of the kit.
Tables
High-volume tables — pipeline runs, catalog rows, audit logs — are built on @tanstack/react-table 8 and virtualised with @tanstack/react-virtual 3, so a table of thousands of rows renders only the visible window.
Graph and code surfaces
Two specialised surfaces appear across the product:
- Flow graphs use
@xyflow/react12 (React Flow) — the Design Studio pipeline canvas and the governance lineage graph. - Code editing uses
@monaco-editor/react4 for YAML pipeline definitions and SQL.
Typography
The platform uses a single sans-serif family for UI text and a monospace family for code, SQL and YAML.
/* UI text */
font-family: Inter, system-ui, -apple-system, sans-serif;
/* Code, SQL, pipeline YAML */
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
Type scale
| Level | Size | Weight | Line height | Letter spacing | Usage |
|---|---|---|---|---|---|
| Display | 72px | 700 | 1.1 | -1.8px | Hero headings |
| H1 | 48px | 700 | 1.0 | -0.5px | Page headings |
| H2 | 36px | 700 | 1.1 | normal | Section headings |
| H3 | 24px | 600 | 1.3 | normal | Card headings |
| H4 | 20px | 600 | 1.4 | normal | Sub-section headings |
| Body Large | 18px | 400 | 1.6 | normal | Lead paragraphs |
| Body | 16px | 400 | 1.5 | normal | Default body text |
| Body Small | 14px | 400 | 1.5 | normal | Descriptions |
| Caption | 12px | 500 | 1.4 | normal | Labels, metadata |
| Micro | 11px | 500 | 1.3 | 0.5px | Badges, tags |
| Tiny | 10px | 600 | 1.2 | 0.5px | Overline labels |
Font weights
| Weight | Value | Usage |
|---|---|---|
| Regular | 400 | Body text |
| Medium | 500 | Labels, captions |
| Semibold | 600 | Buttons, sub-headings |
| Bold | 700 | Headings |
| Extrabold | 800 | Display metrics |
Spacing scale
Spacing follows Tailwind's 4px base unit. Components draw from a fixed set of steps rather than arbitrary values.
| Token | Value | Usage |
|---|---|---|
space-1 | 4px | Tight gaps |
space-2 | 8px | Default gap |
space-3 | 12px | Card padding (compact) |
space-4 | 16px | Section gap |
space-6 | 24px | Section padding, standard card padding |
space-8 | 32px | Large section spacing |
space-12 | 48px | Page sections |
space-16 | 64px | Major sections |
Border radius
| Token | Value | Usage |
|---|---|---|
rounded-sm | 6px | Tags, small badges |
rounded | 8px | Buttons, inputs, small cards |
rounded-md | 12px | Cards, panels |
rounded-lg | 16px | Large cards, modals |
rounded-full | 9999px | Pills, avatars, badges |
Shadows
| Token | Value | Usage |
|---|---|---|
shadow-sm | 0 1px 2px rgba(0,0,0,0.05) | Subtle lift |
shadow-md | 0 4px 6px -1px rgba(0,0,0,0.1) | Cards |
shadow-lg | 0 10px 15px -3px rgba(0,0,0,0.1) | Elevated cards, buttons |
shadow-xl | 0 20px 25px -5px rgba(0,0,0,0.1) | Modals, dropdowns |
shadow-2xl | 0 25px 50px -12px rgba(0,0,0,0.25) | Hero elements |
shadow-glow | 0 0 20px rgba(255,210,0,0.15) | Gold accent glow |
Buttons and interaction states
Buttons come in three weights — a gold primary CTA, a ghost secondary, and a red destructive — at a standard and a large size.
/* Primary CTA — standard */
background: linear-gradient(to right, #ffd200, #d4a00a);
color: #101828;
font-weight: 600;
font-size: 14px;
padding: 8px 16px;
border-radius: 8px;
/* Primary CTA — large */
font-size: 16px;
padding: 16px 32px;
border-radius: 12px;
/* Secondary (ghost) */
background: transparent;
color: #d1d5dc;
border: 0.8px solid rgba(255,255,255,0.15);
/* Destructive */
background: #ef4444;
color: #ffffff;
Interaction states
Every interactive primitive resolves the same four states, so a button, a row and a nav item all respond consistently.
| State | Effect |
|---|---|
| Hover | Lighten 10%, scale 1.02 |
| Active | Darken 5%, scale 0.98 |
| Disabled | Opacity 0.5, cursor not-allowed |
| Focus | 2px ring, amber-500/50 — the amber accent doubles as the focus highlight |
Gradients
/* Gold text gradient — headlines */
background: linear-gradient(to right, #ffd200, #e6b800, #cc9e00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* Gold button gradient */
background: linear-gradient(to right, #ffd200 0%, #d4a00a 50%, #b8860b 100%);
/* Subtle gold glow — background accent */
background: radial-gradient(rgba(255,210,0,0.08) 0%, transparent 60%);
Component patterns
The UI kit primitives are built from the tokens above. The recurring patterns below define how a card, a nav item, a badge, an input, a table and a tab bar look in the dark theme.
| Pattern | Specification |
|---|---|
| Card | bg white/3, border 1px white/8, radius 12px, padding 24px; hover → bg white/5, border white/12 |
| Sidebar nav item | Default text-gray-400; hover text-white on bg white/5; active text-amber-400 on bg amber-500/10 with border-l-2 border-amber-500 |
| Badge / tag | bg {color}-500/10, text {color}-400, border 1px {color}-500/20, radius 6px, padding 2px 8px, 11px medium |
| Input field | bg white/5, border 1px white/10, radius 8px, white text, gray-500 placeholder; focus → border amber-500/50, 2px amber-500/20 ring |
| Table | Header bg white/3, header text gray-400 uppercase 11px tracking-wider; row border white/5, row hover white/3; cell text gray-300 (data) / white (primary) |
| Tab bar | Default text-gray-400 with border-b-2 border-transparent; hover text-gray-200 / border-gray-600; active text-amber-400 / border-amber-500 |
Iconography
- Library — Lucide React. The brand logo icon is the Lucide
Workflowglyph. - Stroke width — 1.5px by default.
- Sizes — Small 16px (
h-4 w-4, inline with text); Default 20px (h-5 w-5, navigation and buttons); Large 24px (h-6 w-6, feature icons). - Colour — icons inherit the surrounding text colour, so they re-theme with the rest of the UI.
Animation and transitions
Motion is restrained and consistent. Each interaction class has a fixed duration and easing.
| Property | Duration | Easing |
|---|---|---|
| Colour transitions | 150ms | ease |
| Background | 200ms | ease-in-out |
| Transform (hover) | 200ms | ease-out |
| Modal open | 300ms | ease-out |
| Sidebar toggle | 300ms | ease-in-out |
| Spinner | 1000ms | linear (infinite) |
Responsive breakpoints
The layout resolves against Tailwind's five standard breakpoints.
| Breakpoint | Width | Usage |
|---|---|---|
sm | 640px | Mobile landscape |
md | 768px | Tablet |
lg | 1024px | Desktop |
xl | 1280px | Wide desktop |
2xl | 1536px | Ultra-wide |
Layout primitives
Pages are assembled from a small set of layout primitives so that spacing, page headers and split views look identical everywhere.
| Primitive | Folder | Role |
|---|---|---|
PageContainer | layout/ | Standard page wrapper with consistent padding and max width |
PageHeader | layout/ | Title, description and action area at the top of a page |
SplitPane | layout/ | Resizable two-pane layouts (e.g. list plus detail) |
The shell itself contributes the persistent frame: Sidebar, TopBar, the <main id="main-content"> outlet, and the AI Copilot trigger and sidebar. Main content is offset by an effectiveMargin that tracks the current sidebar width (collapsed 60px, expanded 240px).
Accessibility
Accessibility is built into the shell and the UI kit rather than bolted on per page.
- Skip navigation. The shell renders a skip link to
#main-content; theSkipNavprimitive standardises it.<main>carries theid="main-content"target. - Keyboard command surface. Global search opens with
Ctrl/Cmd+Kvia a shell keydown handler; the command palette is built oncmdk. TheuseKeyboardShortcuthook standardises other shortcuts. - Focus visibility. The amber accent doubles as the focus highlight, so keyboard focus is always clearly visible against zinc surfaces.
- Permission-aware rendering.
PermissionGateandProtectedRouteensure users are not shown — and cannot tab into — controls or routes they cannot use. - Loading and empty states.
SkeletonandEmptyStategive every async surface a defined, screen-reader-friendly state instead of a blank region.
Resilient by default
Every page is wrapped in an ErrorBoundary (inside SuspenseWrap) and panels use PanelErrorFallback. A user always sees a recoverable error surface, never a blank screen, when something fails.
Responsive behaviour
The layout adapts across three breakpoint classes, computed by useIsMobile, useIsTablet and useSidebarState from hooks/useResponsive (with supporting useMediaQuery and useBreakpoint hooks).
| Breakpoint | Navigation | Notes |
|---|---|---|
| Desktop | Full sidebar, collapsible 60px ⇄ 240px | Main content offset by effectiveMargin |
| Tablet | Collapsed sidebar with hover-expand | Compact chrome, same routes |
| Mobile | MobileMenu overlay + MobileBottomNav | Sidebar hidden; bottom nav for primary destinations |
DESKTOP TABLET MOBILE
┌────┬────────────┐ ┌──┬──────────────┐ ┌──────────────┐
│side│ content │ │░░│ content │ │ TopBar │
│bar │ │ │ho│ │ ├──────────────┤
│240 │ │ │ve│ │ │ │
│px │ │ │r │ │ │ content │
│ │ │ │60│ │ │ │
│ │ │ │px│ │ ├──────────────┤
└────┴────────────┘ └──┴──────────────┘ │ ▣ ▣ ▣ ▣ nav │
└──────────────┘
The ResponsiveGrid, Stack and Show layout primitives let feature components reflow without bespoke media-query CSS, keeping responsive behaviour consistent with the design system rather than improvised per page.
Summary
The DataFlow AI design system rests on one idea applied consistently: components describe roles, not pixels. Semantic Tailwind 4 tokens carry surface, text, border and accent meaning; the dark amber/zinc theme is the default expression of those tokens and light is the same set re-mapped; a single dark class toggle re-themes everything; the shell adapts to the active persona; a forty-piece UI kit and a handful of layout primitives keep every screen coherent; and accessibility plus responsive behaviour are wired into the shell so every page inherits them for free.