Shared Components
Both the Backend Dashboard and Heimdall ID share a common set of UI components built with React and TailwindCSS.
UI Components
Button
A versatile button component with multiple variants and sizes.
import { Button } from "@/components/ui/Button";
// Variants
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
// States
<Button disabled>Disabled</Button>
<Button loading>Loading...</Button>
// With icons
<Button>
<IconPlus className="mr-2" />
Add Item
</Button>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'primary' | Button style |
size | 'sm' | 'md' | 'lg' | 'md' | Button size |
disabled | boolean | false | Disabled state |
loading | boolean | false | Loading state |
type | 'button' | 'submit' | 'reset' | 'button' | HTML button type |
onClick | () => void | - | Click handler |
Alert
Display alert messages with different severity levels.
import { Alert } from "@/components/ui/Alert";
<Alert type="success" title="Success!">
Your changes have been saved successfully.
</Alert>
<Alert type="error" title="Error">
Something went wrong. Please try again.
</Alert>
<Alert type="warning" title="Warning">
This action cannot be undone.
</Alert>
<Alert type="info" title="Info">
Your session will expire in 5 minutes.
</Alert>
// Dismissible
<Alert type="info" dismissible onDismiss={() => setShow(false)}>
Click the X to dismiss this alert.
</Alert>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'success' | 'error' | 'warning' | 'info' | 'info' | Alert type |
title | string | - | Optional title |
children | ReactNode | - | Alert content |
dismissible | boolean | false | Show dismiss button |
onDismiss | () => void | - | Dismiss callback |
Modal
A modal dialog component for confirmations and forms.
import { Modal } from "@/components/ui/Modal";
function ConfirmDialog() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Confirm Action"
size="md"
>
<p>Are you sure you want to proceed with this action?</p>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleConfirm}>
Confirm
</Button>
</div>
</Modal>
</>
);
}
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | - | Whether modal is open |
onClose | () => void | - | Close callback |
title | string | - | Modal title |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Modal size |
children | ReactNode | - | Modal content |
closeOnOverlayClick | boolean | true | Close on backdrop click |
closeOnEscape | boolean | true | Close on Escape key |
FloatingInput
An input field with a floating label animation.
import { FloatingInput } from "@/components/ui/FloatingInput";
<FloatingInput
label="Email Address"
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={errors.email}
required
/>
<FloatingInput
label="Password"
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={errors.password}
required
/>
// With helper text
<FloatingInput
label="Username"
helperText="This will be your public display name"
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Input label |
type | string | 'text' | Input type |
name | string | - | Input name |
value | string | - | Input value |
onChange | (e) => void | - | Change handler |
error | string | - | Error message |
helperText | string | - | Helper text |
required | boolean | false | Required field |
disabled | boolean | false | Disabled state |
LoadingSpinner
A loading spinner component.
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
// Sizes
<LoadingSpinner size="sm" />
<LoadingSpinner size="md" />
<LoadingSpinner size="lg" />
// With text
<div className="flex items-center gap-2">
<LoadingSpinner size="sm" />
<span>Loading...</span>
</div>
// Full page
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" />
</div>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Spinner size |
className | string | - | Additional classes |
ElctoLogo
The Elcto brand logo component.
import { ElctoLogo } from "@/components/ui/ElctoLogo";
// Variants
<ElctoLogo variant="full" /> // Full logo with text
<ElctoLogo variant="icon" /> // Icon only
<ElctoLogo variant="wordmark" /> // Text only
// Sizes
<ElctoLogo size="sm" />
<ElctoLogo size="md" />
<ElctoLogo size="lg" />
// Dark/Light mode
<ElctoLogo theme="dark" />
<ElctoLogo theme="light" />
Auth Components
ProtectedRoute
Wrap pages that require authentication.
import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
export default function DashboardPage() {
return (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
);
}
// With required permissions
<ProtectedRoute requiredPermissions={["gps:write", "users:read"]}>
<AdminPanel />
</ProtectedRoute>
// With custom fallback
<ProtectedRoute fallback={<CustomLoginPrompt />}>
<Content />
</ProtectedRoute>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Protected content |
requiredPermissions | string[] | - | Required permissions |
fallback | ReactNode | <LoginRedirect /> | Fallback component |
ProtectedContent
Conditionally render content based on auth state.
import { ProtectedContent } from "@/components/auth/ProtectedContent";
<ProtectedContent>
{/* Only shown when authenticated */}
<UserMenu />
</ProtectedContent>
<ProtectedContent fallback={<LoginButton />}>
<LogoutButton />
</ProtectedContent>
<ProtectedContent requiredPermissions={["admin"]}>
<AdminTools />
</ProtectedContent>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Protected content |
fallback | ReactNode | null | Fallback when not authenticated |
requiredPermissions | string[] | - | Required permissions |
UserButton
Display user info with dropdown menu.
import { UserButton } from "@/components/auth/UserButton";
// Basic usage
<UserButton />
// With custom menu items
<UserButton
menuItems={[
{ label: "Settings", href: "/settings" },
{ label: "Help", href: "/help" },
]}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
menuItems | MenuItem[] | - | Additional menu items |
showEmail | boolean | true | Show email in dropdown |
Layout Components
DashboardLayout
The main layout for authenticated dashboard pages.
import { DashboardLayout } from "@/components/layouts/DashboardLayout";
export default function Page() {
return (
<DashboardLayout
title="GPS Data"
breadcrumbs={[
{ label: "Dashboard", href: "/dashboard" },
{ label: "GPS Data" }
]}
>
<GpsDataContent />
</DashboardLayout>
);
}
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Page content |
title | string | - | Page title |
breadcrumbs | Breadcrumb[] | - | Breadcrumb navigation |
OverlayLayout
Full-screen overlay layout for modals and overlays.
import { OverlayLayout } from "@/layouts/OverlayLayout";
export default function ModalPage() {
return (
<OverlayLayout onClose={() => router.back()}>
<div className="bg-white rounded-lg p-6 max-w-md w-full">
<h2>Modal Content</h2>
{/* ... */}
</div>
</OverlayLayout>
);
}
Footer
The site footer component.
import { Footer } from "@/components/layout/Footer";
export default function Layout({ children }) {
return (
<>
<main>{children}</main>
<Footer />
</>
);
}
Providers
SessionProvider
Wrap your app with the session provider for auth state.
// src/app/layout.tsx
import { SessionProvider } from "@/components/providers/SessionProvider";
export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
);
}
ApolloWrapper
GraphQL client provider (Backend Dashboard only).
// src/app/layout.tsx
import { ApolloWrapper } from "@/components/providers/ApolloWrapper";
export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>
<ApolloWrapper>
{children}
</ApolloWrapper>
</SessionProvider>
</body>
</html>
);
}
Styling
All components use TailwindCSS for styling. Common patterns:
Color Tokens
/* Primary colors */
bg-primary-500
text-primary-600
border-primary-400
/* Semantic colors */
bg-success-100 text-success-700
bg-error-100 text-error-700
bg-warning-100 text-warning-700
bg-info-100 text-info-700
Spacing
/* Padding */
p-4 /* 1rem */
px-6 py-3 /* horizontal 1.5rem, vertical 0.75rem */
/* Margin */
m-4
mt-2 mb-4
mx-auto /* center horizontally */
Responsive Design
/* Mobile first */
w-full md:w-1/2 lg:w-1/3
/* Hide/show */
hidden md:block
block md:hidden
Best Practices
Component Composition
// Good: Compose smaller components
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>
Content here
</CardContent>
</Card>
// Avoid: Monolithic components with many props
<Card
title="Title"
headerActions={...}
content={...}
footerActions={...}
/>
Error Boundaries
import { ErrorBoundary } from "@/components/ErrorBoundary";
<ErrorBoundary fallback={<ErrorMessage />}>
<RiskyComponent />
</ErrorBoundary>
Loading States
function DataTable() {
const { data, isLoading, error } = useQuery(...);
if (isLoading) return <LoadingSpinner />;
if (error) return <Alert type="error">{error.message}</Alert>;
return <Table data={data} />;
}
Next Steps
- Backend Dashboard - Dashboard documentation
- Heimdall ID - Identity service documentation
- Authentication Flow - Auth flow details