npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@startsimpli/ui

v0.4.133

Published

Shared UI components package for StartSimpli applications

Readme

@startsimpli/ui

Shared React UI primitives for every StartSimpli Next.js app. Buttons, dialogs, the production UnifiedTable, a Tailwind preset, a CSS-variable design-token contract, and all the domain compositions (compose flows, kanban, gantt, email editor, command palette, dashboard widgets) that are reused across raise-simpli, market-simpli, trade-simpli, vault-web, and friends.

  • Package: @startsimpli/ui · v0.4.13
  • Source: ~240 .ts/.tsx files across 30 barrel files
  • Tests: 58 test files (~1600 test/it cases) under src/components/**/__tests__/
  • Peer deps: react 18/19, react-dom 18/19, next 14/15/16, optional @tanstack/react-query ≥5
  • Status: the biggest shared package; the inventory below is exhaustive (read from the barrel files, not aspirational)

Shared-first policy (monorepo CLAUDE.md rule 9): any reusable button, dialog, table, hook, util, layout, or domain widget belongs here, not in an app's src/. Wrapping @startsimpli/ui in an app-local provider just to add a field is also forbidden — extend the shared component.


Install / wire-up

Every app must do four things to consume the UI package end-to-end:

// app/layout.tsx
import { AuthProvider } from '@startsimpli/auth';
import { QueryProvider, Toaster } from '@startsimpli/ui';
import '@startsimpli/ui/theme/contract'; // or copy contract.css into your theme dir
import './globals.css';

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body suppressHydrationWarning>
        <AuthProvider config={{ apiBaseUrl: process.env.NEXT_PUBLIC_API_URL ?? '' }}>
          <QueryProvider>
            {children}
            <Toaster />
          </QueryProvider>
        </AuthProvider>
      </body>
    </html>
  );
}
  1. Mount <QueryProvider> — shared QueryClient with sane defaults (staleTime: 5min, refetchOnWindowFocus: false). One per app, at the root.
  2. Mount <Toaster> — driver for useToast / notify. Mount once, near the root. Fixed bottom-right, accessible region with live-region semantics.
  3. Define the theme tokens — copy theme/contract.css (the public contract, exported as @startsimpli/ui/theme/contract) into your app's globals or import it directly. See "Theme tokens" below.
  4. Wire Tailwind content paths — described next. Skipping this is the cause of the "classes silently tree-shaken" bug.

Tailwind: content paths are MANDATORY

Apps' tailwind.config.ts MUST list this package's src/ (and every other @startsimpli/* package whose JSX renders Tailwind classes) under content. Otherwise Tailwind walks only the app source and tree-shakes every class that only exists inside the package — the rendered HTML has className="bg-blue-600 text-white" but no matching CSS rule, so featured CTAs render grey-on-grey and buttons collapse.

This was the root cause of startsim-ja6 ("Pro CTA grey-on-grey" in vault-web) and is now codified in vault-web/CLAUDE.md rule 3. Reference config:

// vault-web/tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
    "../packages/ui/src/**/*.{js,ts,jsx,tsx}",
    "../packages/billing/src/**/*.{js,ts,jsx,tsx}",
    "../packages/auth/src/**/*.{js,ts,jsx,tsx}",
    "../packages/forms/src/**/*.{js,ts,jsx,tsx}",
    "../packages/hooks/src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: { /* ... */ },
};

export default config;

You also need this package as a presets entry or to merge its theme by hand:

// (option A) preset
module.exports = { presets: [require('@startsimpli/ui/tailwind')], content: [/* ... */] }

…and add '@startsimpli/ui' to transpilePackages in next.config.{ts,js} so Next compiles its JSX with the app.


Theme tokens (CSS-variable contract)

The design system is HSL-channel CSS variables. Changing one variable reshapes every component — radii, shadows, brand colors, typography stacks. The single contract lives in theme/contract.css and is mirrored in src/theme/contract.ts as a typed list (THEME_TOKENS).

:root {
  --primary: 221.2 83.2% 53.3%;
  --radius: 0.5rem;          /* set 0 = sharp editorial, 1.5rem = pill */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --font-heading: var(--font-sans);
  --border-width: 1px;
  /* ...19 colors · 6 radii · 4 shadows · 3 type · 4 motion · 1 border-width */
}

The Tailwind preset (src/theme/tailwind.preset.ts) consumes those variables — bg-primary resolves to hsl(var(--primary)), rounded-md resolves to var(--radius-md), etc. To swap the look of an entire app, override the variables in your globals; no Tailwind config change needed.

