Skip to content

Commit 538e26e

Browse files
committed
feat(components): prepare package for public beta
Harden the component library ahead of a public beta release. Accessibility: - Darken --eb-color-border to meet the WCAG 2.2 3:1 non-text contrast minimum (1.95:1 -> 3.03:1 light, 2.04:1 -> 3.00:1 dark). - Give Checkbox and Radio a 24x24px pointer target via a transparent ::before, leaving the 16px visual box unchanged. - Add chart series tokens that meet 3:1 in both themes and sit in distinct lightness tiers, so series stay separable in grayscale and for red-green color vision deficiency. - Let CardTitle render any heading level through `as`, so cards no longer force an h2 into the document outline. Public API: - Namespace every custom property under --eb-, including the internal primitives, so nothing collides with consumer properties. The library declares them at :where(:root), where an app's own :root rule wins. - Rename primitives to describe their role: --eb-border-width, --eb-font-size-body, --eb-font-size-title, --eb-font-weight-*, --eb-line-height-*, --eb-color-scrim. - Split --eb-color-red-700, which served both error borders and chart series 3, into --eb-color-danger and --eb-chart-series-*. - Complete the spacing scale with --eb-size-5, -7, and -9, and promote the tooltip's inline shadow to --eb-shadow-raised-content. - Publish all 38 tokens as supported API with a documented compatibility policy, enforced by tests/tokens.test.ts. Packaging: - Move charts to a @byearlybird/components/charts entry so the core entry no longer pulls in Recharts and its transitive redux and d3 dependencies. Core bundle drops from 41.3kB to 30.5kB. - Converge the Combobox backdrop on the same scrim treatment Dialog and Drawer use. Release: - Add CI running vp check, the workspace tests, and the workspace build on push and pull request. - Document requirements, supported browsers, accessibility targets, and beta status; publish prereleases under the beta dist-tag. BREAKING CHANGE: chart imports move to @byearlybird/components/charts, and consumers referencing the old unprefixed tokens must adopt the --eb- names.
1 parent 15785fc commit 538e26e

29 files changed

Lines changed: 664 additions & 340 deletions

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
check:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
18+
- name: Install Vite+
19+
run: npm install -g vite-plus@0.2.6
20+
21+
- name: Install dependencies
22+
run: vp install --frozen-lockfile
23+
24+
- name: Install Playwright browsers
25+
run: vp exec playwright install --with-deps chromium
26+
27+
- name: Format, lint, and type check
28+
run: vp check
29+
30+
- name: Test
31+
run: vp run -r test
32+
33+
- name: Build
34+
run: vp run -r build

packages/components/README.md

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22

33
Reusable React components by Early Bird, built with TypeScript and CSS Modules.
44

5-
> [!WARNING]
6-
> **Status: Alpha.** The component library is under active development. Component APIs, styling,
7-
> and design tokens may change before 1.0.
5+
> [!NOTE]
6+
> **Status: Beta.** The public API is stabilizing. Breaking changes are still possible before 1.0,
7+
> but they will be called out in the changelog rather than shipped silently.
8+
9+
## Requirements
10+
11+
- **React** 19.2 or later (React 19 only).
12+
- **ESM only.** The package ships no CommonJS build.
13+
- **Browsers:** the current and previous release of Chrome, Edge, Firefox, and Safari. The styles
14+
rely on `color-mix()` and CSS custom properties, so browsers without them are unsupported.
15+
- **Accessibility target:** WCAG 2.2 Level AA. Interactive controls meet the 3:1 non-text contrast
16+
and 24×24px target-size minimums, and chart series meet 3:1 in distinct lightness tiers. Charts
17+
still separate series by color and legend rather than by shape or dash pattern, so keep the
18+
legend visible and do not rely on color as the sole cue in your own labeling.
819

920
## Install
1021

