@dcs-tech/ui
React UI components and design tokens — form fields, buttons, tooltips, popups and layout primitives, built on one shared control surface so every control agrees on size, spacing and state.
Install
npm i @dcs-tech/ui react react-dom
React >=18 is a peer dependency — your app brings its own copy.
Use
import { Button, Input, Select, Suggest, Tooltip, Typography } from '@dcs-tech/ui';
import '@dcs-tech/ui/style.css'; // tokens + component CSS, import once at the root
export const Example = () => (
<>
<Input label="Party name" tooltip="Legal name as printed on the invoice" />
<Select label="Material" options={[{ name: 'Cotton', value: 'cotton' }]} search group />
<Tooltip content="Deletes this row"><Button color="error">Delete</Button></Tooltip>
<Typography variant="body1" truncate={{ lines: 2 }}>Long text…</Typography>
</>
);
What's in it
- Fields — Input, Select, Suggest, DatePicker (a day, a range, or
picker="month"/"year"), Checkbox, Radio, Switch, ChipInput (including free-texttags), all sharing one control surface, a 6-step size scale and one state model - Images — ImagePicker, ImageStrip, ImageViewer, and ImageEditor (crop, zoom, rotate,
straighten, flip; each tool can be allowed or restricted).
prepareImageapplies the same transform with no UI - Overlays — Dialog (
useDialog()for confirms;DialogHostif you do not useUIProvider), Popover, DropdownMenu, Toast - Primitives — Button, Typography, Tooltip, Alert (with a page-wide
bannerform) - Navigation — Link (
astakes a router's link), Pagination, CommandPaletteTrigger, PageProgress (a route-change bar, driven byactive), NavList (a vertical link list for inside a page) - Layout — Collapsible, FieldWrapper
- Lists — ReorderList (drag to reorder: rows slide live, with mouse, touch or keyboard)
- Composites — Form, Fields, EditableTable
- Data — DataTable (client data or a server
dataSource: views, search, filters, sort, grouping, column chooser, table / card / split layouts,toolbarActions, selection + bulk actions (the table reports whatonRunreturns, and nothing if it returns nothing), header / footer summaries with charts, export to xlsx / pdf / csv / html / json / xml) and Charts (Chart, ColumnChart, BarList, Donut, Progress, Legend, ChartTable, LineChart, Funnel, Gauge, Treemap, … with a picture menu) - Scroll — one scrollbar rule (thin, hidden until needed) in
style.css, edge fades (.scroll-fade-x/.scroll-fade-y) andScrollStripfor tab / chip rows.UIProviderstarts its listener; without it, mount<ScrollRule />once - Tokens — a hue-variable theme:
--primary-hue: 211retints the whole palette from one number, with light and dark themes keyed offdata-theme
DataTable and Charts
import { DataTable, Donut, type DataTableView } from '@dcs-tech/ui';
const view: DataTableView<Order> = {
id: 'all', rowKey: 'id',
columns: [
{ key: 'customer', header: 'Customer' },
{ key: 'amount', header: 'Amount', type: 'number', align: 'right', summable: true },
],
export: { quick: { filename: 'Orders' } },
};
<DataTable data={orders} views={[view]} tableId="orders" />; // client mode
<DataTable dataSource={source} views={[view]} tableId="orders" />; // server mode
<Donut ariaLabel="By status" slices={[{ key: 'a', label: 'Open', value: 12, slot: 1 }]} />;
The table's runtime dependencies (@tanstack/react-table, @tanstack/react-virtual,
@tanstack/match-sorter-utils, exceljs and pdfmake — loaded on the first PDF export)
install with the package. The icons are the library's own (IconFilter, filterIcon, …): it
has no icon dependency.
Files and the background worker
Exports (DataTable, chart images, your own reports) run through one file engine that
UIProvider sets up. Excel and PDF files build in a background worker, so a large
export does not freeze the page.
The worker ships as its own file and imports exceljs and pdfmake by package name, so
your bundler must treat it as a worker entry:
-
Vite and webpack 5: nothing to do. They follow the package's
new Worker(new URL(…, import.meta.url))and bundle the worker themselves. -
Anything else: build a worker from
@dcs-tech/ui/file-workerwith your bundler and hand it to the engine:// file.worker.ts — bundled by your setup as a module worker import '@dcs-tech/ui/file-worker';<UIProvider fileEngine={{ createWorker: () => new Worker('/assets/file.worker.js', { type: 'module' }) }}>
If a worker cannot start, the engine builds the same file on the main thread instead.
fileEngine={{ workers: 'off' }} always uses the main thread. Large PDF fonts (a script font)
belong on the engine, so they reach each worker once: fileEngine={{ pdfFonts: { fonts, vfs } }}.
Form validation
Form validates with any Standard Schema library — Zod 3.24+,
Valibot 1+, ArkType 2+, Yup 1.7+ — with no adapter:
import { z } from 'zod'
<Form fields={fields} defaultData={data} validation={{ schema: z.object({ name: z.string().min(1, 'Required') }), validateOnBlur: true }} />
Errors are keyed by each issue's path (address.city). Leaving a field checks only that field; a
failed submit shows every error and focuses the first. For other schemas (e.g. Yup before 1.7),
pass validate: (schema, data) => ({ valid, errors }) as before. The form's words (Submit, Reset,
its error toasts) come from UIProvider labels.form.
Theming
Every colour, size and spacing value is a CSS custom property. Override them on
:root after importing the stylesheet:
:root { --primary-hue: 264; } /* retints the entire theme */
:root { --border-radius-sm: 4px; }
Sizes and variants
size is uniform across every control: xxs | xs | small | medium | large | xl.
variant is per family — outlined | filled | standard | underlined for text
fields, outlined | filled | standard for Checkbox and Radio, none for Switch.
Field width
A text field fills the space its parent gives it, up to 320px. width changes that:
width | The field is |
|---|---|
| not given | the available space, up to 320px |
"full" | the whole row, with no limit |
"auto" | as wide as its content |
240, "15rem" | exactly that, never wider than its parent |
In a table cell (layout="cell") the column decides and width is ignored. With layout="horizontal"
it sizes the control; the label keeps its column.
List width
A Select or Suggest list fits its content: never narrower than its field, up to 320px (Select) or
480px (Suggest), and always on screen. listWidth="field" makes it exactly the field's width.
Icon size
An icon-only button or a Sidebar row sizes its icon with the --icon-size-xxs … --icon-size-xl
tokens, which follow --ui-scale. Override them on :root to change icon sizes across the app. An
icon given its own size keeps it.
Dialog width
Dialog takes size="sm | md | lg | xl | full" (420 / 560 / 720 / 960 px / the whole
window). For any other width, set --dialog-width. It is the supported way, and it wins
over size:
<Dialog open title="Wide" style={{ '--dialog-width': '840px' } as React.CSSProperties} />
.invoice-dialog { --dialog-width: min(1100px, 95vw); } /* <Dialog className="invoice-dialog"> */
For a side or top/bottom sheet (placement), it sets the panel's width or its max width.
Licence
UNLICENSED — © 2026 Noyyal Tech. All rights reserved.