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

cancado-crm

v0.0.1

Published

CRM components for React — agenda, data tables, dashboards, pipelines and record pages, styled after iOS 26

Readme

cancado-crm

CRM components for React — agenda, data tables, dashboards, pipelines and record pages — styled after iOS 26.

Built to be vertical-neutral: nothing in the API names a domain, so the same components serve a dental clinic, a law firm, a physiotherapist or a shop. A "record" is a patient, a client, a matter or a customer; a calendar event is a consultation, a hearing, a fitting or a delivery slot.

pnpm add cancado-crm

Quick start

import { AppShell, DataTable, FilterBar, useDataTable, Tag } from 'cancado-crm';
import type { Column } from 'cancado-crm';

const columns: Column<Client>[] = [
  { id: 'name', header: 'Client', accessor: (r) => r.name, width: 'minmax(200px, 1.4fr)' },
  { id: 'city', header: 'City', accessor: (r) => r.city, hideBelow: 900 },
  {
    id: 'status',
    header: 'Status',
    accessor: (r) => r.status,
    cell: (r) => <Tag tone="success" dot>{r.status}</Tag>,
  },
];

function Clients({ clients }: { clients: Client[] }) {
  const table = useDataTable({ rows: clients, columns, pageSize: 25 });

  return (
    <DataTable
      rows={table.rows}
      columns={columns}
      getRowId={(r) => r.id}
      sort={table.query.sort}
      onSortChange={table.toggleSort}
      page={{ ...table.query.page, count: table.pageCount, total: table.total }}
      onPageChange={table.setPageIndex}
      toolbar={
        <FilterBar
          search={table.query.search}
          onSearchChange={table.setSearch}
          filters={[{ columnId: 'status', label: 'Status', options: table.optionsFor('status') }]}
          applied={table.query.filters}
          onFilterChange={table.setFilter}
          onClear={table.clearFilters}
        />
      }
    />
  );
}

Components

| Component | What it is | | --- | --- | | AppShell | Sidebar nav + top bar + content. Drawer under 900px, bottom tab bar under 640px. | | DataTable | Sortable, selectable, paginated list. Collapses to labelled cards on a phone. | | FilterBar | Search, dropdown filters, removable filter chips, page actions. | | Agenda | Month / week / day scheduling, with optional per-resource columns. | | StatCard · LineChart · BarChart · DonutChart · Sparkline | Dashboard tiles and charts. | | Pipeline | Drag-and-drop stage board — funnel, case phases, order flow. | | RecordDetail · RecordHeader · RecordTabs · Section · DefinitionList | Record pages. | | ActivityTimeline | The history thread: calls, notes, payments, status changes. | | StockCard · StockGrid · StockSummary · StockTake · MovementList · QuantityStepper | Stock control — see below. | | Dashboard · Widget · WidgetPicker | A dashboard the user arranges — see below. |

Plus the primitives: Button, IconButton, Card, Tag, Avatar, AvatarGroup, TextInput, TextArea, Select, SearchInput, Segmented, Stack, Toolbar, Heading, Text, EmptyState, Skeleton.

Architecture

Four layers, each importable on its own.

| Layer | Path | What it is | | --- | --- | --- | | core/ | cancado-crm/core | Pure TypeScript: table pipeline, chart geometry, agenda maths, pipeline moves, formatters. No React, no DOM, no CSS. | | hooks/ | cancado-crm/hooks | React bindings: useDataTable, useAgenda, useMediaQuery, useDebouncedValue, useDisclosure, useDismiss. | | primitives/ | cancado-crm/primitives | The atoms. | | components/ | cancado-crm/DataTable, /Agenda, … | The CRM surfaces. All controlled. |

Behaviour lives in core as pure functions, so a 50-column table's sorting and filtering is testable without rendering one:

import { runQuery, emptyQuery } from 'cancado-crm/core';

const result = runQuery(rows, columns, {
  ...emptyQuery(25),
  search: 'jose',                  // accent-insensitive: matches "José"
  sort: { columnId: 'amount', direction: 'desc' },
});

useDataTable is a thin binding over it. Pass manual to take over search/sort/paging server-side while keeping the same toolbar and header.

Configurable dashboard

Dashboard lets the user arrange their own view: drag tiles to reorder, resize them, and choose which widgets appear at all.

import { Dashboard } from 'cancado-crm';
import type { WidgetDefinition, DashboardLayout } from 'cancado-crm';

const widgets: WidgetDefinition[] = [
  {
    id: 'revenue',
    title: 'Revenue',
    description: 'Monthly revenue and how it is trending.',
    icon: '₿',
    category: 'Metrics',
    defaultSize: 'sm',
    render: ({ detail }) =>
      detail === 'rich' ? <LineChart data={months} /> : <StatCard … />,
  },
];

<Dashboard
  widgets={widgets}
  layout={saved}                  // from your backend or localStorage
  onLayoutChange={persist}        // serialize and store it
/>

Resizing changes the detail level, not just the footprint. Each widget's render receives { size, detail } and decides what to show:

| Size | Grid | Detail | A KPI widget shows | | --- | --- | --- | --- | | sm | 1×1 | compact | the number and its trend | | md | 2×1 | normal | …plus a sparkline | | lg | 2×2 | rich | …plus the full chart with axes | | xl | 4×2 | rich | the same, full width |