@@ -31,7 +42,6 @@ pnpm add @fontsource-variable/karla
3142
```tsx
3243
import "@fontsource-variable/karla";
3344
import {
34-
BarChart,
3545
Button,
3646
ButtonIcon,
3747
Checkbox,
@@ -49,12 +59,10 @@ import {
4959
InputAction,
5060
InputGroup,
5161
InputIcon,
52-
LineChart,
5362
Menu,
5463
MenuContent,
5564
MenuItem,
5665
MenuTrigger,
57-
PieChart,
5866
Radio,
5967
RadioGroup,
6068
Select,
@@ -175,6 +183,18 @@ adornments need to sit beside it. `SelectLeadingIcon` and
175183
`ComboboxLeadingIcon` provide the same leading-icon composition for selection
176184
controls. `Textarea` extends the native textarea props.
177185

186+
`CardTitle` renders an `h2` by default. Set `as` to the heading level that fits the
187+
surrounding document outline so cards do not break the page's heading order:
188+
189+
```tsx
190+
<Card>
191+
<CardHeader>
192+
<CardTitle as="h3">Recent activity</CardTitle>
193+
</CardHeader>
194+
<CardContent>...</CardContent>
195+
</Card>
196+
```
197+
178198
## Overlays
179199

180200
Dialog and Drawer include themed backdrops, surfaces, titles, descriptions, triggers,
@@ -227,8 +247,40 @@ the shared tokens can import the standalone entry:
227247
```
228248

229249
`@byearlybird/components/style.css` already includes these tokens, so apps using the
230-
components should import only the component stylesheet. The standalone token entry
231-
contains the public theme values and the Pollen primitives used by the library.
250+
components should import only the component stylesheet.
251+
252+
Every custom property the library defines is prefixed `--eb-` and every one of them is
253+
supported public API. Read them, compose them in `calc()`, or override them — they are
254+
meant to be built on, not just themed. Because the library declares them at
255+
`:where(:root)`, an application's own `:root` rule always wins without specificity tricks.
256+
257+
```css
258+
.my-toolbar {
259+
gap: var(--eb-size-2);
260+
padding: var(--eb-size-3) var(--eb-size-4);
261+
font-size: var(--eb-font-size-body);
262+
border-block-end: var(--eb-border-width) solid var(--eb-color-border);
263+
}
264+
```
265+
266+
### The token contract
267+
268+
| Group | Tokens |
269+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
270+
| Layout | `--eb-border-width`, `--eb-size-1``--eb-size-10` (each step is 4px) |
271+
| Typography | `--eb-font-family`, `--eb-font-size-body`, `--eb-font-size-title`, `--eb-font-weight-regular`, `--eb-font-weight-semibold`, `--eb-line-height-none`, `--eb-line-height-normal` |
272+
| Color | `--eb-color-primary`, `--eb-color-primary-text`, `--eb-color-background`, `--eb-color-text`, `--eb-color-text-muted`, `--eb-color-border`, `--eb-color-muted`, `--eb-color-danger`, `--eb-color-scrim` |
273+
| Charts | `--eb-chart-series-1`, `--eb-chart-series-2`, `--eb-chart-series-3` |
274+
| Shape | `--eb-radius-surface`, `--eb-radius-input`, `--eb-radius-button` |
275+
| Elevation | `--eb-shadow-raised-content`, `--eb-shadow-elevated-content` |
276+
| Motion | `--eb-motion-duration`, `--eb-motion-easing-standard`, `--eb-motion-easing-out` |
277+
278+
**Compatibility policy.** Adding a token is a minor release. Renaming or removing one, or
279+
changing what it means, is a breaking change and will appear in the changelog. Adjusting a
280+
token's _value_ — a shade, a duration — is a minor release, so pin the value yourself if
281+
your design depends on an exact number. The set is enforced by `tests/tokens.test.ts`,
282+
which fails the build if a token is dropped, renamed, or added without being documented
283+
here.
232284

233285
```css
234286
:root {
@@ -237,8 +289,10 @@ contains the public theme values and the Pollen primitives used by the library.
237289
--eb-color-background: #faf9f6;
238290
--eb-color-text: #252523;
239291
--eb-color-text-muted: color-mix(in srgb, var(--eb-color-text) 65%, transparent);
240-
--eb-color-border: #b8b5ad;
292+
--eb-color-border: #939088;
241293
--eb-color-muted: #e3e1da;
294+
--eb-color-danger: #b91c1c;
295+
--eb-color-scrim: #000;
242296
--eb-font-family: "Karla Variable", "Karla", ui-sans-serif, system-ui, sans-serif;
243297
--eb-radius-surface: 2px;
244298
--eb-radius-input: 9999px;
@@ -249,11 +303,22 @@ contains the public theme values and the Pollen primitives used by the library.
249303
}
250304
```
251305

306+
Use `--eb-color-danger` for error and destructive states; it is the border color the
307+
Input and Textarea error styles already use. `--eb-color-scrim` is the base color for
308+
modal backdrops and is intentionally black in both themes — compose it with `color-mix()`
309+
rather than overriding it to a theme color.
310+
252311
Use `--eb-color-text-muted` for secondary content such as timestamps,
253312
metadata, and supporting icons. It derives a translucent color from the active
254313
text color, so it follows both light and dark themes while preserving the color
255314
of the surface beneath it. Use `--eb-color-muted` for muted surfaces instead.
256315

