The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
15 KiB
Quill — Insights's unified design system
A React component library powering Insights's unified UI surfaces (web, MCP, electron), built on Base UI and shadcn primitives. Quill requires Tailwind v4 on the consumer side — the library ships component source and theme metadata, and your app's Tailwind compiles the utilities into your own CSS bundle.
Packages
Quill publishes two independent packages to npm. Everything else in packages/quill/packages/* is internal workspace infrastructure and never ships.
| Package | Description |
|---|---|
@hanzo/quill |
The library — React components, bundled types, and three small CSS files that wire quill into your Tailwind build. Install this. |
@hanzo/quill-tokens |
Typed JavaScript exports of the design tokens (semantic colors, spacing, shadows, etc.) for consumers who want programmatic access outside of components. Optional. |
@hanzo/quill is self-contained for styling purposes — tokens are bundled into dist/tokens.css, so consumers don't also need @hanzo/quill-tokens unless they want the typed JS exports.
Getting started
1. Install
pnpm add @hanzo/quill
Tailwind v4 is a peer dependency. If you don't already have it:
pnpm add -D tailwindcss @tailwindcss/vite
(Use the PostCSS or CLI integration instead of @tailwindcss/vite if your bundler isn't Vite.)
2. Wire up the CSS
Quill ships three small stylesheets. Import them from your app's Tailwind entry — usually globals.css, app.css, or wherever you already have @import "tailwindcss".
@import 'tailwindcss';
@import '@hanzo/quill/tokens.css'; /* design tokens (CSS vars + @theme) */
@import '@hanzo/quill/base.css'; /* one border-color reset rule */
@import '@hanzo/quill/tailwind.css'; /* @source directive pointing at quill's dist */
What each file does:
tokens.css— registers every quill design token (--color-*,--text-*,--leading-*,--radius-*,--shadow-*,--spacing, …) with your Tailwind compiler so that utilities likebg-fill-hover,text-muted-foreground, androunded-mdresolve correctly. Also contains the light/dark CSS custom property values at:rootand.dark.base.css— one@layer base { * { border-border outline-ring/50 } }reset. Load-bearing: primitives write plainborder(no colour modifier) expecting the default border colour to be--color-border. Without this, Tailwind v4 falls back tocurrentColorand every bordered primitive looks broken.tailwind.css— a single@source "./**/*.js"directive. When your Tailwind v4 processes the import, it resolves that glob relative to the file's on-disk location insidenode_modules/@hanzo/quill/dist, so it scans quill's compiled library JS for class-name strings and generates the matching utilities in your ownutilitieslayer. No pre-compiled stylesheet, no cascade-layer fight with your own Tailwind output.
3. Cascade layer order (usually optional)
If you import CSS from other libraries into named layers (e.g. @import "@radix-ui/themes/styles.css" layer(radix)), make sure your layer order puts those library layers after base (so library defaults beat Tailwind's preflight) and before utilities (so your own className overrides still win):
@layer theme, base, radix, components, utilities;
@import '@radix-ui/themes/styles.css' layer(radix);
@import 'tailwindcss';
@import '@hanzo/quill/tokens.css';
@import '@hanzo/quill/base.css';
@import '@hanzo/quill/tailwind.css';
Quill itself is not layered — its utilities land in the top-level utilities layer alongside yours, which is what makes consumer overrides via className Just Work.
4. Using the @config legacy bridge? Two extra lines
If your project still runs Tailwind v4 in @config "tailwind.config.js" legacy-compat mode (common when migrating from v3), the legacy mode has two quirks quill works around but cannot hide:
-
It ignores
@sourcedirectives nested inside@imported CSS files. Add an explicit@sourcepointing at quill's dist next to the imports:@source "../../../../node_modules/@hanzo/quill/dist/**/*.js";Adjust the relative path so it resolves from your CSS entry to
node_modules/@hanzo/quill/dist. Yes, it's fragile — if you can, migrate off@configand delete this line. -
It strips Tailwind v4's default
@themevalues. Quill'stokens.cssships the defaults it depends on (--leading-*,--tracking-*,--font-weight-*) inline so primitives liketext-xs/relaxed,tracking-tight, andfont-mediumstill resolve. Nothing to do on your side — this just explains whytokens.cssis larger than you'd expect.
5. Set up dark mode
Quill uses class-based dark mode: add a .dark class to any ancestor element (usually <html>) and every token flips automatically.
<html class="dark">
<!-- dark mode active for the whole app -->
</html>
Or use the built-in ThemeProvider for system-preference detection, localStorage persistence, and cross-tab sync:
import { ThemeProvider } from '@hanzo/quill'
function App() {
return (
<ThemeProvider defaultTheme="system">
<YourApp />
</ThemeProvider>
)
}
ThemeProvider exposes a useTheme() hook for reading and setting the theme programmatically.
6. Theme the palette (optional)
Quill's surface and brand colours are driven by four CSS custom properties. Override them at :root to reskin the whole app, or on any element to reskin just that subtree — no rebuild, no JS.
| Variable | Default | What it controls |
|---|---|---|
--theme-hue |
90 |
OKLCH hue for light-mode surfaces: background, card, popover, muted, accent, border, input |
--theme-dark-hue |
264 |
OKLCH hue for dark-mode surfaces |
--theme-tint |
0.006 |
OKLCH chroma intensity for neutral surface tinting (0 = pure grey) |
--primary-hue |
37.89 |
OKLCH hue for the brand / primary colour |
Global override — reskin the entire app:
:root {
--theme-hue: 200; /* cool blue neutrals */
--primary-hue: 145; /* green brand */
}
Subtree override — theme one section differently. Any valid CSS selector works, and Tailwind v4 arbitrary properties let you do it inline:
<aside className="[--theme-hue:300] bg-muted">
{/* every quill surface inside this aside shifts to a magenta tint */}
</aside>
What is and isn't themeable:
- ✅ Surfaces (background, card, popover, muted, accent, border, input) and their foregrounds — derived from
--theme-hue+--theme-tint - ✅ Primary / brand — derived from
--primary-hue - ❌ Status colours (destructive, success, warning, info) — fixed by semantic meaning; a red error should stay red regardless of brand hue
- ❌ Secondary — neutral dark by design
7. Use components
import { Button, Card, CardHeader, CardTitle, CardContent } from '@hanzo/quill'
function MyPage() {
return (
<Card>
<CardHeader>
<CardTitle>Hello Quill</CardTitle>
</CardHeader>
<CardContent>
<Button variant="primary">Click me</Button>
<Button variant="outline">Cancel</Button>
</CardContent>
</Card>
)
}
See the Storybook for the full component catalogue, props, and live variants — it's the source of truth and always matches what's shipped.
Overriding quill styles
Quill is designed so that consumer className wins over primitive defaults, without !important or specificity tricks. Every primitive routes its classes through cn() (clsx + tailwind-merge), so the consumer's string is deduplicated against quill's defaults at authoring time:
// h-7 px-2 text-xs/relaxed is quill's default. The overrides below replace
// each conflicting utility individually while keeping everything else.
<Button className="h-10 px-6 text-sm">bigger button</Button>
For variant-level customisation, import quill's cva variants and compose your own wrapper locally:
import { buttonVariants } from '@hanzo/quill'
import { cn } from '@/lib/utils'
export function GhostButton({ className, ...props }) {
return (
<button
className={cn(buttonVariants({ variant: 'link-muted', size: 'sm' }), 'hover:underline', className)}
{...props}
/>
)
}
For structural swaps, quill primitives built on Base UI inherit its render prop, which lets you replace the underlying element while keeping the primitive's behaviour and classes.
Using the design tokens directly
If you want programmatic access to quill's semantic colour definitions — for example to theme a non-quill component or generate previews — install the tokens package separately:
pnpm add @hanzo/quill-tokens
import { semanticColors, spacing, spacingPx, shadow } from '@hanzo/quill-tokens'
// spacing is a function that computes at any step, not a lookup table:
spacing(4) // '1rem' — for CSS-in-JS
spacingPx(4) // 16 — for React Native / Figma plugins
Most apps won't need this — the CSS custom properties (var(--primary), var(--background), var(--border), …) from tokens.css are usually enough.
CSS architecture
Quill follows the shadcn-style build model: the library ships component source and theme metadata, and the consumer's Tailwind does all the utility compilation. There is no pre-compiled utility stylesheet inside @hanzo/quill.
@hanzo/quill-tokens → Raw design token values (TS, CSS)
@hanzo/quill dist/index.js → Compiled library code — class strings preserved as literals
@hanzo/quill dist/tokens.css → :root / .dark vars + Tailwind v4 @theme inline block
@hanzo/quill dist/tailwind.css → @source "./**/*.js" — points Tailwind's scanner at the dist
Your app → Imports the three CSS files, runs Tailwind v4,
emits a single CSS bundle containing your utilities
and quill's utilities in one `utilities` layer
Why this model? The previous pre-compiled-stylesheet approach had a structural problem: any app that also used Tailwind shipped two utilities layers, and the layer order fight made consumer overrides unpredictable. Layer-wrapping quill in a named layer was a band-aid that broke any time cascade order changed.
The shadcn model has no such fight because there is only one Tailwind build: the consumer's. Quill's classes and the consumer's classes live in the same layer, deduplicated through tailwind-merge, and ordered by Tailwind's canonical sort. Consumer overrides work by construction, not by specificity hack.
Tree shaking: better, not worse. Consumer Tailwind only emits classes actually reached from their code plus the quill components they import. Unused quill utilities never land in the final CSS.
Development
Quill lives inside the Insights monorepo. Clone, install dependencies at the repo root, and run:
# Build quill + tokens
pnpm quill:build
# Dev loop: storybook with HMR for everything
pnpm quill:storybook
The storybook command runs:
@hanzo/quill-tokens build— generates the initial token CSS so storybook has something to import at start-up.@hanzo/quill-storybook storybook— the dev server.
Inside storybook's Vite config:
@sourceglobs scan primitive/component/blocksrc/*.{ts,tsx}directly. Any.tsxedit underpackages/{primitives,components,blocks}/srchot-reloads within a couple hundred ms — Tailwind's vite plugin catches the file change, rescans, and emits new utilities. No intermediate quill-dist rebuild.quillTokensWatcherplugin watchespackages/tokens/src/*.ts. Editing a token source file (colors, spacing, typography) rerunstsx src/build.ts, regenerates@hanzo/quill-tokens/dist/*.css, and triggers a full page reload so the new custom property values are live.- React HMR handles story and component code changes the usual way.
In short: edit anything under packages/*/src/**, see the result in the browser without rebuilding quill's dist. The dist artifacts (dist/index.js, dist/tokens.css, dist/tailwind.css) are only for external consumers pulling from npm.
Repo layout
packages/quill/
├── packages/
│ ├── tokens/ @hanzo/quill-tokens (published)
│ ├── primitives/ @hanzo/quill-primitives (private, bundled into @hanzo/quill)
│ ├── components/ @hanzo/quill-components (private, bundled into @hanzo/quill)
│ ├── blocks/ @hanzo/quill-blocks (private, bundled into @hanzo/quill)
│ └── quill/ @hanzo/quill (published — the aggregate consumers install)
└── apps/
└── storybook/ @hanzo/quill-storybook (private, dev tool)
Publishing
Quill is published to npm via a manually triggered GitHub Actions workflow: .github/workflows/publish-quill-npm.yml.
How it works
- Trigger —
workflow_dispatchonly. A maintainer runs "Publish Quill npm packages" from the Actions tab and picks an npm dist-tag (alphaorlatest). - Auth — runs under the
ReleaseGitHub environment withid-token: write, publishing with npm provenance (NPM_CONFIG_PROVENANCE: true) via OIDC trusted publishing. No long-lived npm tokens. - Build — installs the quill workspace with pnpm and runs
pnpm quill:build. The@hanzo/quillaggregate's build step emitsdist/tokens.css,dist/base.css,dist/tailwind.css, then Vite bundles the JS by inlining the source of the internal-primitives,-components, and-blocksworkspace packages into a singledist/index.jsand a single rolled-updist/index.d.ts. Class-name strings are left as literals in the bundled JS — this is what makes consumer-side Tailwind scanning work. - Publish — loops over the two public packages:
@hanzo/quill-tokens, then@hanzo/quill. - Notify — posts success/failure and the published versions to the client-libraries Slack channel.
Versioning
There's no changesets or semantic-release setup — versions are bumped manually. To cut a release:
- Bump
versioninpackages/quill/packages/tokens/package.jsonand/orpackages/quill/packages/quill/package.json. - Merge to
master. - Go to Actions → "Publish Quill npm packages" → Run workflow → pick
alphaorlatest.
Component checklist
- Tokens (colors, shadows, spacing, typography)
- Primitives (40+ components with Storybook stories)
- Components (composed primitives — in progress)
- Blocks (product elements — in progress)