@stcn52/cloud-ui
v1.9.0
Published
NextCli design system — React + TS component library with Tailwind v4.
Downloads
126
Maintainers
Readme
@stcn52/cloud-ui
React + TypeScript component library built on Tailwind v4. 40+ components across six categories: primitives, data display, navigation, overlays, advanced controls, plus foundations. Calm, dense, operator-first visual language — cool neutrals with a single azure accent.
- 📚 Docs: https://stcn52.github.io/cloud-ui/
- 🎨 Storybook: https://stcn52.github.io/cloud-ui/storybook/
- 📦 npm: https://www.npmjs.com/package/@stcn52/cloud-ui
Install
pnpm add @stcn52/cloud-ui
# or
npm install @stcn52/cloud-uiPeer deps: react ^18, react-dom ^18.
Quick start
import '@stcn52/cloud-ui/styles.css'
import { Button, Input, Field } from '@stcn52/cloud-ui'
export function App() {
return (
<div>
<Field label="Service name">
<Input placeholder="api-gateway" clearable />
</Field>
<Field label="Password">
<Input type="password" revealable clearable autoComplete="current-password" />
</Field>
<Button intent="primary">Deploy</Button>
</div>
)
}Tip:
clearableadds a × button for any text input;revealableadds an eye-toggle fortype="password". The two compose — when both are on and the field has content, the buttons stack[clear][reveal]. Aria-labels followConfigProvider.locale.input(showPassword/hidePassword/clear).
ConfigProvider — theme, size, locale
Wrap your app (or a subtree) to control theme, density, and i18n:
import { ConfigProvider, zhCN } from '@stcn52/cloud-ui'
<ConfigProvider theme="dark" size="compact" locale={zhCN}>
<App />
</ConfigProvider>| Prop | Values | Default |
| --------- | ------------------------------------- | ------- |
| theme | 'light' | 'dark' | light |
| size | 'compact' | 'normal' | 'comfortable' | normal |
| locale | Locale object (bundled: en, zhCN) | en |
| target | 'wrapper' | 'body' | wrapper |
The provider writes data-theme and data-size to its wrapping element (or
document.body if target='body'). All components respond automatically via
CSS variables.
Custom locale
The bundled locales cover UI strings used by Pagination, CommandPalette,
CopyField, Banner, Toast, Pill, TagInput, DatePicker, the
imperative confirm() default button labels (locale.dialog.confirm /
cancel), and the Input clear / password-reveal toggles (locale.input).
You can supply your own:
import type { Locale } from '@stcn52/cloud-ui'
const ja: Locale = {
code: 'ja',
pagination: { prev: '‹ 前へ', next: '次へ ›' },
commandPalette: { placeholder: '検索…', empty: '該当なし', escape: 'esc' },
// …
}Available components
01 · Foundations — Color, Typography, Spacing, Radius, Elevation (docs only, no runtime components).
02 · Primitives — Button, ButtonGroup, Input, InputGroup, Affix,
Field, Checkbox, CheckRow, Radio, RadioRow, Switch, Select,
Textarea, Pill, Dot, Badge, Avatar, AvatarStack, Kbd, Form,
FormField, useForm, zodResolver, FormDialog, StepForm, FormSteps.
03 · Data display — Card, CardHead, CardFoot, Kpi, Delta,
Table, DataTable, Progress, Ring, Skeleton, Pipeline, PipeStep,
LogLine, Donut, Gauge.
04 · Navigation — Tabs, Tab, Segmented, Breadcrumbs, Breadcrumb,
Pagination, PageHeader.
07 · Layout — AppShell.
05 · Overlays — Banner, Tooltip, Toast, ToastStack, Empty,
Dialog, DialogHead, DialogBody, DialogFoot, DialogHost, confirm,
dialog, useConfirm, useDialog, Drawer, DrawerHead, DrawerBody,
DrawerFoot, Popover, PopoverItem, PopoverSeparator, CommandPalette.
06 · Advanced — Dropdown, DropdownItem, DropdownGroup,
DropdownSeparator, DropdownSearch, Tree, Cascader, DatePicker,
TagInput, CopyField, Accordion, AccordionItem, PromptInput,
JsonViewer, NotificationCenter, QueryBuilder, MentionPopover,
Transfer, CodeEditor, DiffEditor (Monaco — optional peerDeps),
Terminal (xterm.js — optional peerDeps), FilePanel.
11 · Tables — NxTable (enterprise data table: resize/pin columns, sort,
per-column filters, search, pagination, selection + bulk actions, expandable
rows, density, column toggle, CSV export, optional localStorage persistence).
CodeEditor (Monaco)
<CodeEditor> and <DiffEditor> wrap Monaco with lazy-loading and
theme-following defaults. Because Monaco is multi-MB, monaco-editor and
@monaco-editor/react are optional peerDependencies — install them in
the consuming app only when you need the editor:
pnpm add monaco-editor @monaco-editor/reactimport { CodeEditor, DiffEditor } from '@stcn52/cloud-ui'
<CodeEditor language="yaml" value={yaml} onChange={setYaml} height={280} />
<DiffEditor language="yaml" original={old} modified={next} inline={false} />The editor's theme syncs to ConfigProvider.theme (light → 'vs', dark →
'vs-dark'); pass theme="..." to opt out. options is an escape hatch onto
Monaco's full IStandaloneEditorConstructionOptions for anything not covered
by the surface props.
Forms (useForm)
Headless form state + validation, designed for one-line spread onto cloud-ui controls:
import { useForm, Form, Input, Field, Select, Switch, Button } from '@stcn52/cloud-ui'
const form = useForm({
defaultValues: { name: '', region: 'us-east-1', notify: true },
rules: { name: [required(), minLength(2)] },
onSubmit: async (values) => api.save(values),
})
<Form onSubmit={form.submit}>
<Field label="Name" error={form.touched.name ? form.errors.name : undefined}>
<Input {...form.register('name')} />
</Field>
<Field label="Region">
<Select options={regions} {...form.register('region')} />
</Field>
<Field label="Notify">
<Switch {...form.register('notify')} />
</Field>
<Button type="submit" loading={form.isSubmitting} disabled={!form.isDirty}>Save</Button>
</Form>register(name) auto-shapes itself on the field's value type — boolean fields
get { checked, onChange }; everything else gets { value, onChange }. The
handler accepts both React ChangeEvents and value-passing controls.
Schema validation (zod / yup / valibot)
import { z } from 'zod'
import { useForm, zodResolver } from '@stcn52/cloud-ui'
const schema = z.object({
key: z.string().min(1).max(32),
email: z.string().email(),
})
const form = useForm({
defaultValues: { key: '', email: '' },
resolver: zodResolver(schema),
})zodResolver is duck-typed (ZodLikeSchema) so cloud-ui never imports zod
itself — install it in your own app. Pass any zod-shaped schema.
Form-in-dialog (FormDialog)
const renamed = await dialog.open<{ name: string }>(({ resolve, dismiss }) => {
const form = useForm({
defaultValues: { name: 'api-gateway' },
rules: { name: [required(), minLength(2)] },
onSubmit: resolve,
})
return (
<FormDialog form={form} title="Rename service" onCancel={dismiss}>
<Field label="New name" error={form.touched.name ? form.errors.name : undefined}>
<Input {...form.register('name')} />
</Field>
</FormDialog>
)
})The Save button calls form.handleSubmit(), so validation has to pass before
onSubmit (and therefore the outer resolve) fires.
Terminal (xterm.js)
<Terminal> and useTerminal() lazy-load @xterm/xterm + @xterm/addon-fit
(both optional peerDependencies). Theme and font-size sync to
ConfigProvider; fit-addon auto-recomputes on container resize.
pnpm add @xterm/xterm @xterm/addon-fit// once in your app entry
import '@xterm/xterm/css/xterm.css'import { Terminal, webSocketTransport, useTerminal } from '@stcn52/cloud-ui'
// Drop-in component:
<Terminal
transport={webSocketTransport('wss://host/api/v1/terminal')}
height={320}
onConnect={() => console.log('connected')}
onClose={() => console.log('closed')}
/>
// Or compose your own toolbar with the hook:
function ShellPanel() {
const t = useTerminal({ url: 'wss://host/api/v1/terminal' })
return (
<div>
<button onClick={t.reconnect}>Reconnect</button>
<div ref={t.ref} style={{ height: 320 }} onClick={t.focus} />
</div>
)
}A transport is any object implementing { open(io): cleanup? }; the bundled
webSocketTransport handles connection lifecycle, binary/utf-8 decoding,
and resize messaging out of the box.
Imperative dialogs
Mount <DialogHost /> once near the app root (typically next to <Toaster />),
then call confirm() / dialog.open() from anywhere — both return Promises:
import { ConfigProvider, DialogHost, Toaster, confirm, dialog, toast } from '@stcn52/cloud-ui'
export function App() {
return (
<ConfigProvider>
<Routes />
<DialogHost />
<Toaster />
</ConfigProvider>
)
}
// from any handler:
const ok = await confirm({
title: 'Delete database?',
body: 'Drops 4 schemas, revokes 6 keys. Cannot be undone.',
danger: true,
confirmText: 'Delete forever',
onConfirm: () => api.delete(id), // spinner shows while pending
})
if (ok) toast.error('Deleted')
// custom-content dialog with a Promise<T> result:
const value = await dialog.open<string>(({ resolve, dismiss }) => (
<RenameDialog onSubmit={resolve} onCancel={dismiss} />
))useConfirm() / useDialog() return the same references — purely a
convenience for code that prefers a hook style.
Theming via CSS variables
Every token is a CSS variable — override in your own stylesheet:
:root {
--color-accent: oklch(0.65 0.16 140); /* switch to green */
--color-accent-weak: oklch(0.95 0.04 140);
--color-accent-ink: oklch(0.36 0.10 140);
}Full token list: run Storybook (pnpm dev) → 01 · Foundations/Overview.
Development
pnpm install
pnpm dev # Storybook on :6006
pnpm build # dist/{index.js, index.cjs, index.d.ts, styles.css}
pnpm build-storybook # static Storybook in storybook-static/
pnpm typecheck # tsc -b && stories typecheckLicense
MIT
