@cmgfi/clear-ds
v1.3.6
Published
CMG Financial — Clear Design System React component library
Maintainers
Readme
@cmgfi/clear-ds
Clear Design System — CMG Financial's official React component library.
The UI foundation for CMG's internal loan origination platform. Built from scratch for pixel-fidelity control, token ownership, and zero dead code. Every component was designed in Pencil before it was written in code.
- npm:
@cmgfi/clear-ds - Storybook: clear-ds.cmgfinancial.ai
- Version: 1.1.8
- License: UNLICENSED (CMG Financial internal)
Table of Contents
- Installation
- Consumer Setup
- Component Catalog
- Token System
- Tech Stack
- Project Structure
- Local Development
- Build Output
- Publishing
- Contributing
- Hard-won Rules
Installation
Full install — with PrimeIcons (recommended):
npm install @cmgfi/clear-ds primeiconsWithout PrimeIcons — if your composition does not use any icon-bearing components:
npm install @cmgfi/clear-dsPrimeIcons is a
peerDependency. It must be installed in the consumer app — it is not bundled into the library.
Consumer Setup
Add these three imports once, in your application root (e.g. main.tsx or App.tsx):
import '@cmgfi/clear-ds/tokens'; // CSS custom property definitions (:root)
import '@cmgfi/clear-ds/styles'; // compiled component styles
import 'primeicons/primeicons.css'; // omit if PrimeIcons not installedSet the base font size. The token system is built on 1rem = 12px:
/* global.css or index.css */
html {
font-size: 12px;
}Then use components anywhere in your app:
import { Button, InputText, Modal, DataTable } from '@cmgfi/clear-ds';Component Catalog
44 components across 7 sections. All are fully typed with JSDoc-annotated props, React.forwardRef-wrapped, and documented in Storybook.
Buttons
All button components share a canonical three-size ramp:
| Size | Height | Font | Padding |
|------|--------|------|---------|
| sm | 24px | 10px | 4px 12px |
| md | 36px | 12px | 8px 14px (default) |
| lg | 40px | 15px | 10px 16px |
Icon sizes are explicitly defined per button size using design tokens:
| Button size | Icon token | Value |
|-------------|------------|-------|
| sm | --icon-size-md | 14px |
| md | --icon-size-lg | 18px |
| lg | --icon-size-lg | 18px |
Pass any React node to icon props — PrimeIcons (<i className="pi pi-*" />) are the standard.
| Component | Props | Description |
|---|---|---|
| Button | variant, size, leadingIcon, trailingIcon, badge | Primary action button. Variants: primary (default), secondary, ghost, danger, link. Sizes: sm, md (default), lg. Optional leading/trailing PrimeIcon slots and a notification badge. |
| IconButton | icon, size, variant | Circular icon-only button. Sizes: sm (24×24px), md (36×36px, default), lg (40×40px). Tooltip showing aria-label rendered automatically on hover. |
| DropdownButton | label, items, size, variant, leadingIcon, disabled | Button that opens a dropdown action menu. Chevron uses pi pi-chevron-down. Sizes: sm, md (default), lg. |
| SplitButton | label, items, size, variant, leadingIcon, disabled | Two joined pill buttons: main action (left) + dropdown trigger (right). primary — both halves dark teal. secondary — both halves light teal. Sizes: sm, md (default), lg. Chevron trigger shows a tooltip with triggerAriaLabel on hover. |
| LightningButton | items, size, variant, disabled | Icon-only quick-action button with pi pi-bolt + pi pi-chevron-down. Variants: basic, filled. Sizes: sm, md (default), lg. Tooltip showing aria-label rendered automatically on hover. |
| CloseButton | size, disabled | Circular pi pi-times dismiss button. Default size is sm (24×24px). Sizes: sm (default), md (36×36px), lg (40×40px). Tooltip showing aria-label rendered automatically on hover. |
Form Controls
| Component | Description |
|---|---|
| InputText | Single-line text input with label, helper text, and validation states. |
| TextArea | Multi-line text input. Props: showLabel (boolean), counterPlacement (inside | outside). |
| RichTextEditor | Tiptap-based rich text editor. Every toolbar button (showBold, showItalic, showUnderline, showStrike, showCode, showHighlight, showSubscript, showSuperscript, showHeading, showBlockquote, showCodeBlock, showBulletList, showOrderedList, showTaskList, showHorizontalRule, showTextAlign, showLink, showImage, showMention, showUndo, showRedo) is an individual boolean prop. toolbarMode: always (default) | on-focus. mentionSuggestions accepts a static array or async function for @ mention data. |
| Checkbox | Controlled checkbox with label. Supports indeterminate state. |
| RadioButton | Single radio option. Compose multiples in a group. |
| Select | Single-select dropdown. |
| MultiSelect | Multi-select dropdown with chip display and search. |
| ListBox | Inline scrollable single or multi-select list. |
| SelectButton | Segmented button group for mutually exclusive options. Variants: group (joined toggles) and options (split button + dropdown). size prop (sm/md/lg). Each option accepts an optional icon?: React.ReactNode rendered before its label. |
| DatePicker | Date input with calendar popover. |
| ToggleSwitch | On/off toggle. |
| FileUpload | Drag-and-drop file upload with type validation. |
Data
| Component | Description |
|---|---|
| DataTable | Feature-rich table with sorting, filtering, row selection, grouping, inline editing, column resizing, and row expansion. |
| Paginator | Standalone pagination control for use with any data list. |
| Picklist | Dual-list transfer control for moving items between two sets. |
Messages
| Component | Description |
|---|---|
| BannerAlert | Full-width page-level alert. Severities: info, success, warning, error. |
| InlineAlert | Compact inline alert for form-level feedback. |
| InlineContainedAlert | Bordered inline alert for use inside panels or cards. |
| Toast / Toaster / toast | Programmatic toast notifications. Mount <Toaster /> once; call toast(options) anywhere. |
Overlay
| Component | Description |
|---|---|
| Modal | Dialog overlay with header, body, and footer action slots. |
| Drawer | Slide-in panel overlay. |
| SidePanel / SidePanelLayout | Fixed side panel for persistent secondary content. |
| Popup | Anchored floating popup (e.g. contextual menus, micro-overlays). |
| Tooltip | Hover tooltip. Placements: top, bottom, left, right. |
Panel
| Component | Description |
|---|---|
| Card | Surface container with optional header and footer. |
| Accordion | Collapsible section list. Variants: default, flush. |
| Tabs | Horizontal tab navigation with panel content. |
| BannerTabs | Loan-workflow tab bar with status chips and badge groups. |
Status
| Component | Description |
|---|---|
| SeverityChip | Color-coded severity label. Variants: info, success, warning, error. |
| MiscChip | General-purpose label chip with configurable color. |
| ProfileChip | Avatar-style chip showing a person's initials or image. |
| AUSChip | Automated Underwriting System result chip. |
| ProgressBar | Horizontal progress indicator. |
| ProgressSpinner | Circular loading indicator. Large variant includes the Clear brand logo. |
Navigation
| Component | Description |
|---|---|
| TopBar | App-level top navigation bar with logo, nav items, user menu, and notifications. |
| TopBarMobile | Mobile variant of TopBar with drawer-based navigation. |
| URLATabsNav | URLA section tab navigation with applicant selector. Responsive: desktop, tablet, mobile variants. |
| LoanBannerNav | Loan detail page banner with action toolbar, condition badges, and collapsible sections. |
| FullNav / FullNavMobile | Full application navigation shell with primary and secondary nav areas. |
Token System
All tokens are CSS custom properties defined in src/tokens/tokens.css and exported via @cmgfi/clear-ds/tokens.
Base scale: 1rem = 12px — the consumer app must set html { font-size: 12px }.
Colors
Seven families, 10 shades each (-50 lightest → -900 darkest):
| Family | Prefix | Role |
|---|---|---|
| Teal | --teal-* | Brand color; primary actions, focus rings, active states |
| Surface | --surface-* | Neutral backgrounds; 50 = white, 900 = near-black |
| Navy | --navy-* | Text and borders |
| Yellow | --yellow-* | Warning severity |
| Blue | --blue-* | Informational severity |
| Green | --green-* | Success severity |
| Red | --red-* | Error severity |
Semantic aliases:
--color-text: var(--navy-800); /* primary body text */
--color-text-secondary: var(--navy-500); /* helper / secondary text */Spacing
Two complementary scales:
/* Numeric — direct pixel mapping */
--spacing-02: 2px; --spacing-04: 4px; --spacing-06: 6px;
--spacing-08: 8px; --spacing-12: 12px; --spacing-16: 16px;
--spacing-20: 20px; --spacing-24: 24px; --spacing-32: 32px;
--spacing-40: 40px;
/* Named aliases — semantic */
--spacing-xxxs: 2px; --spacing-xxs: 4px; --spacing-xs: 6px;
--spacing-sm: 8px; --spacing-md: 12px; --spacing-lg: 16px;
--spacing-xl: 24px; --spacing-xxl: 32px;Typography
Font family: Open Sans (--font-family: 'Open Sans', sans-serif)
Heading utility classes: .text-h1 through .text-h6
Body utility classes — 4 sizes × 3 weights = 12 combinations:
.text-xl-bold .text-xl-semibold .text-xl-regular
.text-large-bold .text-large-semibold .text-large-regular
.text-normal-bold .text-normal-semibold .text-normal-regular
.text-small-bold .text-small-semibold .text-small-regularElevation
--shadow-depth-1: 0 1px 4px rgba(0,0,0,0.12);
--shadow-depth-2: 0 2px 8px rgba(0,0,0,0.16);
--shadow-depth-3: 0 4px 16px rgba(0,0,0,0.20);
--shadow-depth-4: 0 8px 32px rgba(0,0,0,0.24);
--scrim: /* navy-500 at 25% opacity */;Icons
--icon-size-sm: 12px;
--icon-size-md: 14px;
--icon-size-lg: 18px;
--icon-size-xl: 24px;Usage with PrimeIcons:
<i className="pi pi-check" style={{ fontSize: 'var(--icon-size-lg)' }} />Grid
/* Mobile — 4 column */
--grid-columns-mobile: 4; --grid-gutter-mobile: 16px;
--grid-margin-mobile: 24px; --grid-container-mobile: 576px;
/* Tablet — 6 column */
--grid-columns-tablet: 6; --grid-gutter-tablet: 16px;
--grid-margin-tablet: 48px; --grid-container-tablet: 768px;
/* Desktop — 12 column */
--grid-columns-desktop: 12; --grid-gutter-desktop: 18px;
--grid-margin-desktop: 128px; --grid-container-desktop: 1200px;
/* Breakpoints */
--breakpoint-sm: 576px; --breakpoint-md: 768px; --breakpoint-lg: 1200px;Tech Stack
| Concern | Choice | Notes |
|---|---|---|
| Framework | React >=17 | Peer dep — never bundled; consumer's React is used |
| Language | TypeScript 5 strict | Exported types are part of the public API |
| Build | Vite 5 library mode | ESM + CJS dual output; CSS Modules built-in |
| Type declarations | vite-plugin-dts rollupTypes: true | Single rolled-up index.d.ts |
| Styles | CSS Modules | Scoped, zero runtime overhead; token vars pass through without JS |
| Docs / QA | Storybook v8 @storybook/react-vite | Shares the same Vite config; no duplicate setup |
| Icons | PrimeIcons >=7 | CSS font glyphs; pi pi-* class API; must be peer dep |
| Node / npm | 22 / 11 (LTS) | |
Not present (intentional):
- No CSS-in-JS — runtime overhead is unacceptable in a library
- No Tailwind — custom token system conflicts with Tailwind's class API
- No third-party component library — pixel-fidelity requires full control
Project Structure
clear-ds/
├── src/
│ ├── index.ts ← explicit public API; all component + type exports
│ ├── tokens/
│ │ ├── tokens.css ← all CSS custom properties (428 lines)
│ │ ├── TokenDocs.module.css ← shared layout for token Storybook pages
│ │ ├── Colors.stories.tsx
│ │ ├── Typography.stories.tsx
│ │ ├── Spacing.stories.tsx
│ │ ├── Elevation.stories.tsx
│ │ ├── Icons.stories.tsx
│ │ └── Grid.stories.tsx
│ └── components/
│ └── ComponentName/
│ ├── ComponentName.tsx ← React.forwardRef; JSDoc props; displayName
│ ├── ComponentName.module.css← token vars only; no hardcoded values
│ ├── ComponentName.stories.tsx← autodocs; named states; AllStates last
│ └── index.ts ← re-exports component + all types
├── .storybook/
│ ├── main.ts ← framework, addons, stories glob
│ └── preview.ts ← global CSS imports, backgrounds, storySort
├── dist/ ← build output (not committed)
├── vercel.json ← Storybook deployment config
├── vite.config.ts
├── tsconfig.json
└── package.jsonLocal Development
Prerequisites: Node 22, npm 11
# Install dependencies
npm install
# Start Storybook dev server (localhost:6006)
npm run storybook
# Build the library (outputs to dist/)
npm run build
# Type-check without emitting
npm run type-check
# Watch mode for library build
npm run build:watchAdding a new component
Every component follows the same invariant structure — do not deviate:
- Create
src/components/ComponentName/with four files:ComponentName.tsx— alwaysReact.forwardRef; props extendReact.HTMLAttributes<HTMLElement>; every prop has a JSDoc comment; setdisplayName; spread...props; mergeclassNameComponentName.module.css— token vars exclusively; no hardcoded hex or pixel values (except sub-pixel precision:border: 1px,outline-offset: 2px); use:focus-visiblenot:focusComponentName.stories.tsx—tags: ['autodocs']on meta; individual state stories;AllStatesis always the last exportindex.ts— re-export component and all types
- Add exports to
src/index.ts - Add the component to
storySort.orderin.storybook/preview.ts
Storybook conventions
- Sidebar order:
Best Practices → Tokens → Components (Buttons, Data, Form Controls, Messages, Overlay, Panel, Status) → Navigation tags: ['autodocs']is mandatory on everymeta— without it there is no Docs tabAllStates(orAllSizes/AllSeverities) must always be the last story export — Storybook renders in declaration order- When a story
render:function needsuseState, define a named inner component and call the hook inside it (hooks cannot be called insiderenderdirectly)
Build Output
dist/
├── index.mjs ← ESM bundle (tree-shakeable), 230 KB
├── index.cjs ← CommonJS bundle, 153 KB
├── index.css ← all compiled component styles, 126 KB
├── index.d.ts ← single rolled-up TypeScript declarations, 81 KB
├── index.mjs.map ← ESM source map
├── index.cjs.map ← CJS source map
└── tokens/
└── tokens.css ← design tokens (separate import path), 14 KBVite config highlights:
cssCodeSplit: false— all CSS Modules compile into a singleindex.cssexternal: ['react', 'react/jsx-runtime', 'react-dom']— React is never bundledrollupTypes: true— singleindex.d.tsinstead of one file per source fileassetFileNames: 'index.css'— predictable output filenamepostbuildnpm script — copiessrc/tokens/tokens.css→dist/tokens/tokens.css
Publishing
Releases are fully automated by GitHub Actions — do not run npm publish or vercel deploy from a laptop. Teammates need only write access to the repo (via XD Team membership); CI uses org-owned NPM_TOKEN and VERCEL_TOKEN secrets.
npm — publish a new version
git checkout main && git pull
npm version patch # or: minor / major
git push origin main && git push origin --tags
gh run watch # confirm the publish workflow goes greenThe v* tag push triggers .github/workflows/publish-npm.yml, which builds and publishes @cmgfi/clear-ds to public npm.
Storybook (Vercel)
Storybook deploys automatically on every push to main via .github/workflows/deploy-vercel.yml, hosted at clear-ds.cmgfinancial.ai under the cmgprojects team. No manual vercel deploy needed.
vercel.json configures the build:
{
"buildCommand": "npm run build-storybook",
"outputDirectory": "storybook-static"
}Full release instructions and the emergency manual fallback live in RELEASING.md.
Contributing
Git workflow
- Remote:
https://github.com/cmg-pilot-program/clear-ds.git - Branch from
main, PR back tomain - Commit messages follow conventional commits (
feat:,fix:,chore:,docs:) - No approval required to merge — open a PR, wait for the
buildcheck (.github/workflows/ci.yml) to pass, then squash-merge your own PR. Admins can--admin-bypass in a true emergency. SeeRELEASING.mdfor the full release flow.
Before submitting a PR
- [ ]
npm run type-checkpasses with zero errors - [ ]
npm run buildcompletes successfully - [ ] New component has all four required files
- [ ]
tags: ['autodocs']is on the story meta - [ ]
AllStatesis the last story export - [ ] No hardcoded colors or spacing values — token vars only
- [ ]
src/index.tsexports the new component and all its types - [ ] Component added to
storySort.orderin.storybook/preview.ts
CI runs
npm run build+npm run build-storybookon every PR (the requiredbuildcheck). A PR cannot merge until it passes — but no human approval is needed.
Hard-won Rules
These are not guidelines — they are invariants derived from real failures during the build of this library:
| Rule | Why |
|---|---|
| tags: ['autodocs'] on every story meta | Without it there is no Docs tab. Retroactively adding it across many files is painful. Add it when the file is created. |
| AllStates always last story export | Storybook renders in declaration order. A misplaced overview story at the top pollutes the component's story list. |
| primeicons in peerDependencies, not devDependencies | If it is only in devDeps, it will not be available in consumer bundles — icon glyphs silently disappear. |
| publishConfig: { "access": "public" } is required | Scoped packages are private by default on npm. Without this field, npm publish is rejected even with a valid token. |
| Never hardcode colors or spacing | Always use token vars. Hardcoded values break theming and make design drift invisible. |
| React.forwardRef on every component | Consumers need ref access for focus management, animations, and third-party integrations. |
| :focus-visible not :focus | :focus shows focus rings on mouse clicks. :focus-visible restricts them to keyboard navigation. |
| min-width: 0 on shrinkable flex children | flex: 1 alone does not allow an item to shrink below its content size. Without min-width: 0, the item overflows its container. |
| Merge consumer className, never replace | Spreading ...props is not enough. Explicitly merge: className={[styles.root, props.className].filter(Boolean).join(' ')}. |
| The postbuild script must stay in package.json | It copies src/tokens/tokens.css → dist/tokens/tokens.css. Without it, the @cmgfi/clear-ds/tokens import path resolves to a file that does not exist. |
Design Source
All visual decisions originate in Pencil — a design tool integrated as an MCP server. The .pen source files are in the project experiments directory alongside this package. The code is a direct translation of those designs: same structure, same hierarchy, same property names.
The token master copy lives at ../clear-ds-tokens.css (one level above this package). When updating tokens, sync that file into src/tokens/tokens.css.
