npm install @bloomneo/uikit

Three steps to a working UI

// 1. Import the core stylesheet ONCE at your app entry (e.g. main.tsx)
import '@bloomneo/uikit/styles';

// Optional: import theme fonts (Elegant, Metro, Studio, Vivid)
import '@bloomneo/uikit/styles/fonts';

// 2. Wrap your app with providers
import { ThemeProvider, ToastProvider, ConfirmProvider } from '@bloomneo/uikit';

<ThemeProvider theme="base" mode="light">
  <ToastProvider position="bottom-right" />
  <ConfirmProvider>
    <App />
  </ConfirmProvider>
</ThemeProvider>

// 3. Add FOUC prevention in index.html <head> — prevents theme flash before React mounts
import { foucScript } from '@bloomneo/uikit/fouc';
<script dangerouslySetInnerHTML={{ __html: foucScript() }} />

There is exactly ONE supported import path for normal use. Always use the flat import:

import {
  Button, Input, Card, CardHeader, CardTitle, CardContent,
  Alert, AlertDescription, Badge, DataTable, Dialog,
  DialogContent, DialogHeader, DialogTitle, DialogFooter,
  Select, Combobox, DatePicker, Skeleton, EmptyState,
  PageHeader, PermissionGate, PermissionProvider,
  FormField, PasswordInput, toast, useToast, useConfirm,
  useTheme, useApi, usePagination, useBreakpoint,
  formatBytes, formatCurrency, formatDate, timeAgo,
} from '@bloomneo/uikit';

// Deep imports like '@bloomneo/uikit/button' exist for tree-shaking
// but are NOT the canonical form. Don't mix styles in one file.

Perceptually uniform color science

OKLCH color space ensures colors look consistent across devices and scale predictably — no muddy mid-tones or clipped highs. Each theme ships in light and dark mode.

base

Clean neutral foundation. Good default for any product.

elegant

Refined, subdued palette. Professional and polished.

metro

High contrast, bold. Inspired by transit design systems.

studio

Creative, warm. For design tools and media apps.

vivid

Saturated, energetic. For consumer and marketing apps.

const { theme, mode, availableThemes, setTheme, toggleMode } = useTheme();

setTheme('elegant');   // switch theme at runtime
toggleMode();          // light ↔ dark

// All available themes:
// 'base' | 'elegant' | 'metro' | 'studio' | 'vivid' | (your custom theme)
import { Button } from '@bloomneo/uikit';

// Variants
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>

// Sizes
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>

// States
<Button disabled>Disabled</Button>
<Button loading>Saving...</Button>
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button } from '@bloomneo/uikit';

<Card>
  <CardHeader>
    <CardTitle>User Settings</CardTitle>
    <CardDescription>Manage your account preferences</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Content goes here</p>
  </CardContent>
  <CardFooter>
    <Button>Save changes</Button>
  </CardFooter>
</Card>

Sortable, filterable, paginated table with row actions. Define columns once and let the table handle the rest.

import { DataTable, type DataTableColumn, type RowAction } from '@bloomneo/uikit';
import { Pencil, Trash2 } from 'lucide-react';

type User = { id: string; name: string; email: string; role: 'admin' | 'user'; createdAt: string; };

const columns: DataTableColumn<User>[] = [
  { id: 'name',      header: 'Name',  accessorKey: 'name',      sortable: true },
  { id: 'email',     header: 'Email', accessorKey: 'email' },
  { id: 'role',      header: 'Role',  accessorKey: 'role',      sortable: true },
  { id: 'createdAt', header: 'Joined', accessorKey: 'createdAt', sortable: true, dataType: 'date' },
];

const actions: RowAction<User>[] = [
  { id: 'edit',   label: 'Edit',   icon: <Pencil size={14} />, onClick: (row) => handleEdit(row) },
  { id: 'delete', label: 'Delete', icon: <Trash2 size={14} />, onClick: (row) => handleDelete(row), destructive: true },
];

