@editora/ui-react
v0.1.28
Published
Enterprise React component library with SaaS UI components, modern design system patterns, and wrappers for Editora UI Core.
Maintainers
Readme
Features
- A broad catalog: inputs and forms, pickers, tables, tabs, menus, dialogs, drawers, charts, a calendar and a Gantt planner. The full list.
- Typed props and events. Callbacks receive a typed
detail, not a raw DOM event. - Forms that validate:
<Form>collects the values of the controls inside it, anduseForm()drives it. - Promise dialogs:
await dialog.confirm({...}), with providers that work in ReactStrictMode. - Theming with design tokens, persisted if you want.
- Floating layers: hooks for popovers, menus and tooltips with flip, shift and focus management.
- Wrappers for
@editora/ui-core, so the components are standard custom elements underneath.
Install
npm install @editora/ui-react@editora/ui-core comes with the package. It is tested with React 18 and 19 (the peer range is React 16.8 to 20). These components render custom elements, which are
defined in the browser only: see Server rendering.
Quick start
import { Button, Input, ThemeProvider } from '@editora/ui-react';
export default function App() {
return (
<ThemeProvider>
<div style={{ display: 'grid', gap: 12, maxWidth: 360 }}>
<Input name="title" label="Title" placeholder="Untitled" clearable />
<Button variant="primary">Save</Button>
</div>
</ThemeProvider>
);
}The picture at the top of this page is made of these components:
import { Alert, Avatar, Badge, Button, Checkbox, DatePicker, Input, Progress, Switch } from '@editora/ui-react';
export default function Gallery() {
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, alignItems: 'start' }}>
<div style={{ display: 'grid', gap: 16 }}>
<Input label="Project name" description="Shown in the sidebar" placeholder="Untitled" clearable />
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="ghost">Skip</Button>
<Button variant="danger">Delete</Button>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Badge tone="success" text="Live" />
<Badge tone="warning" text="Review" />
<Badge tone="danger" text="Failed" />
<Avatar initials="AM" />
<Avatar initials="JK" />
</div>
<Alert title="Heads up" description="Your trial ends in 3 days." tone="warning" />
</div>
<div style={{ display: 'grid', gap: 16 }}>
<Switch checked label="Email notifications" description="A summary every Monday" />
<Checkbox checked>Remember me</Checkbox>
<Progress value={64} />
<DatePicker label="Start date" clearable />
</div>
</div>
);
}Import from the package or from one component
Importing from the package root is the simplest, and it is the heaviest: every wrapper and every component's custom element is in the bundle, and the bundler cannot drop the ones you do not use. Each component also has its own entry:
// Everything from the package root: the simplest, and the heaviest.
import { Button, Input } from '@editora/ui-react';
// One component per entry: a lighter first load.
import ButtonOnly from '@editora/ui-react/Button';
import InputOnly from '@editora/ui-react/Input';
export default function Imports() {
return (
<div style={{ display: 'grid', gap: 12, maxWidth: 360 }}>
<Input label="From the root" />
<InputOnly label="From its own entry" />
<Button>Root</Button>
<ButtonOnly>Own entry</ButtonOnly>
</div>
);
}Measured with esbuild, minified, with code splitting, importing one component and leaving React out. This is everything the import needs:
| Import | Gzipped |
| --- | --- |
| import { Button } from '@editora/ui-react' | 1 MB |
| import Button from '@editora/ui-react/Button' | 55 KB |
| import Input from '@editora/ui-react/Input' | 8 KB |
| import DataTable from '@editora/ui-react/DataTable' | 14 KB |
Each entry registers only the custom elements its component renders, so nothing else is fetched later. The package root registers the whole catalog on purpose, which is why it is the heavy one.
Forms
<Form> finds the named controls inside it. useForm() gives you its methods; onDirtyChange tells you when the
values differ from the starting ones:
import { useState } from 'react';
import { Form, Field, Input, Button, useForm } from '@editora/ui-react';
export default function ProfileForm() {
const { ref, submit, validate, getValues, reset } = useForm();
const [dirty, setDirty] = useState(false);
return (
<Form
ref={ref}
autosave
guardUnsaved
onSubmit={(values) => console.log('submit', values)}
onInvalid={(errors) => console.log('invalid', errors)}
onDirtyChange={(isDirty) => setDirty(isDirty)}
style={{ display: 'grid', gap: 12, maxWidth: 520 }}
>
<Field label="Full name" required>
<Input name="fullName" required />
</Field>
<Field label="Email" required>
<Input name="email" type="email" required />
</Field>
<div style={{ display: 'flex', gap: 8 }}>
<Button variant="secondary" onClick={() => validate()}>Validate</Button>
<Button variant="secondary" onClick={() => console.log(getValues())}>Values</Button>
<Button variant="secondary" onClick={() => reset()}>Reset</Button>
<Button onClick={() => submit()} disabled={!dirty}>Submit</Button>
</div>
</Form>
);
}useForm() returns ref, submit(), validate(), getValues(), reset(values?), isDirty() and markClean().
autosave reports the values through onAutosave after they stop changing, and guardUnsaved warns before the page is
left with unsaved changes.
Promise dialogs
Wrap your app in the providers, then await the answer from anywhere inside:
import { DialogProvider, useDialog, AlertDialogProvider, useAlertDialog, Button } from '@editora/ui-react';
function Actions() {
const dialog = useDialog();
const alerts = useAlertDialog();
return (
<div style={{ display: 'flex', gap: 8 }}>
<Button
onClick={async () => {
const res = await dialog.confirm({
title: 'Archive project?',
description: 'You can restore it later.',
submitText: 'Archive'
});
console.log(res);
}}
>
Confirm
</Button>
<Button
variant="secondary"
onClick={async () => {
const res = await alerts.prompt({
title: 'Rename',
input: { required: true, placeholder: 'New name' }
});
console.log(res);
}}
>
Prompt
</Button>
</div>
);
}
export default function DialogExample() {
return (
<DialogProvider>
<AlertDialogProvider>
<Actions />
</AlertDialogProvider>
</DialogProvider>
);
}useDialog() has open() and confirm(). useAlertDialog() is for confirmations and prompts that need an answer.
Both resolve with an object whose action says what the user did. The providers work in StrictMode.
Data tables
DataTable gives a table sorting, selection, column resizing and paging. Link a Pagination to it with
paginationId:
import { DataTable, Pagination } from '@editora/ui-react';
import { useState } from 'react';
export default function UsersTable() {
const [page, setPage] = useState(1);
return (
<div style={{ display: 'grid', gap: 10 }}>
<DataTable
sortable
selectable
resizableColumns
draggableColumns
page={page}
pageSize={2}
paginationId="users-pager"
onPageChange={(d) => setPage(d.page)}
onSortChange={(d) => console.log('sort', d)}
onRowSelect={(d) => console.log('rows', d.indices)}
>
<table>
<thead>
<tr>
<th data-key="id">ID</th>
<th data-key="name">Name</th>
<th data-key="role">Role</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>Asha</td><td>Admin</td></tr>
<tr><td>2</td><td>Marco</td><td>Editor</td></tr>
<tr><td>3</td><td>Lena</td><td>Viewer</td></tr>
<tr><td>4</td><td>Tariq</td><td>Editor</td></tr>
</tbody>
</table>
</DataTable>
<Pagination id="users-pager" page={page} />
</div>
);
}Pickers
import { DatePicker, DateRangePicker, DateTimePicker, ColorPicker } from '@editora/ui-react';
export default function PickersDemo() {
return (
<div style={{ display: 'grid', gap: 12, maxWidth: 420 }}>
<DatePicker label="Start date" clearable onValueChange={(v) => console.log(v)} />
<DateRangePicker label="Window" closeOnSelect />
<DateTimePicker label="Publish at" />
<ColorPicker mode="popover" format="hex" alpha presets={['#2563eb', '#16a34a', '#dc2626']} />
</div>
);
}Theming
ThemeProvider applies the design tokens of @editora/ui-core as CSS variables. With storageKey it remembers the
theme in localStorage:
import { Button, ThemeProvider } from '@editora/ui-react';
export default function Themed() {
return (
<ThemeProvider
tokens={{
colors: { primary: '#0f766e', text: '#0f172a', background: '#ffffff' },
radius: '10px'
}}
storageKey="my-app.theme"
>
<Button variant="primary">Save</Button>
</ThemeProvider>
);
}Gantt planner
Gantt is a planning timeline: tasks, summary rows and milestones, dependencies, baselines, critical tasks, drag and
resize, keyboard selection and a toolbar for zoom, filter and sort.
import { Gantt, type GanttLink, type GanttTask } from '@editora/ui-react';
import { useState } from 'react';
const initialTasks: GanttTask[] = [
{
id: 'foundation',
label: 'Foundation',
start: '2026-02-01',
end: '2026-02-18',
progress: 68,
baselineStart: '2026-01-29',
baselineEnd: '2026-02-14',
critical: true
},
{
id: 'handoff',
label: 'Handoff milestone',
start: '2026-02-20',
type: 'milestone',
tone: 'success'
}
];
const links: GanttLink[] = [
{ id: 'foundation-handoff', source: 'foundation', target: 'handoff', type: 'e2s' }
];
export default function PlanningDemo() {
const [tasks, setTasks] = useState(initialTasks);
return (
<Gantt
tasks={tasks}
links={links}
zoom="week"
sort="start"
barVariant="soft"
onTaskChange={({ id, start, end }) => {
setTasks((items) => items.map((task) => task.id === id ? { ...task, start: start ?? task.start, end: end ?? task.end } : task));
}}
onTaskDelete={({ id }) => setTasks((items) => items.filter((task) => task.id !== id))}
onLinkSelect={(detail) => console.log('dependency selected', detail)}
/>
);
}- Dependency types:
e2s,s2s,e2eands2e. Bar designs:solid,soft,striped,outlineandglass. - Also in the data: baselines, critical tasks, milestones, parent and child summaries, assignees, progress and split
segments. Row virtualization turns on for large task sets; pass
virtualized={false}for small custom layouts. - Planner features are props:
zoom(includingfit),weekStart,locale,labels,columns,gridWidth,fill,maxHeight,snap,nonWorkingDays,holidays,autoSummary,cascade,schedule="auto",criticalPath,linkable,creatableandinitialScroll. The chart reports what the user does (onBeforeTaskChange, which can refuse a change withpreventDefault(),onTaskChange,onTaskRename,onTaskCreate,onLinkCreate,onLinkDelete,onTaskToggle,onGridResize) and your state stays the source of truth. - A ref gives the element's methods:
scrollToDate,scrollToTask,toggleTask,expandAll,collapseAllandexportSvg(the whole chart as an SVG string). The chart also prints as a whole. - The attribute-by-attribute guide, with a full planner example and screenshot, is in the
@editora/ui-coreREADME; the props above are the same options. - The types
GanttTask,GanttLink,GanttTaskChangeDetail,GanttLinkSelectDetail,GanttLabels,GanttColumn,GanttBeforeTaskChangeDetail,GanttTaskRenameDetail,GanttTaskCreateDetail,GanttLinkCreateDetailandGanttElementare exported.
Floating layers
useFloating() positions a layer next to its anchor; useInteractions() combines the interaction hooks. This is the
layer the menus, popovers and tooltips are built on:
import {
FloatingArrow,
FloatingFocusManager,
FloatingPortal,
useClick,
useDismiss,
useFloating,
useInteractions
} from '@editora/ui-react';
export default function Example() {
const floating = useFloating({
placement: 'bottom-start',
offset: 10,
flip: true,
shift: true,
fitViewport: true,
strategy: 'fixed'
});
const interactions = useInteractions([
useClick(floating.context),
useDismiss(floating.context)
]);
return (
<>
<button ref={floating.referenceRef} {...interactions.getReferenceProps()}>
Open
</button>
{floating.open ? (
<FloatingPortal>
<FloatingFocusManager context={floating.context}>
<div
ref={floating.floatingRef}
{...interactions.getFloatingProps({
style: {
position: 'fixed',
top: floating.coords.top,
left: floating.coords.left
}
})}
>
<FloatingArrow context={floating.context} fill="#fff" stroke="#111827" />
Floating content
</div>
</FloatingFocusManager>
</FloatingPortal>
) : null}
</>
);
}More hooks: useHover, useFocus, useRole, useListNavigation and useTypeahead. More helpers: FloatingOverlay,
FloatingTree, FloatingList and FloatingDelayGroup. For the same engine without React, see createPositioner in
@editora/ui-core.
Components
Forms and inputs:
Form, Field, Label, Input, Textarea, Select, Combobox,
Checkbox, Radio, RadioGroup, Switch, Slider,
DatePicker, DateRangePicker, TimePicker, DateTimePicker, DateRangeTimePicker, ColorPicker
Data and display:
Table, DataTable, Pagination, Calendar, Chart, Timeline, Gantt,
Badge, Alert, Skeleton, EmptyState, Progress, Avatar, AspectRatio
Overlays and interaction:
Tooltip, HoverCard, Popover, Dropdown, Menu, Menubar, ContextMenu,
Dialog, AlertDialog, Drawer, Portal, Presence,
CommandPalette, QuickActions, Toolbar, FloatingToolbar, BlockControls, SelectionPopup, PluginPanel
Navigation and layout:
Layout, Sidebar, AppHeader, Breadcrumb, NavigationMenu, Tabs,
Box, Flex, Grid, Section, Container, Separator, Slot, VisuallyHidden,
Accordion, Collapsible, Stepper, Wizard, DirectionProvider
Providers and hooks:
DialogProvider, useDialog, AlertDialogProvider, useAlertDialog, ThemeProvider, useTheme, useForm, useFloating
Server rendering
The package can be imported on the server: loading a module does not touch the DOM, and the custom elements are defined
only in the browser. A 'use client' file that uses these components is still rendered on the server first (Next.js
does this), and that HTML carries the <ui-*> tags, which upgrade once the browser has loaded the package.
In React Server Components import from @editora/ui-react/server. It has no hooks, so a server component can load it,
and it holds the components that need none: Box, Flex, Grid, Container, Section, Separator, Anchor,
AspectRatio, Field, Label, Icon, Kbd, Shortcut, Stat, MetricCard, CodeBlock, CodeSnippet,
DirectionProvider and VisuallyHidden. It only renders the markup. To define the elements, import
@editora/ui-react/client (or the package root) from a client component; a page made of server components alone never
loads the definitions, so its <ui-*> tags are not upgraded.
In development React can log a hydration warning for client components: the elements add their own inline styles and attributes as soon as they are defined, before React hydrates. The page works. The dialog providers create their host element on the client, in an effect, and clean it up when they unmount.
Troubleshooting
- A component renders as an empty tag, or React warns that a tag is not registered. Import the wrapper from the
package root or from its own entry (
@editora/ui-react/Button); both register the element. - A callback does not fire as expected. Use the typed
detailthat wrapper callbacks receive, such asonPageChange={(d) => d.page}, instead of parsing the DOM event.
The rest of the family
| Package | |
| --- | --- |
| @editora/ui-core | The web components these wrap, for any framework. |
| @editora/ui-sortable | Sortable lists and boards, with a React wrapper. |
| @editora/toast | Toast notifications. |
| @editora/react | The Editora rich text editor for React. |
License
MIT © Ajay Kumar