316+
> [!IMPORTANT]
317+
> Because `--eb-color-text-muted` is translucent, its contrast depends on whatever sits
318+
> behind it. Against the default background it measures 4.77:1 in light mode and 5.89:1
319+
> in dark mode. If you override `--eb-color-text` or `--eb-color-background`, or place
320+
> muted text on a `--eb-color-muted` surface, re-check it against the 4.5:1 minimum.
321+
257322
The standard motion duration is the default for control feedback, visual-state
258323
changes, and larger surface or shape transitions. Pair it with the standard easing
259324
for general state changes, or use the out easing when movement or expansion should
@@ -296,9 +361,17 @@ tricks.
296361

297362
## Charts
298363

364+
Charts ship from a separate entry so applications that do not render charts never pull
365+
Recharts into their bundle:
366+
367+
```tsx
368+
import { BarChart, LineChart, PieChart } from "@byearlybird/components/charts";
369+
```
370+
299371
The library includes opinionated line, bar, and pie charts powered by Recharts. Each chart
300372
is responsive, supports light and dark themes, and uses the existing component color
301-
tokens by default.
373+
tokens by default. `@byearlybird/components/style.css` covers both entries, so there is no
374+
separate chart stylesheet to import.
302375

303376
```tsx
304377
const data = [
@@ -338,8 +411,27 @@ const data = [
338411
/>;
339412
```
340413

341-
Set a series `color` or pass a `colors` array to `PieChart` when a product-specific
342-
palette is needed. CSS color values, including application theme variables, are
414+
Series are colored from `--eb-chart-series-1` through `--eb-chart-series-3`, cycling if a
415+
chart has more series than that. Each one meets the 3:1 non-text contrast minimum against
416+
the default background in both themes, and the three sit in distinct lightness tiers so
417+
they stay separable in grayscale and for red-green color vision deficiency.
418+
419+
Override them to match a product palette:
420+
421+
```css
422+
:root {
423+
--eb-chart-series-1: #1d4ed8;
424+
--eb-chart-series-2: #0e7490;
425+
--eb-chart-series-3: #b45309;
426+
}
427+
```
428+
429+
If you do, keep each color at 3:1 or better against your chart background and vary
430+
lightness as well as hue — a palette that differs only by hue collapses for a
431+
meaningful share of users.
432+
433+
Set a series `color` or pass a `colors` array to `PieChart` when a single chart needs to
434+
depart from the palette. CSS color values, including application theme variables, are
343435
supported.
344436

345437
## Develop
@@ -367,11 +459,13 @@ The package build is written to `dist/`, and the static Storybook site is writte
367459

368460
## Publish
369461

370-
Authenticate with npm, choose a new version, then publish:
462+
Authenticate with npm, choose a new version, then publish under the `beta` dist-tag so
463+
prereleases never become `latest`:
371464

372465
```bash
373466
vp dlx bumpp
374-
pnpm publish
467+
pnpm publish --tag beta
375468
```
376469

377-
The package is configured for public scoped publishing.
470+
The package is configured for public scoped publishing. CI runs `vp check`, `vp test`,
471+
and the workspace build on every push and pull request; publishing stays manual.

packages/components/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
"types": "./dist/index.d.mts",
3535
"import": "./dist/index.mjs"
3636
},
37+
"./charts": {
38+
"types": "./dist/charts.d.mts",
39+
"import": "./dist/charts.mjs"
40+
},
3741
"./package.json": "./package.json",
3842
"./tokens.css": "./dist/tokens.css",
3943
"./style.css": "./dist/style.css"

packages/components/src/charts.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import "./styles/tokens.css";
2+
3+
export { BarChart } from "./components/Charts/BarChart.tsx";
4+
export type { BarChartProps } from "./components/Charts/BarChart.tsx";
5+
export type { ChartSeries, ChartValueFormatter } from "./components/Charts/Chart.types.ts";
6+
export { LineChart } from "./components/Charts/LineChart.tsx";
7+
export type { LineChartProps } from "./components/Charts/LineChart.tsx";
8+
export { PieChart } from "./components/Charts/PieChart.tsx";
9+
export type { PieChartProps } from "./components/Charts/PieChart.tsx";