<DataTable<User>
  data={users}
  columns={columns}
  rowActions={actions}
  searchable
  pagination
  pageSize={10}
  getRowId={(row) => row.id}
/>
import { FormField, Input, PasswordInput, Select, Combobox, Button } from '@bloomneo/uikit';

// FormField wraps any input with label, error, and helper text
<FormField label="Email" required error={emailError} helper="We'll never share this">
  <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</FormField>

<FormField label="Password" required>
  <PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} />
</FormField>

// Searchable select with async loading
<Combobox
  value={country}
  onChange={setCountry}
  options={countries}
  placeholder="Select a country"
  searchPlaceholder="Search countries..."
  clearable
/>

Mount <ToastProvider /> once at the root. Call toast.* from anywhere — no hooks, no context drilling.

import { toast } from '@bloomneo/uikit';

// Simple toasts
toast.success('Changes saved');
toast.error('Something went wrong');
toast.info('Heads up — deployment is running');
toast.warning('Your session expires in 5 minutes');

// With description and action
toast('Saved', {
  description: 'Your changes are now live',
  action: {
    label: 'Undo',
    onClick: () => toast.info('Undone'),
  },
});

useConfirm() returns a promise — true if confirmed, false if cancelled. No boilerplate modal state needed.

import { useConfirm } from '@bloomneo/uikit';

function DeleteButton() {
  const confirm = useConfirm();

  async function handleDelete() {
    const ok = await confirm({
      title: 'Delete this record?',
      description: 'This cannot be undone.',
      confirmLabel: 'Delete',
      tone: 'destructive',
    });
    if (!ok) return;
    await deleteRecord();
    toast.success('Deleted');
  }

  // High-stakes: user must type a phrase before confirm enables
  async function handleHardDelete() {
    const ok = await confirm.destructive({
      title: 'Delete account',
      description: 'This will permanently delete the account and all its data.',
      verifyText: 'delete my account',
    });
    if (ok) await nukeAccount();
  }

  return <Button variant="destructive" onClick={handleDelete}>Delete</Button>;
}
import { useState } from 'react';
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@bloomneo/uikit';

export function EditProfileDialog() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>Edit profile</Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Edit profile</DialogTitle>
            <DialogDescription>Make changes and save when you're done.</DialogDescription>
          </DialogHeader>
          <p className="text-sm text-muted-foreground">Form fields go here.</p>
          <DialogFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
            <Button onClick={() => setOpen(false)}>Save changes</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}
import { Users } from 'lucide-react';
import { Button, PageHeader } from '@bloomneo/uikit';

<PageHeader
  icon={<Users />}
  title="User management"
  description="View and manage all users in your workspace"
  breadcrumbs={[
    { label: 'Admin', href: '/admin' },
    { label: 'Users' },
  ]}
  actions={<Button>Add user</Button>}
/>
import { Inbox } from 'lucide-react';
import { Button, EmptyState } from '@bloomneo/uikit';

<EmptyState
  icon={<Inbox />}
  title="No invoices yet"
  description="Create your first invoice to get started."
  action={<Button onClick={handleCreate}>Create invoice</Button>}
/>
import { PermissionGate, PermissionProvider, Button } from '@bloomneo/uikit';

// Bring your own auth source — just provide a check function
const check = (perm: string) => currentUser.roles.includes(perm);

<PermissionProvider check={check}>
  {/* Single permission */}
  <PermissionGate when="admin">
    <Button variant="destructive">Delete user (admin only)</Button>
  </PermissionGate>

  {/* OR logic across multiple roles */}
  <PermissionGate when={['admin', 'moderator']} fallback={<span>Restricted</span>}>
    <Button>Moderate content</Button>
  </PermissionGate>

  {/* Custom predicate */}
  <PermissionGate when={() => currentUser.plan === 'pro'}>
    <ProFeature />
  </PermissionGate>
</PermissionProvider>
import { formatBytes, formatCurrency, formatDate, timeAgo, Time } from '@bloomneo/uikit';

