Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/precompiled-utilities-and-token-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'@e412/rnui-themes': minor
'@e412/rnui-react': patch
---

Add a precompiled utility layer and a declared token export.

**Why:** rnui is normally consumed with Tailwind running in the consumer's build, so any class you write compiles on demand. But it is also consumed *precompiled* — as a single stylesheet embedded in a design tool or preview. In that mode a class Tailwind never emitted silently does nothing: no error, no warning, the element just renders unstyled. Ordinary layout markup (`gap-10`, `grid-cols-4`, `max-w-4xl`, `text-3xl`) hit this constantly.

**`@e412/rnui-themes/utilities`** — a precompiled stylesheet (~353 KB, ~40 KB gzip) covering complete scales rather than only the values rnui's own components happen to use:

- spacing `0`–`24` across `p m gap space-x space-y w h min-w min-h` and every axis/side variant
- `text-xs`–`text-9xl`, `font-thin`–`font-black`, `tracking-*`, `leading-*`
- `grid-cols-1..12`, `col-span-*`, `row-span-*`
- `max-w-xs`–`max-w-7xl`, `rounded-*`, `border-*`, `opacity-0..100`
- `sm: md: lg: xl:` responsive and `hover: focus: active: disabled: dark:` states on the interactive subset

Import it only if you cannot run Tailwind yourself; if you can, keep importing `@e412/rnui-themes` and let your build compile what you use.

**`@e412/rnui-themes/safelist`** — the safelist source, for adding these scales to your own Tailwind build.

**`dist/utilities.json`** — the classes that actually shipped, derived from the compiled output rather than the safelist source, so tooling can tell a consumer "`gap-9` is not in this bundle" instead of letting it fail silently.

**`dist/tokens.json`** — the declared public token surface: 399 tokens across every theme, each with its name, theme, `kind` (`color` | `dimension` | `font` | `radius`), value, and light/dark pair. Built from the hand-authored theme sources, never from compiled CSS, so Tailwind's internal custom properties are structurally excluded. This is now the source of truth for token extraction — do not scan the generated stylesheets. Tokens a theme's dark ramp omits are flagged `darkInherited` rather than silently presented as declared.

**`@e412/rnui-react`:** renamed the private custom properties `--cell-size` / `--cell-radius` (Calendar) and `--data-grid-fill-size` (DataGrid) to a `--rnui-_` prefix, so they are unambiguously internal and not mistaken for design tokens. These were never documented as customization points. `--sidebar-width` and `--sidebar-width-icon` are deliberately unchanged: they are set via a `style` prop on `SidebarProvider`, which is a supported override.
249 changes: 249 additions & 0 deletions .design-sync/NOTES.md

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions .design-sync/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"projectId": "06c8634f-1fde-4d6f-9598-7445be82a439",
"shape": "storybook",
"pkg": "@e412/rnui-react",
"globalName": "RnuiReact",
"entry": "dist/index.mjs",
"storybookConfigDir": "apps/storybook/.storybook",
"storybookStatic": ".design-sync/sb-reference",
"buildCmd": "pnpm run build",
"readmeHeader": ".design-sync/conventions.md",
"docsDir": "../../apps/docs/content/docs",
"extraFonts": ["../../.design-sync/fonts/fonts.css"],
"titleMap": {
"Resizable": "ResizablePanelGroup",
"Sonner": "Toaster",
"Advanced": "EChart",
"useFileUpload": null
},
"extraEntries": ["echarts/charts"],
"docsMap": {
"ResizablePanelGroup": "../../apps/docs/content/docs/components/resizable.mdx",
"Sortable": "../../apps/docs/content/docs/utilities/sortable-list.mdx",
"Toaster": "../../apps/docs/content/docs/components/sonner.mdx"
},
"overrides": {
"AreaChart": { "cardMode": "column" },
"BarChart": { "cardMode": "column" },
"LineChart": { "cardMode": "column" },
"PieChart": { "cardMode": "column" },
"AspectRatio": { "cardMode": "column" },
"Card": { "cardMode": "column" },
"CopyButton": { "cardMode": "column" },
"DateSelector": { "cardMode": "column" },
"Progress": { "cardMode": "single", "primaryStory": "WithLabelAndValue" },
"Separator": { "skip": ["components-separator--horizontal"] },
"Slider": { "cardMode": "column" },
"EChart": { "cardMode": "column" },
"ScrollArea": { "cardMode": "column" },
"Stepper": { "cardMode": "column" },
"Tabs": { "cardMode": "column" }
}
}
103 changes: 103 additions & 0 deletions .design-sync/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Building with rnui