import { THEME_TOKENS, type ThemeColorTokens } from '@startsimpli/ui';
// THEME_TOKENS.colors → ['--background', '--foreground', '--primary', ...]

Cross-link: startsim-sw1.8 tracks single-sourcing these tokens between this web contract.css and @startsimpli/ui-native/tokens.ts (currently mirrored by hand — the goal is one source feeding both). When that lands, the canonical token file moves up a level; this README's contract path will be updated accordingly.


Public surface

5 root exports plus a handful of subpath exports. The root re-exports almost everything by domain; subpaths exist for the heaviest features so apps can tree-shake.

Subpath exports

| Subpath | Purpose | |---|---| | @startsimpli/ui | Everything except email-editor blocks; primary surface | | @startsimpli/ui/table | UnifiedTable only (lighter import) | | @startsimpli/ui/gantt | Gantt chart family (GanttChart, views, hooks, types) | | @startsimpli/ui/gantt/styles | Required CSS for the gantt views | | @startsimpli/ui/email-editor | Block-based email editor (heavy; opt-in) | | @startsimpli/ui/components | Same as . minus theme/utils — historical alias | | @startsimpli/ui/utils | cn, formatDate, formatCurrency, getInitials, ... | | @startsimpli/ui/theme | tailwindPreset, tailwindConfig, THEME_TOKENS | | @startsimpli/ui/theme/contract | The CSS variables file (import in globals.css) | | @startsimpli/ui/tailwind | CommonJS preset (for legacy JS configs) |

Inventory by domain

Drawn directly from the barrel files in src/components/**/index.ts. If something isn't listed here, it isn't exported.

shadcn/ui primitives (src/components/ui/) — every Radix-backed primitive the rest of the package builds on:

Accordion · Alert · Badge · Button · Calendar · Card · Checkbox · Collapsible · Dialog · DropdownMenu · Input · Label · Popover · Progress · ScrollArea · Select · Separator · Skeleton · Table · Tabs · Textarea · Tooltip · FeatureGate · ApiErrorBoundary · PageLoader · QueryProvider

UnifiedTable (src/components/unified-table/) — the production data table.

  • UnifiedTable, StandardTableToolbar, MobileView, MobileCard, MobileCardActions, MOBILE_BREAKPOINT
  • Hooks: useTableState, useSelection, usePagination, useFilters, useResponsive, useColumnVisibility, useColumnReorder, useColumnResize, useTablePreferences, useTableKeyboard, useTableURL
  • All the column / sort / filter / bulk-action / saved-view types are exported from ./types
  • See "UnifiedTable" section below for the full API

Dialogs (src/components/dialog/):

  • BaseDialog (compound API: BaseDialog.Header, .Title, .Body, .Footer), useDialogContext, ConfirmDialog

Toast / Notify (src/components/toast/):

  • Toaster (mount once at root)
  • useToast(), toast(), clearAllToasts() — low-level imperative API (shadcn-style)
  • useNotify(), notify() — opinionated wrapper (success / error / warning / info) used across the apps

State components (src/components/states/):

  • EmptyState, ErrorState — standardized empty/error UI with optional action

Loading (src/components/loading/):

  • TableSkeleton, DashboardSkeleton

Badges (src/components/badge/):

  • StatusBadge, StageBadge (config-driven, brand-aware)

Wizard (src/components/wizard/):

  • StepIndicator + StepConfig

Account forms (src/components/account/):

  • ProfileForm, ChangePasswordForm — used by raise-simpli's settings

Compose flow (src/components/compose/) — shared email/draft composer:

  • useAutoSave, SaveStatusIndicator, ComposeHeader, SubjectInput, SendConfirmationDialog, ComposeLoading

Email editor (src/components/email-editor/) — block-based builder with drag/drop, undo/redo, HTML renderer:

  • EmailEditor, BlockRenderer
  • Block types: Block, TextBlock, MetricsBlock, DividerBlock, CTABlock, ImageBlock, SpacerBlock, SocialBlock, HeaderBlock, FooterBlock, Section, Row, ColumnLayout, GlobalStyles
  • Helpers: createBlock, createRow, createSection, serializeSections, deserializeSections, migrateFromLegacy, flattenToLegacy
  • Renderer: renderToEmailHtml, renderToPreviewHtml
  • Templates: createInvestorUpdateTemplate, createEmptyTemplate, THEME_PRESETS