Declare sizes: ['md', 'lg'] to restrict what a widget allows — the − / + controls clamp to it.

Layouts survive deploys. reconcileLayout runs on every change: widgets the user arranged keep their place and size, types you removed from the catalogue are dropped, and a size a widget no longer supports snaps to one it does. Nothing is auto-added — a dashboard growing new tiles by itself after a deploy is worse than one missing an option the user can add.

Drive it from your own chrome with useDashboard() and the controller prop, or set hideToolbar and supply your own arrange button.

Stock control

Inventory for any vertical — dental materials, legal supplies, retail SKUs, restaurant stock. Counts are plain numbers in whatever unit the item declares, so discrete boxes and measured millilitres share one model.

import { useStock, StockSummary, StockGrid, StockCard } from 'cancado-crm';

const stock = useStock({ items, movements });   // movements optional

<StockSummary items={stock.items} currency="BRL" />
<StockGrid
  items={stock.items}
  renderItem={(item) => (
    <StockCard
      item={item}
      editable
      onQuantityChange={(q) => stock.setQuantity(item.id, q)}
      onReorder={placeOrder}
    />
  )}
/>

Two ways to track the current count. Pass only items and their quantity is the truth — the simple case most apps start with. Pass movements too and counts derive from the ledger, which is what you want once someone asks where a box of gloves went. Same components either way.

Six states, ordered by what to act on first: out → expired → low → expiring → over → ok. You cannot use what you do not have, then you must discard what has expired, then reorder what is running low.

The level bar marks the reorder point on the same scale as the fill. A bare percentage tells you nothing actionable; a bar you can see has dropped below its threshold does.

StockTake freezes expected counts when the session starts, so a movement someone else records mid-count can't silently change what you're counting against. Finishing emits adjustment movements for the variances only — counting an item and finding it correct shouldn't pollute the ledger.

stockColumns() is a DataTable preset rather than a bespoke table, so sorting, filtering, selection and the mobile card layout all come from the component you already have.

Pure helpers live in cancado-crm/core: stockStatus, summarize, reorderList, quantityFromMovements, quantityAt, countVariances, movementsFromCount.

Liquid Glass

Controls follow iOS 26's material rather than the flat-filled convention: they are translucent capsules that float above content, not solid blocks of colour. A primary action signals itself through a tinted glass fill and an accent-coloured label — not by being a saturated rectangle.

Every control shares one material, built from four parts:

| Part | Token | Role | | --- | --- | --- | | Fill | --glass-fill | Low-opacity background; content shows through | | Edge | --glass-edge | Bright hairline border | | Specular | --glass-specular | Highlight along the top edge — what makes it read as glass rather than flat translucency | | Lift | --glass-shadow | Separation from the surface below |

Button variants differ only in how much accent bleeds through:

<Button variant="primary">Book appointment</Button>   {/* accent-tinted glass */}
<Button variant="secondary">Message</Button>          {/* neutral glass */}
<Button variant="ghost">Today</Button>                {/* capsule forms on hover */}
<Button variant="outline">Export</Button>             {/* defined edge, for busy backgrounds */}
<Button variant="danger">Delete</Button>              {/* red-tinted glass */}
<Button variant="solid">Confirm</Button>              {/* the deliberate exception: opaque fill */}

Two details worth knowing:

  • Label contrast is derived, not hand-set. The raw accent on light glass sits near 3.5:1, under the text minimum, so --color-accent-label darkens it with color-mix. Swap --color-accent to any hue and the label stays legible.
  • IconButton has no blur at rest. A table can hold dozens of them, and dozens of live blur layers is a real scroll cost; the capsule forms on interaction, which is when the affordance matters. Pass floating to keep it visible over imagery or a chart.

Both fall back cleanly: @supports rules swap in an opaque surface where backdrop-filter or color-mix is unsupported.

Theming

The whole kit is CSS custom properties (src/styles/tokens.css), following the iOS 26 system palette.

Light and dark work with no setup — light is the default and the OS preference picks dark. Set data-theme="light" | "dark" on <html> to pin it:

<html data-theme="dark" data-accent="indigo">

Accent — six built-ins (blue, indigo, purple, pink, orange, green) via data-accent, or override directly:

:root {
  --color-accent: #b8336a;
  --radius-lg: 20px;
}

Status and chart colours are separate token families, so re-branding the accent doesn't disturb "overdue" red or a chart's categorical series.

Responsive

CRMs get used at a desk and on a phone between appointments, so every layout has a compact form:

  • AppShell → under 900px the sidebar and its toggle disappear entirely and navigation becomes a native-style bottom tab bar: the first four destinations plus a More button opening a glass sheet with the rest. There is no hamburger and no side drawer on a phone. Mark items primary to choose the four yourself; otherwise it takes the first four in order.
  • DataTable → rows become labelled cards; hideBelow drops columns by width
  • Agenda → week view narrows from 7 columns to 3
  • DashboardGrid → auto-fit tiles, no breakpoints needed

Styles

Component subpaths inject their own CSS automatically. For everything at once:

import 'cancado-crm/styles';   // all components
import 'cancado-crm/tokens';   // just the variables

Development

pnpm install
pnpm storybook     # full CRM demo app + component stories
pnpm typecheck
pnpm build

License

MIT