formatCurrency(1234.56, { currency: 'USD' })             // '$1,234.56'
formatCurrency(1234.56, { currency: 'INR', locale: 'en-IN' }) // '₹1,234.56'
formatDate(new Date(), { preset: 'long' })               // 'April 12, 2026'
timeAgo(new Date(Date.now() - 10 * 60 * 1000))           // '10 minutes ago'
formatBytes(1_572_864)                                    // '1.5 MB'

// Auto-updating relative time component
<Time date={publishedAt} />

Data fetching, theme, breakpoints, pagination

import { useApi, useTheme, usePagination, useBreakpoint, useActiveBreakpoint, useMediaQuery } from '@bloomneo/uikit';

// API hook — managed loading/data/error state
const api = useApi({ baseURL: 'http://localhost:3000' });
await api.get('/api/users');
// api.data, api.loading, api.error available reactively

// Theme hook
const { theme, mode, setTheme, toggleMode, availableThemes } = useTheme();

// Pagination hook — works with any data source
const pagination = usePagination({ total: 234, pageSize: 10 });
const visible = allItems.slice(pagination.startIndex, pagination.endIndex);
// pagination.page, pagination.pageCount, pagination.hasPrev, pagination.hasNext
// pagination.prev(), pagination.next(), pagination.goTo(n), pagination.pages

// Responsive breakpoints
const isAtLeastMd = useBreakpoint('md');          // true when ≥ 768px
const isMobile    = useBreakpoint('md', 'down');  // true when < 768px
const active      = useActiveBreakpoint();         // 'sm' | 'md' | 'lg' | 'xl' | '2xl'
const reduced     = useMediaQuery('(prefers-reduced-motion: reduce)');

A complete user list with search, sort, row actions, and delete-with-confirm — in ~80 lines:

import { useState } from 'react';
import { Pencil, Trash2, Users } from 'lucide-react';
import {
  Button, ConfirmProvider, DataTable, PageHeader,
  ToastProvider, toast, useConfirm,
  type DataTableColumn, type RowAction,
} from '@bloomneo/uikit';

type User = { id: string; name: string; email: string; role: 'admin' | 'user' };

function UserListPage() {
  const [users, setUsers] = useState<User[]>(initialUsers);
  const confirm = useConfirm();

  const columns: DataTableColumn<User>[] = [
    { id: 'name',  header: 'Name',  accessorKey: 'name',  sortable: true },
    { id: 'email', header: 'Email', accessorKey: 'email' },
    { id: 'role',  header: 'Role',  accessorKey: 'role',  sortable: true },
  ];

  const actions: RowAction<User>[] = [
    { id: 'edit',   label: 'Edit',   icon: <Pencil size={14} />, onClick: handleEdit },
    {
      id: 'delete', label: 'Delete', icon: <Trash2 size={14} />, destructive: true,
      onClick: async (user) => {
        const ok = await confirm({ title: `Delete ${user.name}?`, tone: 'destructive' });
        if (!ok) return;
        setUsers(prev => prev.filter(u => u.id !== user.id));
        toast.success(`${user.name} deleted`);
      },
    },
  ];

  return (
    <>
      <PageHeader icon={<Users />} title="Users" actions={<Button>Add user</Button>} />
      <DataTable data={users} columns={columns} rowActions={actions} searchable pagination getRowId={r => r.id} />
    </>
  );
}

export default function App() {
  return (
    <ConfirmProvider>
      <ToastProvider position="bottom-right" />
      <UserListPage />
    </ConfirmProvider>
  );
}

One component library, four targets

Web

Vite + React. Full component library with all themes. SSR-compatible.

Desktop

Electron renderer process. Same components, same themes, same API.

Mobile

Capacitor + React. Touch-optimised with MobileLayout, TabBar, SafeArea.

Extensions

Browser extension popup. Constrained layout variants for small viewports.

llms.txt

One canonical snippet per component. No hallucinated prop shapes. View →

AGENTS.md

Always-do, never-do rules: one import path, required providers, canonical patterns. View →

Cookbook patterns

CRUD page, dashboard, login flow, settings page, delete flow — full working examples in llms.txt.