rnui is a shadcn/ui-style system: Base UI primitives, styled with Tailwind CSS v4
utilities that resolve to OKLch CSS custom properties from `@e412/rnui-themes`.

## Setup — no theme provider

Theming is **pure CSS**. There is no `ThemeProvider` and nothing to wrap for
colors, fonts, or radius. Set attributes on a root element instead:

```jsx
const { Button, Card, CardHeader, CardTitle, CardContent } = window.RnuiReact;

// data-theme picks a preset; add `dark` for dark mode. Both are optional —
// omitting them gives the default warm-neutral light theme.
<div data-theme="ocean" className="bg-background text-foreground p-6">
<Card>
<CardHeader><CardTitle>Monthly revenue</CardTitle></CardHeader>
<CardContent className="flex gap-3 items-center">
<Button>Save</Button>
<Button variant="outline">Cancel</Button>
</CardContent>
</Card>
</div>
```

Presets: `oxide`, `ocean`, `violet`, `forest`, `rose`, `amber`, `slate`,
`crimson`. Each changes colors, radius, and typography. **The brand fonts ship
with this design system** (`fonts/`, reached via `styles.css`) — set
`data-theme` and the real typeface renders, nothing to load:

| Theme | Body (`--font-sans`) | Headings (`--font-heading`) |
|---|---|---|
| `oxide` | DM Sans | Bitter |
| `forest` | Source Sans 3 | Fraunces |
| `ocean` | Plus Jakarta Sans | system stack |
| `violet` | Outfit | system stack |
| `rose` | Nunito | system stack |
| `amber` | Sora | system stack |
| `slate` | Geist | system stack |
| `crimson` | Instrument Sans | system stack |

Only `oxide` and `forest` set a distinct heading face; the rest inherit the
system stack for `h1`–`h6`. The default (no `data-theme`) is a system stack
throughout, by design.

Three components need a wrapper, and only these three:

- `Tooltip*` — wrap the subtree in `TooltipProvider`, or tooltips never open.
- `Sidebar*` — wrap in `SidebarProvider` (`SidebarTrigger`/`useSidebar` need it).
- Toasts — render `<Toaster />` once near the root, then call `toast(...)`.

## The styling idiom — Tailwind utilities, precompiled

Tailwind v4 compiles at **build time** and there is no compiler here, so only
utilities already present in `_ds_bundle.css` resolve. The stylesheet ships
complete scales, so ordinary layout markup works:

| Concern | Available |
|---|---|
| Spacing | `p m gap space-x space-y` and every axis/side variant (`px- py- pt- pr- pb- pl- mx- my- …`) at `0 0.5 1 1.5 2 2.5 3 3.5 4 5 6 7 8 9 10 11 12 14 16 20 24` |
| Sizing | `w h min-w min-h max-h` on the same scale, plus `auto full screen fit min max`, fractions (`w-1/2` `w-1/3` `w-2/3` `w-3/4` …), and `max-w-xs`…`max-w-7xl` |
| Type | `text-xs`…`text-9xl`, `font-thin`…`font-black`, `tracking-tighter`…`tracking-widest`, `leading-none`…`leading-loose`, `text-left/center/right`, `truncate` |
| Layout | `flex grid block inline-flex inline-block hidden`, `flex-row/col/wrap`, `items-*`, `justify-*`, `grid-cols-1..12`, `grid-rows-1..6`, `col-span-1..12`, `row-span-1..6` |
| Position | `relative absolute fixed sticky`, `inset/top/right/bottom/left`, `z-0..50`, `overflow-*` |
| Borders | `rounded-none`…`rounded-4xl` + `rounded-full` (and per-corner/side), `border-0/2/4/8`, `shadow-xs`…`shadow-2xl` |
| Color | `bg- text- border-` + every design token (below), with `/5`…`/90` alpha on surfaces |
| Variants | `sm: md: lg: xl:` responsive, and `hover: focus: focus-visible: active: disabled: dark:` on colors, opacity and rings |

Design tokens usable as colors: `background foreground card popover primary
secondary muted accent destructive success info warning invert focus border
input ring sidebar` (each with its `-foreground` pair where one exists), plus
`chart-1`…`chart-5` and the `sidebar-*` family.