packages/components/src/components/Button/Button.module.css

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
justify-content: center;
66
box-sizing: border-box;
77
max-width: 100%;
8-
min-height: var(--size-10);
9-
padding-inline: var(--size-4);
10-
gap: var(--size-2);
11-
border: var(--size-px) solid var(--eb-color-primary);
8+
min-height: var(--eb-size-10);
9+
padding-inline: var(--eb-size-4);
10+
gap: var(--eb-size-2);
11+
border: var(--eb-border-width) solid var(--eb-color-primary);
1212
border-radius: var(--eb-radius-button);
1313
font-family: var(--eb-font-family);
14-
font-size: var(--scale-00);
14+
font-size: var(--eb-font-size-body);
1515
font-style: normal;
16-
font-weight: var(--weight-semibold);
17-
line-height: var(--line-none);
16+
font-weight: var(--eb-font-weight-semibold);
17+
line-height: var(--eb-line-height-none);
1818
text-decoration: none;
1919
white-space: nowrap;
2020
cursor: default;
@@ -54,7 +54,7 @@
5454
}
5555

5656
.iconSize {
57-
width: var(--size-10);
57+
width: var(--eb-size-10);
5858
padding-inline: 0;
5959
aspect-ratio: 1;
6060
}

packages/components/src/components/Card/Card.module.css

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
box-sizing: border-box;
33
display: grid;
44
grid-template-columns: auto minmax(0, 1fr);
5-
border: var(--size-px) solid var(--eb-color-border);
5+
border: var(--eb-border-width) solid var(--eb-color-border);
66
border-radius: var(--eb-radius-surface);
77
overflow: hidden;
88
color: var(--eb-color-text);
99
background: var(--eb-color-background);
1010
font-family: var(--eb-font-family);
11-
font-size: var(--scale-00);
11+
font-size: var(--eb-font-size-body);
1212
font-style: normal;
13-
font-weight: var(--weight-regular);
14-
line-height: var(--line-normal);
13+
font-weight: var(--eb-font-weight-regular);
14+
line-height: var(--eb-line-height-normal);
1515
}
1616

1717
.card > :not(.image) {
@@ -20,26 +20,26 @@
2020

2121
.header {
2222
box-sizing: border-box;
23-
padding: var(--size-4) var(--size-4) 0;
23+
padding: var(--eb-size-4) var(--eb-size-4) 0;
2424
}
2525

2626
.title {
2727
margin: 0;
2828
color: var(--eb-color-text);
2929
font-family: var(--eb-font-family);
30-
font-size: var(--scale-2);
30+
font-size: var(--eb-font-size-title);
3131
font-style: normal;
32-
font-weight: var(--weight-semibold);
33-
line-height: var(--line-normal);
32+
font-weight: var(--eb-font-weight-semibold);
33+
line-height: var(--eb-line-height-normal);
3434
}
3535

3636
.content {
3737
box-sizing: border-box;
38-
padding: var(--size-4);
38+
padding: var(--eb-size-4);
3939
}
4040

4141
.header + .content {
42-
padding-top: var(--size-2);
42+
padding-top: var(--eb-size-2);
4343
}
4444

4545
.image {
@@ -48,8 +48,8 @@
4848
grid-column: 1;
4949
align-self: center;
5050
width: 100%;
51-
max-width: calc(var(--size-10) * 3);
52-
margin: var(--size-3) 0 var(--size-3) var(--size-3);
51+
max-width: calc(var(--eb-size-10) * 3);
52+
margin: var(--eb-size-3) 0 var(--eb-size-3) var(--eb-size-3);
5353
aspect-ratio: 1;
5454
border-radius: var(--eb-radius-surface);
5555
object-fit: cover;
@@ -59,12 +59,12 @@
5959
grid-row: 1;
6060
grid-column: 2;
6161
align-self: end;
62-
padding: var(--size-3) var(--size-3) 0;
62+
padding: var(--eb-size-3) var(--eb-size-3) 0;
6363
}
6464

6565
.image ~ .content {
6666
grid-row: 2;
6767
grid-column: 2;
6868
align-self: start;
69-
padding: var(--size-2) var(--size-3) var(--size-3);
69+
padding: var(--eb-size-2) var(--eb-size-3) var(--eb-size-3);
7070
}

packages/components/src/components/Card/Card.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const META = {
2323
},
2424
decorators: [
2525
(Story) => (
26-
<div style={{ width: "calc(var(--size-10) * 10)" }}>
26+
<div style={{ width: "calc(var(--eb-size-10) * 10)" }}>
2727
<Story />
2828
</div>
2929
),

0 commit comments

Comments
 (0)