Skip to content

Commit b4d042f

Browse files
committed
feat(ui-new): enhance theme with typography tokens and semantic colors
1 parent 834caad commit b4d042f

File tree

24 files changed

+474
-175
lines changed

24 files changed

+474
-175
lines changed

apps/portal/src/app/(public)/test-ui/components/TestUiPrimitives.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function ColorRow({
6464
<Box
6565
key={shade}
6666
bg={`${name}.${shade}`}
67-
h={8}
67+
h={16}
6868
flex={1}
6969
borderRadius="sm"
7070
title={`${name}.${shade}`}

apps/portal/src/app/layout.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { initializeServer } from '@comp/analytics/server';
44
import { cn } from '@comp/ui/cn';
55
// import '@comp/ui/globals.css';
66
import { Provider as ChakraProvider } from '@trycompai/ui-new';
7+
import { GeistSans } from 'geist/font/sans';
78
import { GeistMono } from 'geist/font/mono';
89
import type { Metadata } from 'next';
9-
import localFont from 'next/font/local';
1010
import { headers } from 'next/headers';
1111
import { NuqsAdapter } from 'nuqs/adapters/next/app';
1212
import { Suspense } from 'react';
@@ -66,12 +66,6 @@ export const viewport = {
6666
],
6767
};
6868

69-
const font = localFont({
70-
src: '../../public/fonts/GeneralSans-Variable.ttf',
71-
display: 'swap',
72-
variable: '--font-general-sans',
73-
});
74-
7569
export const preferredRegion = ['auto'];
7670

7771
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) {
@@ -92,7 +86,8 @@ export default async function Layout(props: { children: React.ReactNode }) {
9286
<html lang="en" suppressHydrationWarning>
9387
<body
9488
className={cn(
95-
`${GeistMono.variable} ${font.variable}`,
89+
// `variable` only defines the CSS variables. `className` actually applies the font-family.
90+
`${GeistSans.className} ${GeistSans.variable} ${GeistMono.variable}`,
9691
'overscroll-none whitespace-pre-line antialiased',
9792
)}
9893
>

packages/ui-new/agents.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
### UI‑New Agent Guide
2+
3+
This package is our Chakra v3 design system (`@trycompai/ui-new`). The goal is **one source of truth** for design decisions (colors, typography, radii, borders, focus rings) so changing a token updates the entire system.
4+
5+
### Golden rules
6+
7+
- **No hardcoded styling in recipes** if a token exists.
8+
- Use **tokens** (`radii`, `borders`, `shadows`, `fonts`, …) and **semantic tokens** (`colorPalette.*`, `bg/fg/border`) instead of raw values.
9+
- **Semantic tokens define behavior**, recipes only consume behavior.
10+
- Example: recipes use `colorPalette.solid/hover/active/contrast` instead of `primary.700`.
11+
- **DRY through factories + shared helpers**
12+
- Shared logic lives in `theme/semantic/` and `theme/recipes/shared/`.
13+
- **Colocate config with the recipe**
14+
- Each component recipe gets its own folder under `theme/recipes/<component>/` once it needs multiple files.
15+
- **Light palettes can use black contrast**
16+
- `yellow` and `sand` intentionally use `colorPalette.contrast = black` for readability.
17+
18+
### Folder structure
19+
20+
```
21+
packages/ui-new/src/
22+
components/
23+
ui/
24+
provider.tsx # ChakraProvider wiring
25+
color-mode.tsx # Color mode helpers
26+
... # UI wrappers
27+
theme/
28+
index.ts # createSystem + theme config (imports everything)
29+
global-css.ts # token-driven globalCss (low specificity :where)
30+
colors/
31+
index.ts # raw palettes (primary/secondary/blue/orange/rose/yellow/sand)
32+
tokens/
33+
borders.ts # border style tokens (subtle/strong/none)
34+
radii.ts # radii tokens + semantic radii (input/card)
35+
shadows.ts # focus ring shadow token (palette-aware)
36+
typography.ts # Geist fonts + brand line-height/letter-spacing + weights
37+
index.ts # barrel exports for tokens/*
38+
semantic/
39+
helpers.ts # {colors.<palette>.<shade>} refs + token helpers
40+
types.ts # types used by semantic token factories
41+
semantic-colors.ts # exports semanticColors
42+
palettes/
43+
dark-palette.ts # factory for “dark” palettes (white contrast)
44+
light-palette.ts # factory for “light” palettes (black contrast)
45+
secondary-palette.ts # neutral palette semantics
46+
index.ts # barrel export
47+
semantic-tokens.ts # backwards-compatible re-export of semanticColors
48+
recipes/
49+
index.ts # exports recipes
50+
shared/
51+
color-palettes.ts # SUPPORTED_COLOR_PALETTES + variant generator
52+
button/
53+
defaults.ts # BUTTON_DEFAULT_VARIANTS
54+
sizes.ts # BUTTON_SIZES
55+
variants.ts # BUTTON_VARIANTS
56+
recipe.ts # buttonRecipe
57+
index.ts # barrel for button recipe folder
58+
```
59+
60+
### How to add a new color palette
61+
62+
- **1) Add raw palette**
63+
- Add the scale in `src/theme/colors/index.ts`
64+
- **2) Add semantic behavior**
65+
- Add to `src/theme/semantic/semantic-colors.ts`
66+
- Choose the correct factory:
67+
- `makeDarkPalette({ palette: '...' })` for “dark” palettes (white text)
68+
- `makeLightPalette({ palette: 'yellow' | 'sand', ... })` for “light” palettes (black text)
69+
- **3) Allow it in recipes**
70+
- Add it to `src/theme/recipes/shared/color-palettes.ts` (`SUPPORTED_COLOR_PALETTES`)
71+
- Recipes like button will automatically pick it up.
72+
73+
### How to add a new recipe (best practice)
74+
75+
- Create `src/theme/recipes/<component>/` once you have multiple concerns:
76+
- `defaults.ts`, `sizes.ts`, `variants.ts`, `recipe.ts`, `index.ts`
77+
- Keep `recipe.ts` as composition:
78+
- base styles + `defaultVariants` + `variants` wired from the local folder + shared helpers.
79+
- Prefer **semantic tokens**:
80+
- `bg: 'colorPalette.solid'`
81+
- `_hover: { bg: 'colorPalette.hover' }`
82+
- `color: 'colorPalette.contrast'`
83+
- `borderColor: 'colorPalette.border'`
84+
- focus ring: `boxShadow: 'focusRing'` (palette-aware)
85+
86+
### Typography rules (Geist Sans)
87+
88+
- Host apps must load fonts via `next/font` and apply `GeistSans.className` at the document root.
89+
- `ui-new` defaults are enforced via:
90+
- `theme/tokens/typography.ts` for `fonts/*`, `lineHeights.brand`, `letterSpacings.brand`
91+
- `theme/global-css.ts` for document + headings + form controls
92+
93+
### Required checks (before shipping)
94+
95+
Run these (scoped to this package):
96+
97+
```bash
98+
bun run -F @trycompai/ui-new typecheck
99+
bun run -F @trycompai/ui-new lint
100+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Chakra global styles should be low-specificity and token-driven.
2+
// `:where(...)` keeps specificity near-zero so product code can still override when needed.
3+
4+
const BRAND_TYPOGRAPHY = {
5+
lineHeight: 'brand',
6+
letterSpacing: 'brand',
7+
} as const;
8+
9+
export const globalCss = {
10+
// Ensure Geist Sans is the default everywhere (Chakra components inherit from document).
11+
':where(html, body)': {
12+
fontFamily: 'body',
13+
...BRAND_TYPOGRAPHY,
14+
},
15+
16+
// Headings should use the heading face (still Geist Sans, but separate token for future).
17+
':where(h1, h2, h3, h4, h5, h6)': {
18+
fontFamily: 'heading',
19+
...BRAND_TYPOGRAPHY,
20+
},
21+
22+
// Form controls don't always inherit font styles consistently across browsers.
23+
':where(button, input, textarea, select)': {
24+
fontFamily: 'body',
25+
...BRAND_TYPOGRAPHY,
26+
},
27+
} as const;

packages/ui-new/src/theme/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react';
22
import { colors } from './colors';
3+
import { globalCss } from './global-css';
34
import { buttonRecipe } from './recipes';
45
import { semanticColors } from './semantic-tokens';
5-
import { borders, radii, shadows } from './tokens';
6+
import { borders, fonts, fontWeights, letterSpacings, lineHeights, radii, shadows } from './tokens';
67

78
const config = defineConfig({
9+
globalCss,
810
theme: {
911
tokens: {
1012
colors,
13+
fonts,
14+
fontWeights,
15+
letterSpacings,
16+
lineHeights,
1117
radii,
1218
borders,
1319
shadows,
1420
},
1521
semanticTokens: {
16-
colors: {
17-
...semanticColors,
18-
},
22+
colors: semanticColors,
1923
},
2024
recipes: {
2125
button: buttonRecipe,

packages/ui-new/src/theme/recipes/button.recipe.ts

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const BUTTON_BASE = {
2+
display: 'flex',
3+
alignItems: 'center',
4+
justifyContent: 'center',
5+
gap: '2',
6+
fontWeight: 'medium',
7+
transition: 'all 0.2s ease-in-out',
8+
cursor: 'pointer',
9+
borderRadius: 'input',
10+
border: 'none',
11+
outline: 'none',
12+
boxShadow: 'sm',
13+
_focusVisible: {
14+
borderColor: 'colorPalette.focusRing',
15+
boxShadow: 'focusRing',
16+
outline: 'none',
17+
border: 'none',
18+
},
19+
} as const;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const BUTTON_DEFAULT_VARIANTS = {
2+
variant: 'solid',
3+
size: 'sm',
4+
colorPalette: 'primary',
5+
} as const;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { BUTTON_BASE } from './base';
2+
export { BUTTON_DEFAULT_VARIANTS } from './defaults';
3+
export { buttonRecipe } from './recipe';
4+
export { BUTTON_SIZES } from './sizes';
5+
export { BUTTON_VARIANTS } from './variants';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineRecipe } from '@chakra-ui/react';
2+
3+
import { BUTTON_BASE, BUTTON_DEFAULT_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS } from '.';
4+
import { createColorPaletteVariants } from '../shared/color-palettes';
5+
6+
const COLOR_PALETTE_VARIANTS = createColorPaletteVariants();
7+
8+
export const buttonRecipe = defineRecipe({
9+
base: BUTTON_BASE,
10+
defaultVariants: BUTTON_DEFAULT_VARIANTS,
11+
variants: {
12+
size: BUTTON_SIZES,
13+
variant: BUTTON_VARIANTS,
14+
colorPalette: COLOR_PALETTE_VARIANTS,
15+
},
16+
});

0 commit comments

Comments
 (0)