Three tweaks · 15 → 45
so you get a present.
## Design system rules
<!-- Generated by roast-my-design-system from a scan of dubinc/dub on 2026-08-12.
Paste into CLAUDE.md, .cursor/rules or AGENTS.md. Regenerate after big refactors:
npx roast-my-design-system --rules -->
Follow these rules when writing or editing UI in this repo. Every rule below was derived from a scan of this codebase, with real paths and usage counts.
### Colours and tokens
- Design tokens live in `packages/tailwind-config/themes.css`. Reach for an existing token before inventing any value.
- Never hardcode colour values in components. The palette already has 49 tokens; the scan still found 55 hardcoded colours sitting next to them. Do not add more.
- Never eyeball a colour from memory: the scan found 14 nearly identical pairs (like #ffffff next to #fafafa). Look the exact value up, or better, use its token.
### Canonical components
- Use these existing components instead of writing new ones:
- `<Button>` from `packages/ui/src/button.tsx` (used 963x · props: text, variant, textWrapperClassName, shortcutClassName)
- `<Modal>` from `packages/ui/src/modal.tsx` (used 183x · props: showModal, setShowModal, onClose, desktopOnly)
- `<Tooltip>` from `packages/ui/src/tooltip.tsx` (used 145x · props: content, contentClassName, disabled, side)
- `<LoadingSpinner>` from `packages/ui/src/icons/loading-spinner.tsx` (used 110x)
- `<Popover>` from `packages/ui/src/popover.tsx` (used 100x · props: content, align, side, openPopover)
- `<InfoTooltip>` from `packages/ui/src/tooltip.tsx` (used 97x)
- `<AnimatedSizeContainer>` from `packages/ui/src/animated-size-container.tsx` (used 88x)
- `<PageContent>` from `apps/web/ui/layout/page-content/index.tsx` (used 84x · props: contentWrapperClassName, headerProps)
### Known duplicates: do not make it worse
- `<PayoutStats>` exists in 3 places. Treat `apps/web/ui/layout/sidebar/payout-stats.tsx` as canonical; do not import the other copies, and never create another.
- `<Logo>` exists in 2 places. Treat `packages/ui/src/logo.tsx` as canonical; do not import the other copy, and never create another.
- `<EmptyState>` is defined twice and one wraps the other. Import `packages/ui/src/empty-state.tsx`; do not create a third.
- `<FormControl>` exists in 2 places (`apps/web/ui/partners/groups/design/application-form/fields/form-control.tsx`, `apps/web/ui/submitted-leads/form-fields/form-control.tsx`). Match whichever the surrounding code already imports, and never create another.
- `<SettingsRow>` exists in 2 places (`apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/settings-row.tsx`, `apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/program-settings-row.tsx`). Match whichever the surrounding code already imports, and never create another.
- `<Hero>` exists in 2 places (`apps/web/ui/placeholders/hero.tsx`, `apps/web/ui/modals/dot-link-offer-modal.tsx`). Match whichever the surrounding code already imports, and never create another.
- Two icon sets collide on 11 names. Before adding any icon, check which set the surrounding file already imports and stay with it.
### Components nobody imports
- 16 components are defined but never imported (`<Alert>`, `<AlertDescription>`, `<AlertTitle>`…). Before writing any new component, check this list first; adopt one or flag it for deletion instead of adding another.
### Spacing and sizing
- Stay on the Tailwind spacing scale. If a gap looks wrong on a scale step, flag it instead of nudging by a pixel.
- No new arbitrary bracket values (`p-[13px]`, `text-[10px]`). The scan found 621 already. If a value repeats, it is a decision: name it as a token instead of writing the bracket again.
- Avoid new one-off CSS spacing values; 33 off-scale values are already in play.
### Typography
- The repo uses 3 typefaces: Inter, satoshi, Geist. Do not introduce another, and do not re-declare font stacks by hand; use the existing setup.
### Styling discipline
- Never write `style={{ ... }}` for static values; styling belongs to classes and tokens where the system can see it.
(47 static inline blocks already exist; do not add to them.)
- Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.
---
*Generated by [roast-my-design-system](https://github.com/pencilrebel/roast-my-design-system) ver. 3.11.0. Rescan after refactors to keep these rules honest.*
2 packages with enough UI to judge · the repo score above is the whole thing blended, and 7 packages were too small or too backend to score
| package | score | worst finding | size |
|---|---|---|---|
| apps/web | 40 | 38 inline style blocks | 3,274 files |
| packages/ui | 80 | 16 components nobody imports | 475 files |
A repo scores below its own packages by arithmetic, not by accident: distinct values add up across packages, so the whole always carries more than any part. Read the package scores for where each team stands, and the repo score for what your agent sees when it looks at everything at once.
No CLAUDE.md, no AGENTS.md, no .cursorrules. Every time your AI builds UI here, it guesses, from everything below. This is why its output looks almost-but-not-quite right.
A healthy product palette is up to ~24 colours: one brand hue with a few tints, one accent, up to 13 greys, and status colours.
a disciplined repo keeps these around a dozen · on-scale Tailwind steps (·) shown for context · off-scale in coral
every distinct declaration is a chance for the next one to be wrong
the 5 files carrying the most off-system styling
every duplicate is a place where your agent has to guess which one is canonical, and it picks wrong half the time. Paths open in VS Code
apps/web/ui/shared/icons/lock.tsx vs packages/ui/src/icons/nucleo/lock.tsx
styling no system can see
1,555 components defined · top by adoption · the real system, buried in here
| component | used | defined in | props |
|---|---|---|---|
| <Button> | 963× | packages/ui/src/button.tsx | text variant textWrapperClassName shortcutClassName loading |
| <Link> | 615× | apps/web/ui/shared/icons/link.tsx | none |
| <Modal> | 183× | packages/ui/src/modal.tsx | showModal setShowModal onClose desktopOnly preventDefaultClose |
| <Tooltip> | 145× | packages/ui/src/tooltip.tsx | content contentClassName disabled side disableHoverableContent |
| <LoadingSpinner> | 110× | packages/ui/src/icons/loading-spinner.tsx | none |
| <Footer> | 106× | packages/email/src/components/footer.tsx | email marketing unsubscribeUrl https notificationSettingsUrl |
| <Footer> | 106× | packages/ui/src/footer.tsx | staticDomain |
| <Popover> | 100× | packages/ui/src/popover.tsx | content align side openPopover setOpenPopover |
| <InfoTooltip> | 97× | packages/ui/src/tooltip.tsx | none |
| <AnimatedSizeContainer> | 88× | packages/ui/src/animated-size-container.tsx | none |
Routers, dynamic imports and barrel files can hide real usage, so treat this as a shortlist to check, not a demolition order.