Email dialogs (src/components/email-dialogs/):

  • ScheduleDialog, TestSendDialog, PreviewDialog, TemplatePicker
  • Merge fields: MergeFieldsMenu, MergeFieldPreview, replaceMergeFields, DEFAULT_MERGE_FIELDS, DEFAULT_CATEGORIES

Dashboard (src/components/dashboard/):

  • MetricCard, PeriodSelector, SparklineTrend, DashboardGrid, DashboardSection, PipelineFunnel, TopCampaigns

Enrichment (src/components/enrichment/):

  • QualityBadge, EnrichButton, EnrichmentProgress, ApolloEnrichButton

Integrations (src/components/integrations/):

  • IntegrationCard, ConnectionStatus

Command palette (src/components/command-palette/):

  • CommandPalette, CommandPaletteProvider, useCommandPalette, useCommandPaletteSearch, CommandGroup, CommandResultItem

Settings layout (src/components/settings/):

  • SettingsLayout, SettingsNav, SettingsCard

Kanban (src/components/kanban/):

  • KanbanBoard (drag-drop via @hello-pangea/dnd)

Lists (src/components/lists/):

  • ListCard, CreateListDialog

Pipeline (src/components/pipeline/):

  • StageTransitionModal

Calendar / meetings (src/components/calendar/):

  • CalendarView (wraps react-big-calendar), UpcomingMeetings, MeetingsList

Gantt (src/components/gantt/, also via @startsimpli/ui/gantt):

  • GanttChart, GanttTimelineView, GanttBoardView, GanttListView, GanttFilterBar, useGanttState
  • Pure helpers: calculateExpectedProgress, calculateHealthStatus, getHealthColor, parseDateRangeFromTitle, getHierarchyLevel

Activity (root src/components/):

  • ActivityTimeline, QuickLogButtons, LogActivityDialog

Navigation (src/components/navigation/sidebar.tsx):

  • Sidebar, SidebarLayout, types SidebarLink, SidebarSection

Safe HTML:

  • SafeHtml — dompurify-backed; raise-simpli/.eslintrc.json forbids dangerouslySetInnerHTML in favor of this.

Top headline components (by reach across the monorepo)

| Component | Used by | Why | |---|---|---| | UnifiedTable | market-simpli (CRM lists, campaigns, contacts), raise-simpli | Every server-paginated table | | QueryProvider | vault-web, market-simpli, trade-simpli, raise-simpli | Single shared QueryClient per app | | Toaster | vault-web, market-simpli, trade-simpli, raise-simpli | Notification region | | notify / useToast | All apps | Imperative success/error/warn | | Button | All apps | shadcn primitive, gradient variants | | Dialog + BaseDialog + ConfirmDialog | market-simpli, raise-simpli | Modal flows | | EmptyState / ErrorState | market-simpli, raise-simpli | Empty/list/page fallbacks | | Card (+ Header/Title/Content) | trade-simpli (dashboards), market-simpli | Container primitive | | Badge / StatusBadge | trade-simpli, market-simpli | Pipeline + status chips | | SettingsLayout / SettingsCard / SettingsNav | market-simpli | Whole settings shell | | ProfileForm + ChangePasswordForm | raise-simpli | Account settings | | MeetingsList | raise-simpli | Upcoming meetings widget | | DashboardGrid + MetricCard + PeriodSelector | trade-simpli, market-simpli | Dashboard composition | | IntegrationCard | market-simpli | Integrations grid | | GanttChart (via @startsimpli/ui/gantt) | raise-simpli | Roadmaps / timelines | | EmailEditor + email-dialogs (ScheduleDialog, TestSendDialog, TemplatePicker) | market-simpli (campaign composer), raise-simpli (investor updates) | Block-based email builder | | ApolloEnrichButton / EnrichmentProgress | raise-simpli (Apollo enrichment) | Enrichment lifecycle UI | | SafeHtml | raise-simpli (ESLint-enforced) | XSS-safe HTML rendering | | FeatureGate | raise-simpli | Plan-gated UI | | StepIndicator | market-simpli (onboarding wizard) | Multi-step UX |


UnifiedTable

Production-ready data table. Every table in every app uses server-side pagination, sorting, and search — this is non-negotiable (CLAUDE.md rule 8).

Features: pagination, sorting, search, column visibility / reordering / resizing, bulk + row actions, inline editing with validation, filter sections with chip / range / select / boolean inputs, CSV + Excel export, saved views, URL state persistence, mobile card view, keyboard navigation.