Never invent a color pairing: `bg-primary` goes with `text-primary-foreground`,
and the same holds for `card`, `popover`, `muted`, `accent`, `destructive`,
`success`, `info`, `warning`, `sidebar`.

**If a class isn't in the list above, don't guess** — an unemitted class fails
silently. Use an inline style against a token instead; tokens always resolve:

```jsx
<div style={{ marginTop: 'var(--radius-2xl)', color: 'var(--muted-foreground)' }} />
```

The authoritative list of what shipped is `utilities.json` in the
`@e412/rnui-themes` package (~4,900 classes); the tables above summarize it.

## Where the truth lives

- `styles.css` — the single entry; its `@import` closure is `tokens/*.css`
(every theme preset) plus `_ds_bundle.css` (all component styles). Read
`tokens/light.css` for the full custom-property list.
- `components/<group>/<Name>/<Name>.prompt.md` — real usage examples and variant
tables for that component. Read it before composing anything non-obvious.
- `components/<group>/<Name>/<Name>.d.ts` — the exact prop contract.

Charts are echarts-based: `BarChart`, `LineChart`, `AreaChart`, `PieChart`,
`RadarChart`, `ScatterChart` for the common cases, and `EChart` with `echarts` +
`chartConfig` when you need a custom option object. They read `--chart-1`…
`--chart-5` from the active theme, so they restyle with `data-theme` for free.

`cn(...)` is exported for conditional class merging.
56 changes: 56 additions & 0 deletions .design-sync/fetch-fonts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Fetch the theme presets' brand fonts from Google Fonts as local woff2 +
// a rewritten @font-face sheet, so designs render real brand typography.
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

const OUT = '/home/aloks98/projects/rnui/.design-sync/fonts';
mkdirSync(OUT, { recursive: true });

// families -> weight range actually reachable via Tailwind utilities in this DS
// (normal 400, medium 500, semibold 600, bold 700)
const FAMILIES = [
'Bitter', 'DM Sans', 'Fraunces', 'Geist', 'Instrument Sans',
'Nunito', 'Outfit', 'Plus Jakarta Sans', 'Sora', 'Source Sans 3',
];
// Keep the byte budget sane: latin covers the DS's own copy; latin-ext is a
// cheap safety net for accented content in designs.
const KEEP_SUBSETS = new Set(['latin', 'latin-ext']);
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const out = [];
let files = 0, bytes = 0;

for (const fam of FAMILIES) {
const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fam)}:wght@400..700&display=swap`;
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) { console.error(`! ${fam}: css ${res.status}`); continue; }
let css = await res.text();

// Google emits one @font-face per subset, each preceded by a /* subset */ comment.
const blocks = [...css.matchAll(/\/\*\s*([\w-\[\]]+)\s*\*\/\s*(@font-face\s*\{[^}]*\})/g)];
let kept = 0;
for (const [, subset, block] of blocks) {
if (!KEEP_SUBSETS.has(subset)) continue;
const m = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/.exec(block);
if (!m) continue;
const fontRes = await fetch(m[1], { headers: { 'User-Agent': UA } });
if (!fontRes.ok) { console.error(`! ${fam}/${subset}: font ${fontRes.status}`); continue; }
const buf = Buffer.from(await fontRes.arrayBuffer());
const name = `${slug(fam)}-${subset}.woff2`;
writeFileSync(join(OUT, name), buf);
files++; bytes += buf.length; kept++;
out.push(block.replace(m[0], `url(./${name})`).trim());
}
console.error(` ${fam}: ${kept} face(s)`);
}

writeFileSync(
join(OUT, 'fonts.css'),
`/* Brand fonts for the @e412/rnui-themes presets, self-hosted so designs\n` +
` render real typography instead of a fallback stack.\n` +
` Generated from Google Fonts (all families SIL Open Font License 1.1).\n` +
` Regenerate: node .design-sync/fetch-fonts.mjs */\n\n` +
out.join('\n\n') + '\n',
);
console.error(`\n${files} font file(s), ${(bytes / 1024 / 1024).toFixed(2)} MB total`);
Binary file added .design-sync/fonts/bitter-latin-ext.woff2
Binary file not shown.
Binary file added .design-sync/fonts/bitter-latin.woff2
Binary file not shown.
Binary file added .design-sync/fonts/dm-sans-latin-ext.woff2
Binary file not shown.
Binary file added .design-sync/fonts/dm-sans-latin.woff2
Binary file not shown.
Loading
Loading