Skip to content

SkandaDX/sdxui

Repository files navigation

SkandaDX UI

Foundation of the SkandaDX digital ecosystem: a headless design system (React + TypeScript) plus a theme contract that a future theme builder will target to produce branded component libraries.

  • 54 unstyled, accessible primitives — behavior, keyboard interaction, and ARIA, zero visual opinion.
  • 1,848 themeable SVG icon components (@sdxui/icons) — 32×32 by default, colored via currentColor so --sdx-* tokens apply.
  • A theme is a plain JSON object. themeToCss() / applyTheme() turn it into --sdx-* CSS custom properties.
  • A Storybook workspace ships every primitive with playgrounds, props tables, and live Brand + Mode toolbars (19 bundled brands × light/dark — 38 themes) proving that brands re-skin without touching component code.

Architecture

┌─────────────────────────────────────────────────────────┐
│  Brand component libraries (future)                     │
│  e.g. @sdxui/brand-acme = primitives + acme theme     │
├──────────────────┬───────────────────┬──────────────────┤
│ @sdxui/react  │ @sdxui/icons   │ @sdxui/tokens │
│ headless         │ 1,848 themeable   │ Theme contract,  │
│ components:      │ SVG icons —       │ default theme,   │
│ behavior + a11y +│ 32×32 default,    │ createTheme(),   │
│ data-* hooks,    │ currentColor →    │ themeToCss() /   │
│ zero visual      │ --sdx-* tokens    │ applyTheme() →   │
│ opinion          │ apply             │ --sdx-* CSS vars │
├──────────────────┴───────────────────┴──────────────────┤
│  Theme builder (future) — edits a `Theme` JSON object,   │
│  previews live via applyTheme(), exports CSS/JSON        │
└─────────────────────────────────────────────────────────┘
  • packages/react — unstyled, accessible components in the spirit of Radix UI / Base UI. Each component ships behavior, keyboard interaction, ARIA semantics, and data-state / data-disabled / data-orientation attributes as CSS hooks. Every part supports asChild to render onto your own element.
  • packages/icons — 1,848 SVG icon components generated from Lucide (ISC, free for commercial use). Icons render 32×32 by default and draw with currentColor, so setting color: var(--sdx-color-primary-600) (or any theme token) themes them; size, color, and strokeWidth props override per-use. Each icon is published as its own module (@sdxui/icons/<name>) so client apps only ship the icons they import.
  • packages/tokens — the theming seam. A Theme is a plain JSON object (color scales, semantic colors, typography, space, radii, shadows, motion). themeToCss() renders it to --sdx-* CSS variables; applyTheme() applies it at runtime for live preview; createTheme() derives brand themes from the default.
  • apps/storybook — Storybook showcasing every primitive with interactive prop controls, auto-generated props tables, usage samples, and a toolbar brand switcher (all 22 bundled themes) proving that brands re-skin without touching component code.
  • apps/docs — the SDX UI documentation portal: reference docs, live previews, props/methods/token tables, and consumer-integration guides for all three packages, built with React + TypeScript + Vite + plain CSS. Run with pnpm --filter @sdxui/docs dev.

Repository layout

sdxui/
├── apps/
│   ├── docs/               Documentation portal — see apps/docs/README.md
│   └── storybook/          Storybook workspace — playgrounds, docs, theme switcher
│       └── src/
│           ├── stories/    one *.stories.tsx per primitive
│           └── brand.css   example brand stylesheet consuming --sdx-* tokens
├── packages/
│   ├── icons/              @sdxui/icons — 1,848 themeable SVG icon components
│   │   ├── scripts/        generate-icons.mjs — regenerates the set from lucide-static
│   │   └── src/            createIcon() factory + generated icons/ + iconNames manifest
│   ├── react/              @sdxui/react — headless components + shared utilities
│   │   └── src/
│   │       ├── utils/      Slot, Primitive, Portal, popper, focus-trap, controllable state, …
│   │       └── <name>/     one folder per component, colocated with its *.test.tsx
│   └── tokens/              @sdxui/tokens — theme contract, themes, CSS generation
│       └── src/
│           ├── themes/      defaultTheme, createTheme + one file per brand theme
│           └── …            Theme types, themeToCss, applyTheme
├── pnpm-workspace.yaml
└── tsconfig.base.json