Basic example

import { UnifiedTable } from '@startsimpli/ui/table';
import type { ColumnConfig } from '@startsimpli/ui';

function UsersTable() {
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState('');
  const [sort, setSort] = useState({ sortBy: 'name', sortDirection: 'asc' as const });
  const { data, isLoading } = useQuery({
    queryKey: ['users', page, search, sort],
    queryFn: () => api.users.list({ page, search, ...sort }),
  });

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

  return (
    <UnifiedTable<User>
      tableId="users"
      data={data?.items ?? []}
      columns={columns}
      getRowId={(u) => u.id}
      loading={isLoading}
      pagination={{
        enabled: true,
        pageSize: 25,
        totalCount: data?.total ?? 0,
        currentPage: page,
        serverSide: true,        // REQUIRED
        onPageChange: setPage,
      }}
      sorting={{
        enabled: true,
        serverSide: true,        // REQUIRED
        value: sort,
        onChange: setSort,
      }}
      search={{
        enabled: true,
        placeholder: 'Search users…',
        value: search,
        onChange: setSearch,
      }}
    />
  );
}

Why server-side, always

// CORRECT
pagination={{ serverSide: true,  totalCount, currentPage: page, onPageChange }}
sorting   ={{ serverSide: true,  onChange: (s) => fetchData({ ...s }) }}

// WRONG — loads everything into memory, doesn't scale
pagination={{ serverSide: false }}
sorting   ={{ serverSide: false }}

Bulk actions

<UnifiedTable
  selection={{ enabled: true, selectedIds, onSelectionChange: setSelectedIds }}
  bulkActions={[
    {
      id: 'delete',
      label: 'Delete',
      icon: Trash2,
      variant: 'gradient-purple',
      onClick: async (ids) => deleteUsers(Array.from(ids)),
      confirmMessage: 'Delete {count} users?',
    },
  ]}
/>

Filters

<UnifiedTable
  filters={{
    enabled: true,
    position: 'top',
    collapsible: true,
    config: {
      sections: [{
        id: 'status',
        type: 'chips',
        label: 'Status',
        filters: [{ id: 'status', label: 'Status', type: 'chips', options: ['active', 'inactive'] }],
      }],
    },
    value: filters,
    onChange: setFilters, // forward to your API query
  }}
/>

Inline editing

<UnifiedTable
  columns={[{
    id: 'name', header: 'Name', accessorKey: 'name',
    editable: true, editType: 'text',
    validate: (v) => (!v ? 'Name is required' : null),
  }]}
  inlineEdit={{
    enabled: true,
    optimisticUpdate: true,
    onSave: async (rowId, columnId, value) => updateUser(rowId, { [columnId]: value }),
  }}
/>

Export, saved views, mobile

<UnifiedTable
  export={{ enabled: true, baseFilename: 'users', formats: ['csv', 'excel'], showProgress: true }}
  savedViews={{
    enabled: true,
    views: savedViews,
    currentViewId,
    onSaveView: async (v) => saveView(v),
    onLoadView: (id) => applyView(savedViews.find(v => v.id === id)!),
  }}
  mobileConfig={{
    titleKey: 'name',
    subtitleKey: 'email',
    primaryFields: ['name', 'email'],
    secondaryFields: ['status', 'createdAt'],
  }}
  urlPersistence={{ enabled: true, debounceMs: 300 }}
/>

Column config

interface ColumnConfig<TData> {
  id: string
  header: string | ((props: any) => ReactNode)
  accessorKey?: string                              // dot notation OK: 'user.name'
  accessorFn?: (row: TData) => any
  cell?: (row: TData) => ReactNode
  sortable?: boolean
  sortingFn?: (a: TData, b: TData) => number
  width?: string | number
  minWidth?: string | number
  maxWidth?: string | number
  mobilePrimary?: boolean
  mobileSecondary?: boolean
  hideOnMobile?: boolean
  hideable?: boolean
  editable?: boolean
  editType?: 'text' | 'number' | 'select' | 'date'
  editOptions?: string[]
  validate?: (value: any, row: TData) => string | null
}

The full prop reference (pagination, sorting, search, selection, bulkActions, rowActions, filters, columnVisibility, columnReorder, columnResize, inlineEdit, export, savedViews, urlPersistence, mobileConfig, loading, loadingRows, emptyState, errorState, onRowClick) lives in src/components/unified-table/types.ts — that is the source of truth.


