Brings Tremor (Apache-2.0) in as the project's chart component
library and converts the response-time chart as the first
integration. Tremor is distributed copy-paste ("Tremor Raw") so
the components live under `src/components/tremor/`; the only
runtime peer dependency added is `recharts`. Two small local
adaptations to the vendored `BarChart`:
- `cx` (Tremor's `clsx`+`twMerge` helper) → our `cn` from
`@/lib/utils`, aliased locally to keep diff vs upstream minimal.
- `@remixicon/react` legend-slider icons →
`lucide-react`'s `ChevronLeft` / `ChevronRight`, since
`lucide-react` is already a project dep.
- `ChartTooltipProps.label` widened from `string` to
`string | number | undefined` to match recharts 3.x (Tremor's
source still types it for recharts 2.x).
Why Tremor and not a wider switch (Mantine, HeroUI):
- The dashboard ships ~700 lines of hand-rolled SVG across three
chart files — a focused charting library is the highest-ROI
addition without touching the wider component surface.
- Tremor is Tailwind-native and built on the same Radix/Base UI
layer as the existing `src/components/ui/*`, so it composes
rather than displaces.
- Apache-2.0 + copy-paste distribution means no upgrade lock-in.
What this PR does
- Adds `src/components/tremor/{bar-chart.tsx,chart-colors.ts,
get-y-axis-domain.ts,use-on-window-resize.ts}` (vendored from
tremorlabs/tremor with the adaptations above).
- Installs `recharts@^3.8.1` (React 19 + Next.js 16 compatible).
- Rewrites `src/components/dashboard/response-time-chart.tsx`:
drops 130 lines of bespoke SVG math (axis scaling, bar
positioning, grid lines, tooltip rendering), reuses the same
card/header/empty-state shell.
Deferred (own follow-up PRs)
- `pipeline-donut.tsx` — has user-customisable per-stage hex
colours; Tremor's palette would override them. Needs either a
small upstream patch or a `colors` override path.
- `conversations-chart.tsx` — area-chart conversion (largest of
the three, 341 lines).
- The previous hand-rolled bar chart drew a horizontal dashed
"target N minutes" reference line. Tremor BarChart doesn't
expose Recharts primitives, so for now the target is promoted
to a header pill (`target 5m`). On-chart reference line is a
follow-up — either via a positioned overlay or an upstream
`referenceLines` prop.
Verification
- `npm run lint` → 0 errors (same 20 pre-existing warnings).
- `npm run typecheck` → clean.
- `npm test` → 319/319 pass.
- `npm run build` → succeeds, dashboard remains a static route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #132 (color themes) only tokenized the high-visibility chrome.
Inbox / contacts / pipelines / broadcasts / automations / auth /
settings sub-forms still had ~150 hard-coded `violet-*` classes
across ~32 files, so picking a non-violet theme felt half-broken —
sidebar and header changed colour, but message bubbles, deal cards,
and the broadcast wizard all stayed violet.
This is the mechanical follow-up: replace every `violet-N(/N)?`
with `primary(/N)?` so the theme picker themes the whole app.
Two non-trivial swaps inside the pass:
- `text-white` on a `bg-primary` button → `text-primary-foreground`.
Emerald and Amber define a *dark* `--primary-foreground` because
their primaries are bright; `text-white` over those would tank
contrast.
- `bg-primary hover:bg-primary` (hover identical to base — original
code had this latent bug from `hover:bg-violet-500`) → adds `/90`
on hover so the lift is visible.
Test update:
- `broadcast-status.test.ts` asserted shade numbers (`bg-X-N/10`).
Relaxed the regex to also accept token form (`bg-X/10`) since
brand-accent status badges now ride the active theme.
Files touched: 49. Lint clean (existing warnings only), 173/173
tests, typecheck + build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legal brief on third-party trademark use says: avoid the WhatsApp
teal/green palette and any visual treatment that could read as a
WhatsApp/Meta product. Emerald-500 was used as the primary brand colour
across hero, CTAs, sidebar, charts, badges, and the favicon — close
enough in hue to WhatsApp's signature green to muddy the line.
Swapping to Tailwind violet (which matches the Hostinger brand purple
#7c3aed) makes the visual identity unambiguously ours.
Scope:
- Mechanical Tailwind class swap: emerald-* → violet-* across src/.
- src/app/globals.css: --primary, --ring, --sidebar-primary, --sidebar-ring,
--chart-1 oklch tokens swapped from emerald (oklch 0.696 0.17 162) to
Hostinger-aligned violet (oklch 0.526 0.247 293).
- src/app/icon.tsx: favicon background #10b981 → #7c3aed.
- src/app/opengraph-image.tsx: badge bg, accent text, gradient.
- src/components/landing/hero.tsx + cta-banner.tsx: radial-gradient hex.
- src/components/landing/mock/analytics-mock.tsx: marketing chart line.
- src/components/dashboard/conversations-chart.tsx + response-time-chart.tsx:
"Outgoing" series colour. Distinguishable from "Incoming" blue at a
glance; we can revisit the chart pair later for stronger colourblind
contrast (separate concern).
- README.md: license badge colour parameter.
Out of scope:
- src/components/settings/tag-manager.tsx still offers "Emerald" as one
of the user-pickable contact-tag colours alongside Blue / Yellow / Red
/ etc. — that's a user data choice, not a brand mark.
Contrast check: white text on #7c3aed measures ~4.78:1 (WCAG AA passing
for normal body text). Violet-400 (#a78bfa) on slate-950 measures ~10:1
for accent text, well clear of AA.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hover was off by ~one letterbox width on wide layouts — users had to
drag the cursor far right of the last data point to see its tooltip.
Root cause: the SVG uses a fixed 760×240 viewBox with preserveAspect-
Ratio's default "xMidYMid meet", which centers the content and leaves
empty letterbox bars on either side when the container is wider than
the viewBox aspect. The old hover math mapped clientX linearly onto
the full SVG DOM rect, so every pixel of letterbox shifted the snap
further from the real data point.
Switched to SVGSVGElement.getScreenCTM().inverse() to map screen
coords back to viewBox units correctly regardless of letterboxing,
scaling, or future transforms. The tooltip's horizontal offset also
now comes from the same CTM (mapping the snapped data point's viewBox
x back to a pixel offset inside the wrapper div), so the tooltip and
crosshair line up exactly on top of each other.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With real data the feed showed 20 rows by default and filled most of
the viewport. Switched to a 5-row default with an inline size picker
(5 / 10 / 20 / 50) so users choose how much history is visible.
- Dashboard page bumps the fetch from 20 → 50 so every size option is
already in memory. Switching sizes is a pure client-side slice, no
round trip.
- ActivityFeed owns the pageSize state, renders "Showing N of M"
plus a button group in the footer. The smallest option is always
enabled; larger options unlock only once we have enough rows to
meaningfully reveal more (e.g. "10" enables once loaded > 5).
- Existing "View all" header link left alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With grid items-stretch (the default) the two cards in the charts
row try to share the tallest sibling's height — but the Pipeline
Value card was rendering at its natural (shorter) height because
neither its col-wrapper nor its inner section had h-full. So the
line chart's taller content drove the row but the pipeline card
hung at the top with trailing whitespace.
Added h-full on both col wrappers and flex h-full flex-col on
each inner section so they actually fill the row. The existing
flex-1 wrapper inside PipelineDonut now has a defined height to
stretch against, so the empty state (and the populated donut +
legend) span the card cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the dashboard page as a data-driven analytics surface using
existing Supabase tables only — no schema changes, no other pages
touched.
Widgets (all render independent skeletons while their data fetches,
and show a shared empty state when there's not enough data to be
useful):
- Metric cards (Active Conversations, New Contacts Today, Open Deals
Value, Messages Sent Today) with up/down/no-change delta indicator.
- Quick actions row (New Contact / Deal / Broadcast / Automation).
- Conversations Over Time: SVG line chart, 7/30/90-day tabs, per-day
hover crosshair + tooltip. Cached per range so tab switches are
instant.
- Pipeline Value: SVG donut with per-stage arc segments, total in the
center, legend with deal count + value.
- Average First Response Time: SVG bar chart by weekday with a
configurable dashed target line and this-week / last-week summary.
- Activity feed: latest 20 items merged across messages, contacts,
deals, broadcasts, and automation logs — click-through deep-links
where applicable.
Deviations from spec, noted up-front:
- Strictly dark theme — spec's #f9fafb alternating rows translate to
slate-900 / slate-900/40 stripes.
- Kept Inter (tabular-nums, bold 28px) for metric values instead of
Space Grotesk to avoid touching root layout.
- Quick actions link to the relevant list pages (/contacts, etc.)
rather than auto-opening their create modals — that would require
modifying those pages.
All aggregation is client-side via RLS-scoped queries. If a tenant's
dataset outgrows a few thousand messages per range window, the
response-time and line-chart queries should migrate to SQL RPCs —
noted as follow-up, not shipped here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>