Components (54)

Tier Components
Basic Button, Label, Separator, Avatar, Progress, Badge, Kbd, Card, Split Button
Feedback Alert, Skeleton, Spinner
Layout Aspect Ratio, Scroll Area, Resizable (panel group), Carousel
Form controls Form, Text Field, Textarea, Number Field, Pin Input, Tags Input, File Upload, Checkbox, Radio Group, Switch, Toggle, Toggle Group, Rating, Slider (multi-thumb)
Disclosure Collapsible, Accordion (single/multiple), Tabs
Navigation Toolbar, Navigation Menu, Breadcrumb, Pagination
Data display Table (sortable/selectable), Tree, Timeline
Date Calendar, Date Picker
Overlays / advanced Tooltip, Popover, Hover Card, Dialog, Alert Dialog, Dropdown Menu, Context Menu, Menubar, Select, Combobox, Command, Toast

Every component is exported from @sdxui/react — see packages/react/src/index.ts for the full export list (parts + prop types).

Icons (1,848)

@sdxui/icons ships the full Lucide set as theme-aware React components. Every icon is its own published module with a subpath export, so client apps bundle only the icons they import (works even without tree-shaking; the root barrel import also exists for bundler users):

import { SearchIcon } from '@sdxui/icons/search';
import { BellIcon } from '@sdxui/icons/bell';
import { CircleCheckIcon } from '@sdxui/icons/circle-check';

<SearchIcon />                                                        // 32×32, inherits CSS color
<CircleCheckIcon style={{ color: 'var(--sdx-color-success-600)' }} /> // themed via tokens
<BellIcon size={20} strokeWidth={1.5} title="Notifications" />        // sized + accessible

Browse and search the whole set in Storybook under Basics/Icons → Gallery. See packages/icons/README.md for props, custom icons via createIcon(), and regeneration instructions.

Getting started

Requires pnpm (see packageManager in package.json) and Node 18+.

pnpm install    # installs all workspaces
pnpm build      # builds @sdxui/tokens + @sdxui/react
pnpm dev        # starts Storybook at http://localhost:6006

All commands

Run from the repo root (pnpm workspace filters fan these out to the right package):

Command Description
pnpm install Install dependencies for every workspace
pnpm dev Start Storybook dev server (localhost:6006)
pnpm build Build @sdxui/tokens, @sdxui/react, and @sdxui/icons (tsup → ESM + CJS + .d.ts)
pnpm build:storybook Build the static Storybook site
pnpm typecheck tsc --noEmit across every workspace
pnpm test Run the Vitest suite for every workspace
pnpm test:coverage Run tests with V8 coverage (text + html + lcov)

Per-package scripts (run inside packages/react or packages/tokens, or via pnpm --filter <name> run <script>):

  • dev (primitives only) — tsup --watch
  • test:watch — Vitest in watch mode

Using a primitive

Primitives render no styles of their own — they render structure, behavior, and data-* attributes for you to hook into with your own CSS or brand layer.

import {
  Dialog,
  DialogTrigger,
  DialogPortal,
  DialogOverlay,
  DialogContent,
  DialogTitle,
  DialogClose,
} from '@sdxui/react';

<Dialog>
  <DialogTrigger className="button">Open</DialogTrigger>
  <DialogPortal>
    <DialogOverlay className="overlay" />
    <DialogContent className="dialog">
      <DialogTitle>Title</DialogTitle>
      <DialogClose className="button">Close</DialogClose>
    </DialogContent>
  </DialogPortal>
</Dialog>;

Style with CSS against class names and the exposed data attributes:

.dialog {
  background: var(--sdx-color-semantic-background);
  border-radius: var(--sdx-radius-xl);
}
.button[data-state='open'] {
  background: var(--sdx-color-primary-700);
}

