From 13ff3215f6af0ff87a4532f715d950816c525c80 Mon Sep 17 00:00:00 2001 From: kosisochukwu1234 Date: Mon, 24 Aug 2026 18:42:31 +0100 Subject: [PATCH 1/2] feat(ui): implement shared ui primitive set with light/dark theme support --- app/compliance/page.tsx | 14 +- app/contracts/page.tsx | 14 +- app/contributors/page.tsx | 14 +- app/docs/page.tsx | 14 +- app/globals.css | 32 +++++ app/layout.tsx | 1 + app/opengraph-image.tsx | 1 + app/operators/page.tsx | 34 ++--- app/page.tsx | 20 +-- app/product/page.tsx | 14 +- app/roadmap/page.tsx | 14 +- app/twitter-image.tsx | 1 + components/brand-logo.tsx | 1 + components/charts/chart-frame.tsx | 1 + components/charts/data-table.tsx | 1 + components/charts/quota-gauge.tsx | 1 + components/charts/sparkline.tsx | 1 + components/charts/stacked-bars.tsx | 1 + components/charts/time-series.tsx | 1 + components/expected-pages.tsx | 81 ++++++++---- components/ui/badge.tsx | 23 ++++ components/ui/button.tsx | 34 +++++ components/ui/card.tsx | 17 +++ components/ui/empty-state.tsx | 21 +++ components/ui/index.ts | 7 + components/ui/skeleton.tsx | 18 +++ components/ui/stat.tsx | 17 +++ components/ui/table.tsx | 47 +++++++ components/ui/ui.module.css | 200 +++++++++++++++++++++++++++++ tests/unit/brand-logo.test.tsx | 1 + tests/unit/expected-pages.test.tsx | 1 + 31 files changed, 565 insertions(+), 82 deletions(-) create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/empty-state.tsx create mode 100644 components/ui/index.ts create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/stat.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/ui.module.css diff --git a/app/compliance/page.tsx b/app/compliance/page.tsx index 938df9f..372fde8 100644 --- a/app/compliance/page.tsx +++ b/app/compliance/page.tsx @@ -1,11 +1,15 @@ +import { Badge, EmptyState } from "@/components/ui"; + export default function Page() { return (
- Compliance -

Compliance surface β€” product definition TBD.

-

- Scaffold page β€” replace with production content, data loaders, and analytics. -

+ Compliance +

Compliance surface — product definition TBD.

+
); } + diff --git a/app/contracts/page.tsx b/app/contracts/page.tsx index 8b4cd26..f98154d 100644 --- a/app/contracts/page.tsx +++ b/app/contracts/page.tsx @@ -1,11 +1,15 @@ +import { Badge, EmptyState } from "@/components/ui"; + export default function Page() { return (
- Contracts -

Contracts surface β€” product definition TBD.

-

- Scaffold page β€” replace with production content, data loaders, and analytics. -

+ Contracts +

Contracts surface — product definition TBD.

+
); } + diff --git a/app/contributors/page.tsx b/app/contributors/page.tsx index 301b35f..76ba70e 100644 --- a/app/contributors/page.tsx +++ b/app/contributors/page.tsx @@ -1,11 +1,15 @@ +import { Badge, EmptyState } from "@/components/ui"; + export default function Page() { return (
- Contributors -

Contributors surface β€” product definition TBD.

-

- Scaffold page β€” replace with production content, data loaders, and analytics. -

+ Contributors +

Contributors surface — product definition TBD.

+
); } + diff --git a/app/docs/page.tsx b/app/docs/page.tsx index 07197ba..d9db482 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -1,11 +1,15 @@ +import { Badge, EmptyState } from "@/components/ui"; + export default function Page() { return (
- Documentation -

Technical specs, governance, and integration guides.

-

- Scaffold page β€” replace with production content, data loaders, and analytics. -

+ Docs +

Docs surface — product definition TBD.

+
); } + diff --git a/app/globals.css b/app/globals.css index 2374fe8..4bf1060 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,5 @@ :root { + /* Dark mode (default for now) */ --bg: #070b14; --surface: #0f1727; --text: #eaf2ff; @@ -8,6 +9,18 @@ --ring: color-mix(in srgb, #59c2ff 35%, transparent); } +@media (prefers-color-scheme: light) { + :root { + --bg: #f8fafc; + --surface: #ffffff; + --text: #0f172a; + --muted: #64748b; + --accent: #0284c7; + --accent-2: #10b981; + --ring: color-mix(in srgb, var(--accent) 35%, transparent); + } +} + * { box-sizing: border-box; } body { @@ -290,3 +303,22 @@ a { color: inherit; text-decoration: none; } background: color-mix(in srgb, var(--surface) 65%, transparent); letter-spacing: 0.04em; } + +.grid-cols-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; padding: 18px 0 8px; } +.grid-cols-auto-lg { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; } +.grid-gap-16 { display: grid; gap: 16px; padding: 8px 0 40px; } +.text-muted-max-62 { color: var(--muted); max-width: 62ch; margin-bottom: 24px; } +.flex-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; } + + +.mb-16 { margin-bottom: 16px; } + + +.mt-12 { margin-top: 12px; } +.mt-16 { margin-top: 16px; } + + +.mt-8 { margin-top: 8px; } +.text-muted { color: var(--muted); } +.text-sm { font-size: 0.85rem; } + diff --git a/app/layout.tsx b/app/layout.tsx index 0875230..bc8f938 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -67,3 +67,4 @@ export default function RootLayout({ children }: { children: React.ReactNode }) ); } + diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx index 59b36f8..944f0aa 100644 --- a/app/opengraph-image.tsx +++ b/app/opengraph-image.tsx @@ -87,3 +87,4 @@ export default function Image() { } ); } + diff --git a/app/operators/page.tsx b/app/operators/page.tsx index 86280f8..e12df97 100644 --- a/app/operators/page.tsx +++ b/app/operators/page.tsx @@ -37,43 +37,36 @@ const tiles = [ { label: "Settlement latency (p95)", value: "6.4s", trend: [9.1, 8.7, 8.2, 7.6, 7.4, 6.9, 6.6, 6.4], tone: "good" as const }, ]; +import { Badge, Card, Stat } from "../../components/ui"; + export default function Page() { return (
- Operators -

Operator dashboard

-

+ Operators +

Operator dashboard

+

Attestation volume, usage by model, and quota headroom. Every chart on this page ships its numbers as a table β€” an auditor asking for the underlying figures should not have to ask anyone.

-
+
{tiles.map((tile) => ( -
-

- {tile.label} -

-
- {tile.value} + + +
+ Trend
-
+ ))}
-
+
-
+
); } + diff --git a/app/page.tsx b/app/page.tsx index 206226b..0bab0b4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { BrandLogo } from "@/components/brand-logo"; import { ExpectedPages } from "@/components/expected-pages"; +import { Badge, Button, Card } from "@/components/ui"; export default function HomePage() { return ( @@ -9,15 +10,15 @@ export default function HomePage() {
- Stellar Β· Soroban Β· AI governance + Stellar Β· Soroban Β· AI governance

Prove every inference.

attested facts on-chain: which model ran, under which policy, and what it costs\u2014so procurement, finance, and auditors share one neutral layer." }} />

- Ship the roadmap - Read the contracts story + +
  • Metered settlement
  • @@ -28,21 +29,21 @@ export default function HomePage() {
-
+
β—†

Attestation rail

Signed inference events tied to tiersβ€”your billing disputes shrink.

-
-
+ +
β—‡

Settlement logic

Soroban payment-router patterns built for fast finality on Stellar.

-
-
+ +
β—‹

Operator-ready UX

Roadmap toward dashboards gateways and enterprises actually adopt.

-
+

Built for teams who sell or buy inference at scaleβ€”and cannot afford black-box invoices.

@@ -51,3 +52,4 @@ export default function HomePage() { ); } + diff --git a/app/product/page.tsx b/app/product/page.tsx index b7a0844..a24a7d5 100644 --- a/app/product/page.tsx +++ b/app/product/page.tsx @@ -1,11 +1,15 @@ +import { Badge, EmptyState } from "@/components/ui"; + export default function Page() { return (
- Product -

Product surface β€” product definition TBD.

-

- Scaffold page β€” replace with production content, data loaders, and analytics. -

+ Product +

Product surface — product definition TBD.

+
); } + diff --git a/app/roadmap/page.tsx b/app/roadmap/page.tsx index 8f68322..ce611b1 100644 --- a/app/roadmap/page.tsx +++ b/app/roadmap/page.tsx @@ -1,11 +1,15 @@ +import { Badge, EmptyState } from "@/components/ui"; + export default function Page() { return (
- Roadmap -

Milestones tied to protocol releases and grant checkpoints.

-

- Scaffold page β€” replace with production content, data loaders, and analytics. -

+ Roadmap +

Roadmap surface — product definition TBD.

+
); } + diff --git a/app/twitter-image.tsx b/app/twitter-image.tsx index 59b36f8..944f0aa 100644 --- a/app/twitter-image.tsx +++ b/app/twitter-image.tsx @@ -87,3 +87,4 @@ export default function Image() { } ); } + diff --git a/components/brand-logo.tsx b/components/brand-logo.tsx index 45b257c..8eafc67 100644 --- a/components/brand-logo.tsx +++ b/components/brand-logo.tsx @@ -27,3 +27,4 @@ export function BrandLogo({ className }: { className?: string }) { ); } + diff --git a/components/charts/chart-frame.tsx b/components/charts/chart-frame.tsx index 7c864c0..1da0237 100644 --- a/components/charts/chart-frame.tsx +++ b/components/charts/chart-frame.tsx @@ -113,3 +113,4 @@ export function ChartFrame({ } export { styles as chartStyles }; + diff --git a/components/charts/data-table.tsx b/components/charts/data-table.tsx index d56c9d7..ed2c19a 100644 --- a/components/charts/data-table.tsx +++ b/components/charts/data-table.tsx @@ -76,3 +76,4 @@ export function DataTable({ ); } + diff --git a/components/charts/quota-gauge.tsx b/components/charts/quota-gauge.tsx index 7c45faa..b28fc21 100644 --- a/components/charts/quota-gauge.tsx +++ b/components/charts/quota-gauge.tsx @@ -131,3 +131,4 @@ export function QuotaGauge({ ); } + diff --git a/components/charts/sparkline.tsx b/components/charts/sparkline.tsx index 5d1196a..d7b3993 100644 --- a/components/charts/sparkline.tsx +++ b/components/charts/sparkline.tsx @@ -65,3 +65,4 @@ export function Sparkline({ ); } + diff --git a/components/charts/stacked-bars.tsx b/components/charts/stacked-bars.tsx index d00c2c2..062e09b 100644 --- a/components/charts/stacked-bars.tsx +++ b/components/charts/stacked-bars.tsx @@ -175,3 +175,4 @@ export function StackedBars({ ); } + diff --git a/components/charts/time-series.tsx b/components/charts/time-series.tsx index 7f7206f..ef4d8e3 100644 --- a/components/charts/time-series.tsx +++ b/components/charts/time-series.tsx @@ -168,3 +168,4 @@ export function TimeSeries({ ); } + diff --git a/components/expected-pages.tsx b/components/expected-pages.tsx index c666f8c..7ce47f8 100644 --- a/components/expected-pages.tsx +++ b/components/expected-pages.tsx @@ -1,33 +1,68 @@ +import { Badge, Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./ui"; + export function ExpectedPages() { return (
- Site map -

Expected pages (delivery backlog)

-

+ Site map +

Expected pages (delivery backlog)

+

This table is the contract between product and engineering. Routes marked scaffold ship as placeholders; planned routes are tracked for sprint planning.

-
- - - - - - - - - - - - - - - - - - -
RoutePurposeStatus
/Marketing hub + site mapScaffold
/productPersonas, pricing hooks, integration storyPlanned
/contractsSoroban modules and interaction flowsPlanned
/operatorsDashboard preview for AI gatewaysPlanned
/complianceAudit exports and policy packsPlanned
/roadmapMilestones vs grantsScaffold
/contributorsGood first issues and guild rolesPlanned
/docsTechnical reference hubScaffold
+
+ + + + Route + Purpose + Status + + + + + / + Marketing hub + site map + Scaffold + + + /product + Personas, pricing hooks, integration story + Planned + + + /contracts + Soroban modules and interaction flows + Planned + + + /operators + Dashboard preview for AI gateways + Planned + + + /compliance + Audit exports and policy packs + Planned + + + /roadmap + Milestones vs grants + Scaffold + + + /contributors + Good first issues and guild roles + Planned + + + /docs + Technical reference hub + Scaffold + + +
); } + diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..f196bf6 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,23 @@ +import { HTMLAttributes } from "react"; +import styles from "./ui.module.css"; + +type BadgeVariant = "default" | "success" | "warning" | "danger" | "neutral"; + +interface BadgeProps extends HTMLAttributes { + variant?: BadgeVariant; +} + +export function Badge({ className = "", variant = "default", ...props }: BadgeProps) { + const variantClass = { + default: styles.badgeDefault, + success: styles.badgeSuccess, + warning: styles.badgeWarning, + danger: styles.badgeDanger, + neutral: styles.badgeNeutral, + }[variant]; + + const rootClass = [styles.badge, variantClass, className].filter(Boolean).join(" "); + + return ; +} + diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..7afdbbb --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,34 @@ +import { ButtonHTMLAttributes, forwardRef, ElementType } from "react"; +import styles from "./ui.module.css"; + +type ButtonVariant = "primary" | "secondary" | "ghost"; +type ButtonSize = "sm" | "md" | "lg"; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + as?: ElementType; + href?: string; +} + +export const Button = forwardRef( + ({ className = "", variant = "primary", size = "md", as: Component = "button", ...props }, ref) => { + const variantClass = { + primary: styles.btnPrimary, + secondary: styles.btnSecondary, + ghost: styles.btnGhost, + }[variant]; + + const sizeClass = { + sm: styles.btnSm, + md: styles.btnMd, + lg: styles.btnLg, + }[size]; + + const rootClass = [styles.button, variantClass, sizeClass, className].filter(Boolean).join(" "); + + return ; + } +); +Button.displayName = "Button"; + diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..163bbbc --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,17 @@ +import { HTMLAttributes } from "react"; +import styles from "./ui.module.css"; + +interface CardProps extends HTMLAttributes { + interactive?: boolean; +} + +export function Card({ className = "", interactive, ...props }: CardProps) { + const rootClass = [ + styles.card, + interactive && styles.cardInteractive, + className + ].filter(Boolean).join(" "); + + return
; +} + diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx new file mode 100644 index 0000000..6f58919 --- /dev/null +++ b/components/ui/empty-state.tsx @@ -0,0 +1,21 @@ +import { HTMLAttributes, ReactNode } from "react"; +import styles from "./ui.module.css"; + +interface EmptyStateProps extends HTMLAttributes { + icon?: ReactNode; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon, title, description, action, className = "", ...props }: EmptyStateProps) { + return ( +
+ {icon &&
{icon}
} +

{title}

+ {description &&

{description}

} + {action &&
{action}
} +
+ ); +} + diff --git a/components/ui/index.ts b/components/ui/index.ts new file mode 100644 index 0000000..832b248 --- /dev/null +++ b/components/ui/index.ts @@ -0,0 +1,7 @@ +export * from './card'; +export * from './badge'; +export * from './button'; +export * from './table'; +export * from './stat'; +export * from './empty-state'; +export * from './skeleton'; diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..6aa394c --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,18 @@ +import { HTMLAttributes } from "react"; +import styles from "./ui.module.css"; + +interface SkeletonProps extends HTMLAttributes { + width?: string | number; + height?: string | number; +} + +export function Skeleton({ width, height, className = "", style, ...props }: SkeletonProps) { + return ( +
+ ); +} + diff --git a/components/ui/stat.tsx b/components/ui/stat.tsx new file mode 100644 index 0000000..35069be --- /dev/null +++ b/components/ui/stat.tsx @@ -0,0 +1,17 @@ +import { HTMLAttributes } from "react"; +import styles from "./ui.module.css"; + +interface StatProps extends HTMLAttributes { + label: string; + value: string | number; +} + +export function Stat({ label, value, className = "", ...props }: StatProps) { + return ( +
+

{label}

+ {value} +
+ ); +} + diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..ad578bc --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,47 @@ +import { HTMLAttributes, ThHTMLAttributes, TdHTMLAttributes, forwardRef } from "react"; +import styles from "./ui.module.css"; + +export const Table = forwardRef>( + ({ className = "", ...props }, ref) => ( +
+ + + ) +); +Table.displayName = "Table"; + +export const TableHeader = forwardRef>( + ({ className = "", ...props }, ref) => ( + + ) +); +TableHeader.displayName = "TableHeader"; + +export const TableBody = forwardRef>( + ({ className = "", ...props }, ref) => ( + + ) +); +TableBody.displayName = "TableBody"; + +export const TableRow = forwardRef>( + ({ className = "", ...props }, ref) => ( + + ) +); +TableRow.displayName = "TableRow"; + +export const TableHead = forwardRef>( + ({ className = "", ...props }, ref) => ( +
+ ) +); +TableHead.displayName = "TableHead"; + +export const TableCell = forwardRef>( + ({ className = "", ...props }, ref) => ( + + ) +); +TableCell.displayName = "TableCell"; + diff --git a/components/ui/ui.module.css b/components/ui/ui.module.css new file mode 100644 index 0000000..ec50c08 --- /dev/null +++ b/components/ui/ui.module.css @@ -0,0 +1,200 @@ +/* UI Primitives */ + +/* --- Card --- */ +.card { + background: linear-gradient(180deg, color-mix(in srgb, var(--surface) 88%, var(--bg)) 0%, var(--surface) 100%); + border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent); + border-radius: 14px; + padding: 18px; + display: flex; + flex-direction: column; +} + +.cardInteractive { + transition: transform 0.15s ease, border-color 0.15s ease; +} +.cardInteractive:hover { + border-color: color-mix(in srgb, var(--accent) 50%, transparent); + transform: translateY(-2px); +} + +/* --- Badge --- */ +.badge { margin-bottom: 10px; + display: inline-flex; + align-items: center; + padding: 4px 10px; + border-radius: 999px; + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + border: 1px solid transparent; + width: fit-content; +} + +.badgeDefault { + background: color-mix(in srgb, var(--accent) 14%, transparent); + border-color: var(--ring); + color: color-mix(in srgb, var(--accent) 85%, var(--text)); +} + +.badgeSuccess { + background: color-mix(in srgb, var(--accent-2) 14%, transparent); + border-color: color-mix(in srgb, var(--accent-2) 35%, transparent); + color: color-mix(in srgb, var(--accent-2) 85%, var(--text)); +} + +.badgeWarning { + background: color-mix(in srgb, #f59e0b 14%, transparent); + border-color: color-mix(in srgb, #f59e0b 35%, transparent); + color: #f59e0b; +} + +.badgeDanger { + background: color-mix(in srgb, #ef4444 14%, transparent); + border-color: color-mix(in srgb, #ef4444 35%, transparent); + color: #ef4444; +} + +.badgeNeutral { + background: color-mix(in srgb, var(--muted) 14%, transparent); + border-color: color-mix(in srgb, var(--muted) 35%, transparent); + color: var(--muted); +} + +/* --- Button --- */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 9px 16px; + border-radius: 8px; + font-weight: 600; + font-size: 0.92rem; + cursor: pointer; + border: 1px solid transparent; + transition: all 0.15s ease; + line-height: 1.4; + text-decoration: none; +} +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.btnPrimary { + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + color: #061018; /* Ensure dark text on light/gradient bg */ +} +.btnPrimary:hover:not(:disabled) { + filter: brightness(1.1); +} + +.btnSecondary { + background: transparent; + border-color: color-mix(in srgb, var(--accent) 45%, transparent); + color: var(--accent); +} +.btnSecondary:hover:not(:disabled) { + background: color-mix(in srgb, var(--accent) 12%, transparent); + border-color: color-mix(in srgb, var(--accent) 65%, transparent); +} + +.btnGhost { + background: transparent; + color: var(--text); +} +.btnGhost:hover:not(:disabled) { + background: color-mix(in srgb, var(--muted) 15%, transparent); +} + +.btnSm { padding: 6px 12px; font-size: 0.85rem; } +.btnMd { padding: 9px 16px; font-size: 0.92rem; } +.btnLg { padding: 12px 24px; font-size: 1rem; } + +/* --- Table --- */ +.tableContainer { + width: 100%; + overflow-x: auto; +} +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.92rem; +} +.table th, .table td { + text-align: left; + padding: 12px; + border-bottom: 1px solid color-mix(in srgb, var(--muted) 25%, transparent); +} +.table th { + color: var(--muted); + font-weight: 600; +} +.tableRow:hover td { + background: color-mix(in srgb, var(--surface) 60%, transparent); +} + +/* --- Stat --- */ +.stat { + display: flex; + flex-direction: column; +} +.statLabel { + color: var(--muted); + font-size: 0.82rem; + margin: 0 0 4px; +} +.statValue { + font-size: 1.7rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text); +} + +/* --- EmptyState --- */ +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 24px; + text-align: center; + border: 1px dashed color-mix(in srgb, var(--muted) 40%, transparent); + border-radius: 14px; + background: color-mix(in srgb, var(--surface) 40%, transparent); +} +.emptyStateIcon { + font-size: 2rem; + color: var(--muted); + margin-bottom: 16px; + opacity: 0.8; +} +.emptyStateTitle { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 8px; + color: var(--text); +} +.emptyStateDescription { + color: var(--muted); + font-size: 0.95rem; + max-width: 400px; + margin: 0; +} + +/* --- Skeleton --- */ +.skeleton { + background: color-mix(in srgb, var(--muted) 20%, transparent); + border-radius: 6px; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} diff --git a/tests/unit/brand-logo.test.tsx b/tests/unit/brand-logo.test.tsx index 10f6f0a..38c782a 100644 --- a/tests/unit/brand-logo.test.tsx +++ b/tests/unit/brand-logo.test.tsx @@ -19,3 +19,4 @@ describe("BrandLogo", () => { expect(container.querySelector("svg")).toHaveClass("nav-logo"); }); }); + diff --git a/tests/unit/expected-pages.test.tsx b/tests/unit/expected-pages.test.tsx index ec216ab..419afa5 100644 --- a/tests/unit/expected-pages.test.tsx +++ b/tests/unit/expected-pages.test.tsx @@ -44,3 +44,4 @@ describe("ExpectedPages", () => { } }); }); + From 60950e91cf5d4b3e4fd5f564804814322ce703e9 Mon Sep 17 00:00:00 2001 From: kosisochukwu1234 Date: Mon, 24 Aug 2026 18:43:53 +0100 Subject: [PATCH 2/2] docs: add usage documentation for UI primitives --- components/ui/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 components/ui/README.md diff --git a/components/ui/README.md b/components/ui/README.md new file mode 100644 index 0000000..2221f51 --- /dev/null +++ b/components/ui/README.md @@ -0,0 +1,21 @@ +# UI Primitives + +This directory contains the foundational UI components for the ModelTrace dashboard. + +## Guidelines + +- **Variants**: Components use typed variants instead of free-form class names. Always use the predefined variants (e.g., ``). +- **Theming**: All primitives are built to support both light and dark themes automatically via `globals.css` CSS custom properties. No extra theme props are needed. +- **Composition**: Primitives are designed to compose easily. Use them together to build complex views. For example, a `` inside a ``. +- **Accessibility**: Controls are accessible by default with visible focus rings. If a component lacks an accessible label inherently, ensure it is provided via `aria-label` or wrapping context. +- **No Inline Styles**: Do not use inline `style={{ ... }}` on page components. If spacing or layout is needed, use the utility classes in `globals.css` (e.g., `mt-16`, `mb-16`, `flex-between`, `grid-cols-auto`) or add new ones. + +## Components + +- **Card**: A foundational container for metrics, forms, and charts. Supports an `interactive` prop. +- **Badge**: A small label to signify status or categories. Variants: `default`, `success`, `warning`, `danger`, `neutral`. +- **Button**: Interactive elements. Supports polymorphic rendering via the `as` prop (e.g., `as={Link}`). Variants: `primary`, `secondary`, `ghost`. Sizes: `sm`, `md`, `lg`. +- **Table**: Composable tabular data elements (`Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`). +- **Stat**: Key-value pair for displaying metrics. +- **EmptyState**: A placeholder for missing or scaffolded content. +- **Skeleton**: A loading placeholder with a pulsing animation.