QueryProvider

A pre-configured QueryClientProvider so every app shares the same defaults:

// src/components/ui/query-provider.tsx
new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,     // 5 minutes — avoids hot refetch storms
      refetchOnWindowFocus: false,  // explicit control over refetches
    },
  },
});

Mount it once at the root (see "Install / wire-up"). Apps that need different defaults should override per-query, not re-instantiate.

@tanstack/react-query is an optional peer dep — the package compiles without it, but QueryProvider requires it at runtime.


Toaster + notify

<Toaster /> is the renderer; mount once at the root. Variants: default, destructive, success, warning, info. The container is a polite live region (role="region", aria-live="polite"), fixed bottom-right, with slide-in animation via tailwindcss-animate.

Two APIs are exposed:

// Low-level (shadcn shape)
import { useToast, toast, clearAllToasts } from '@startsimpli/ui';
toast({ title: 'Saved', description: 'Draft saved', variant: 'success' });

// Opinionated wrapper (most apps use this)
import { notify } from '@startsimpli/ui';
notify.success('Saved');
notify.error('Could not save', { description: err.message });
notify.info('Heads up');
notify.warning('Almost out of credits');

Utils

import { cn, generateId, sleep, formatDate, formatRelativeTime, formatRelativeDate, formatCurrency, getInitials } from '@startsimpli/ui';
  • cn(...classes) — clsx + tailwind-merge (the standard merge)
  • formatDate(d) → "May 28, 2026"
  • formatRelativeTime(d) → "Today" | "Yesterday" | "3 days ago" | "May 5, 2026"
  • formatRelativeDate(d) → fine-grained "just now" | "5m ago" | "2h ago" | "3d ago"
  • formatCurrency(n) → "$1,000,000" (USD, no cents)
  • getInitials(name) → up to 2-char uppercase initials
  • generateId(prefix?), sleep(ms)

Real consumer examples

vault-web (vault-web/src/app/layout.tsx) — root wiring is the canonical example: <AuthProvider> → <QueryProvider> → <Toaster />. Vault's billing page composes SubscriptionManager from @startsimpli/billing, which itself sits on the design tokens defined here.

market-simpli — heaviest consumer (~64 import sites). Uses UnifiedTable extensively for CRM lists, SettingsLayout/SettingsNav/SettingsCard for the settings shell, IntegrationCard for the integrations grid, EmailEditor + email-dialogs for campaign compose, StepIndicator for onboarding, PeriodSelector/DashboardGrid for dashboards.

raise-simpli — settings pages use useToast; ProfileForm + ChangePasswordForm mounted under account settings; SafeHtml enforced via ESLint over dangerouslySetInnerHTML; FeatureGate for plan-gated UI; ApolloEnrichButton + EnrichmentProgress drive Apollo enrichment; GanttChart via @startsimpli/ui/gantt for roadmap views; MeetingsList for upcoming meetings; consumes the Tailwind preset directly (presets: [require('@startsimpli/ui/tailwind')]).

trade-simpli — dashboards: Card, Badge, StatusBadge, DashboardGrid, plus <QueryProvider> + <Toaster /> at the root.

recipesimpli and crochet-patterns are standalone Next apps with their own DB and have not yet adopted @startsimpli/ui.


Testing

pnpm --filter @startsimpli/ui test              # all tests
pnpm --filter @startsimpli/ui test:watch
pnpm --filter @startsimpli/ui test:coverage
pnpm --filter @startsimpli/ui type-check
  • 58 test files under src/components/**/__tests__/ (~1600 test/it cases), covering UnifiedTable (17 files), email editor (8 files), and every domain barrel (dashboard, dialogs, kanban, gantt, settings, compose, command-palette, navigation, lists, pipeline, enrichment, integrations, account, calendar, badge, loading, states, wizard, toast, safe-html).
  • Jest + @swc/jest + jest-environment-jsdom, with next/navigation and next/link mocked in src/__mocks__/next/.
  • Coverage thresholds enforced at 70% (branches / functions / lines / statements) via jest.config.js.

Shared-first reminder

Before adding any reusable button / dialog / hook / formatter / layout / domain widget to an app's src/, ask whether another app could plausibly want it. If yes — or if you're unsure — it goes here. See the monorepo CLAUDE.md rule 9 and .claude/docs/conventions.md#shared-packages-policy.

License

Proprietary — StartSimpli.