asChild composition

Any part can delegate rendering to its child while keeping behavior — useful when composing with your own styled button, a router <Link>, etc.

<DialogTrigger asChild>
  <MyBrandButton>Open</MyBrandButton>
</DialogTrigger>

More examples

Select — composed of trigger, portal, viewport, and items; ships a hidden native <select> when given name so it participates in form submission:

import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectIcon,
  SelectPortal,
  SelectContent,
  SelectViewport,
  SelectItem,
  SelectItemText,
  SelectItemIndicator,
} from '@sdxui/react';

<Select defaultValue="react" name="framework">
  <SelectTrigger className="select-trigger">
    <SelectValue placeholder="Pick a framework" />
    <SelectIcon />
  </SelectTrigger>
  <SelectPortal>
    <SelectContent className="menu">
      <SelectViewport>
        {['react', 'vue', 'svelte', 'solid'].map((fw) => (
          <SelectItem key={fw} value={fw} className="menu-item">
            <SelectItemIndicator></SelectItemIndicator>
            <SelectItemText>{fw}</SelectItemText>
          </SelectItem>
        ))}
      </SelectViewport>
    </SelectContent>
  </SelectPortal>
</Select>;

Toast — self-closing, uncontrolled by default; mount one Toast per notification and drop it from state on onOpenChange(false):

import { Toast, ToastTitle, ToastDescription, ToastClose } from '@sdxui/react';

<Toast duration={4000} type="background" onOpenChange={(open) => !open && dismiss(id)}>
  <ToastTitle>Saved</ToastTitle>
  <ToastDescription>Your changes have been saved.</ToastDescription>
  <ToastClose aria-label="Dismiss"></ToastClose>
</Toast>;

Accordion — single or multiple expansion via a discriminated type prop, controlled or uncontrolled:

import { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from '@sdxui/react';

<Accordion type="single" collapsible defaultValue="item-1">
  <AccordionItem value="item-1">
    <AccordionHeader>
      <AccordionTrigger>Section one</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content for section one.</AccordionContent>
  </AccordionItem>
</Accordion>;

More runnable examples for every component live in apps/storybook/src/stories — run pnpm dev and open the matching story for props, controls, and edge cases.

Theming — the theme-builder contract

A brand theme is data, not code:

import { createTheme, themeToCss, applyTheme } from '@sdxui/tokens';

const acme = createTheme('acme', {
  colors: { primary: {/* 50–950 scale */}, semantic: { focusRing: '#e11d48' } },
  radii: { md: '0.25rem' },
});

applyTheme(acme); // runtime (live preview in the theme builder)
const css = themeToCss(acme); // build time → ship as a .css file

The generated variables are namespaced (--sdx-color-primary-600, --sdx-space-4, --sdx-radius-md, --sdx-shadow-overlay, --sdx-motion-duration-fast, …) and scoped to [data-sdx-theme="<name>"], so multiple brands can coexist on one page. applyTheme also stamps data-sdx-mode="light|dark" (from the theme's mode field), so mode-conditional CSS can target the root independently of the brand.

The package also ships ready-made brand themes (skandaLight, emeraldBrand, nordDark, draculaDark, …, plus a themes array) — each a fully-resolved Theme object in its own file under packages/tokens/src/themes/.

Brands × color modes

Brand and color mode are independent theme dimensions. Every bundled brand pairs a complete light theme and dark theme in the brands registry:

import { applyTheme, getBrand, brands, defaultBrand } from '@sdxui/tokens';

const brand = getBrand('nord') ?? defaultBrand; // preference fallback
const mode = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
applyTheme(brand.themes[mode]);

Persist the brand preference and the color-mode preference ('light' | 'dark' | 'system') separately — resolve 'system' at runtime and never store the resolved value, so the app keeps following the OS while that preference is active. The docs app (apps/docs/src/utils/theme.ts) is the reference implementation, including a build-time static stylesheet of every brand-and-mode theme and an inline no-flash script (see the sdx-theme-css plugin in apps/docs/vite.config.ts).

The Theme shape

Defined in packages/tokens/src/utils/theme.ts:

Section Contents
colors primary / neutral / success / warning / danger — each a 50–950 ColorScale — plus semantic (background, surface, text, border, focusRing, …)
typography fontFamily, fontFamilyMono, fontSize (xs–3xl), fontWeight, lineHeight
space Scale from 1 to 16
radii sm / md / lg / xl / full
shadows sm / md / lg / overlay
motion durationFast / durationNormal / durationSlow / easing

createTheme(name, override) deep-merges a ThemeOverride (any subset of the shape above) onto defaultTheme, so brand themes only need to specify what differs.

Example themes

@sdxui/tokens ships 19 brands — 38 bundled themes (see packages/tokens/README.md for the full list) as stand-ins for what a future theme builder will produce. apps/storybook/.storybook/preview.tsx registers every one of them and switches live via the Storybook toolbar's Brand and Mode controls:

skanda (default) · emerald · midnight · graphite (shadcn/zinc-inspired) · material (Material 3) · ant (Ant Design) · fluent (Microsoft Fluent 2) · chakra (Chakra UI) · nord · dracula · and 9 more (Catppuccin, Solarized, Gruvbox, Rosé Pine, Everforest, One, Primer, Cupcake, Synthwave) — each in light and dark.

Design considerations

  • Headless first: primitives render structure, keyboard behavior, and ARIA — never CSS or visual defaults. All styling hooks are className (or asChild) plus data attributes; the same component looks identical to the browser whether it's unstyled or fully rebranded.
  • Controlled + uncontrolled: every stateful component accepts value/onValueChange (controlled) or defaultValue (uncontrolled) — same pattern for open, checked, pressed, driven by the shared useControllableState hook.
  • Data attributes over props for styling: data-state="open|closed|checked|active|…", data-disabled, data-side/data-align on positioned content — style against these instead of reaching into internals.
  • Forms: Checkbox, Switch, Radio Group, Select, and Slider render a hidden native input when given a name, so they participate in plain <form> submission and native validation without extra glue.
  • Positioning: Tooltip, Popover, Hover Card, Dropdown Menu, Context Menu, Menubar, and Select share one popper layer (@floating-ui/react-dom) with side, align, sideOffset, and collision handling.
  • Composition over configuration: complex components (Dialog, Select, Accordion, Toast, …) are assembled from small parts (Trigger, Portal, Content, Item, …) rather than one prop-heavy component — compose only what you need.
  • asChild everywhere: any part can delegate its DOM element to a child via the shared Slot primitive, so primitives compose cleanly with your own styled components or a router's <Link>.
  • forceMount is available on conditional parts (content that mounts/unmounts) for animation libraries that need the node kept in the DOM during exit transitions.
  • Theme as data: a Theme is a plain, JSON-serializable object — never TS/JS logic — because it must round-trip through a future visual theme builder, be diffed, and be exported as CSS or JSON.
  • Every component is tested: each component.tsx is colocated with a component.test.tsx (Vitest + Testing Library); tokens and core utilities carry the same coverage discipline.

Contributing

See CONTRIBUTING.md.

Roadmap

  • Theme builder app (edits Theme JSON, previews via applyTheme, exports CSS + JSON).
  • Branded component packages composed from primitives + a theme (e.g. @sdxui/brand-acme).
  • Exit-animation presence handling (currently only mount animations are first-class).
  • Dropdown Menu / Context Menu / Menubar submenus (nested menu items) — typeahead is already implemented (useRovingFocus's typeahead option).
  • Date range selection for Date Picker (currently single-date only).
  • Visual regression testing in CI; automated per-story axe-core checks are already wired via Storybook's @storybook/addon-a11y.
  • @sdxui/react, @sdxui/tokens, and @sdxui/icons already auto-publish to npm on version bumps to main (.github/workflows/publish.yml) — next step is Changesets-driven independent versioning + changelogs instead of manual lockstep version bumps.

About

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors