@vinyasa/tokens 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @vinyasa/tokens
2
2
 
3
- The shared design-token and theming foundation for `@vinyasa/*` components: color, spacing, radius, typography, motion, focus rings, opacity, shadows, elevation, blur, and z-index tokens, plus static breakpoint/grid values and a `VinyasaProvider` that resolves and injects the themeable tokens at runtime.
3
+ The shared design-token and theming foundation for `@vinyasa/*` components: color, spacing, radius, border, typography, motion, focus rings, opacity, shadows, elevation, blur, and z-index tokens, plus static breakpoint/grid values and a `VinyasaProvider` that resolves and injects the themeable tokens at runtime.
4
+
5
+ For a live, click-to-copy reference of every single token value, run Storybook and open **Foundations** (`pnpm storybook`, then Colors / Elevation / Typography / Spacing / Brand / Layout). This README covers setup, integration, and the full prop/API surface instead of re-listing every value.
4
6
 
5
7
  ## Installation
6
8
 
@@ -28,65 +30,143 @@ function App() {
28
30
 
29
31
  ## How theming works
30
32
 
31
- Values are organized in three tiers:
33
+ There's no "primitives" tier — every file owns its own literal values directly, organized by what actually varies together:
34
+
35
+ 1. **Structural scales** (`spacing.ts`, `radius.ts`, `border.ts`, `shadow.ts`) — plain numeric/length scales that never change per theme or brand: the same `space1`–`space8`, `radiusXs`–`radiusFull`, `borderWidthThin`/`Thick`, and shadow shape everywhere. `themes/base.ts` pulls these in, adds typography/motion/opacity/blur/z-index (owned directly, since nothing else needs them), and defines the full `VinyasaTheme` interface every theme must satisfy.
36
+ 2. **Scheme** (`themes/light.ts`, `themes/dark.ts`) — the neutral gray ramp, surface, text, borders, shadows, and the info/success/warning/error semantic colors. A true neutral, not tinted toward any brand — switching brand never changes how "the page" looks, only the accent.
37
+ 3. **Brand** (`brands.ts`) — just the accent: `primary`/`onPrimary`/`focus`/`tertiary`/`onTertiary`, tuned separately for light and dark grounds and for normal/high contrast. `duskBrand` (default), `sunriseBrand`, and `yellowBrand` ship today; adding a new one is a new entry in `brands.ts` plus one line in `VinyasaProvider`'s internal lookup — no other file changes.
38
+ 4. **Semantic contract** (`themeContract`, exported as `vars`) — the flat set of CSS custom property names every `@vinyasa/*` component's own `.css.ts` file reads. Components never reach past this into scheme or brand files directly.
39
+ 5. **Runtime theme** — `VinyasaProvider` merges scheme + brand + density + your own `theme` override into one `VinyasaTheme` object and injects it as CSS custom properties at render time (via `@vanilla-extract/dynamic`), not baked into a static stylesheet. Overriding a value is a runtime prop, not a rebuild.
40
+
41
+ `breakpoints` and `grid` (from `layout.ts`) are the one exception to all of this — they're exported as plain static values, **not** part of `themeContract`. CSS custom properties can't appear inside `@media` query conditions (`@media (min-width: var(--x))` is invalid in every browser), so responsive breakpoints can never be runtime-themed the way colors or spacing can. Consume them as literals directly in your own component's build-time `@media` blocks.
42
+
43
+ ## `VinyasaProvider` props
44
+
45
+ | Prop | Type | Default | Description |
46
+ | ------------- | ------------------------------------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `children` | `ReactNode` | — | Required. |
48
+ | `colorScheme` | `'light' \| 'dark'` | `'light'` | Which scheme's neutrals/surface/text/borders/shadows/status colors to use. |
49
+ | `brand` | `'dusk' \| 'sunrise' \| 'yellow'` | `'dusk'` | Which accent (`primary`/`onPrimary`/`focus`/`tertiary`/`onTertiary`) to layer on top. |
50
+ | `contrast` | `'normal' \| 'high'` | `'normal'` | Swaps in WCAG-AAA-verified (7:1) shades for the tokens that need it at high contrast. |
51
+ | `density` | `'compact' \| 'comfortable' \| 'spacious'` | `'comfortable'` | Rescales `space1`–`space8` only — nothing else changes. |
52
+ | `theme` | `ThemeOverride` | `undefined` | `{ light?: { normal?, high? }, dark?: { normal?, high? } }` of `Partial<VinyasaTheme>` — scoped to the active `colorScheme`/`contrast`, see below. |
32
53
 
33
- 1. **Primitives** — raw, numeric-keyed scales with no meaning attached: `moss[700]`, `space[3]`. Two kinds, in two places: `primitives.ts` holds only what's genuinely shared across 2+ color schemes (`dusk`, `ember`/`saffron`/`moss` — reused as-is by both `light.ts` and `orange.ts` — plus the fully structural scales like `space`/`radius`/`fontFamily`, consumed via `base.ts`). A scheme's _exclusive_ palette (e.g. `light.ts`'s own `stone`/`paper`/`shadow`, `orange.ts`'s own `sand`/`sunrise`) lives directly in that scheme's own file under `src/themes/`, not centralized — nothing else references it, so centralizing it would just be an extra file to jump to.
34
- 2. **Semantic contract** (`themeContract`, exported from this package) — a flat, brand-role naming tier covering colors (`primary`/`onPrimary`, `secondary`/`onSecondary`, `tertiary`/`onTertiary`, `error`/`onError`, `focus`), spacing (`space1`–`space6`), radii, border width, typography (font family/size/weight/letter-spacing/line-height), motion (duration/easing), focus rings, opacity, shadows, elevation, blur, and z-index. This is what a component's own `.css.ts` file imports and references — never the primitives directly.
54
+ Resolution order (each step merges onto the previous, right-most wins): `resolveBaseTheme(colorScheme, contrast)` → brand accent for `(brand, colorScheme, contrast)` → density spacing overlay → the `theme[colorScheme][contrast]` branch of your `theme` prop, if you supplied one.
35
55
 
36
- `breakpoints` and `grid` (from `layout.ts`) are the one exception — they're exported as plain static values, **not** part of `themeContract`. CSS custom properties can't appear inside `@media` query conditions (`@media (min-width: var(--x))` is invalid in every browser), so responsive breakpoints can never be runtime-themed the way colors or spacing can. Consume them as literals directly in your own component's build-time `@media` blocks.
56
+ ### `colorScheme`
37
57
 
38
- 3. **Runtime theme** (`lightTheme`, `createTheme`, `VinyasaProvider`) — the actual values, injected as CSS custom properties at render time (via `@vanilla-extract/dynamic`), not baked into a static stylesheet. This is what makes overriding a value a runtime prop, not a rebuild.
58
+ ```tsx
59
+ <VinyasaProvider colorScheme="dark">
60
+ <YourApp />
61
+ </VinyasaProvider>
62
+ ```
39
63
 
40
- ### Overriding values
64
+ ### `brand`
41
65
 
42
- Pass a partial theme — only the keys you override change, everything else keeps its default:
66
+ Independent of `colorScheme` — any brand works with either scheme:
43
67
 
44
68
  ```tsx
45
- <VinyasaProvider theme={{ primary: '#7c3aed' }}>
69
+ <VinyasaProvider brand="sunrise">
70
+ <YourApp />
71
+ </VinyasaProvider>
72
+
73
+ <VinyasaProvider colorScheme="dark" brand="sunrise">
46
74
  <YourApp />
47
75
  </VinyasaProvider>
48
76
  ```
49
77
 
50
- You can also build a full custom theme ahead of time with `createTheme`:
78
+ ### `contrast`
51
79
 
52
80
  ```tsx
53
- import { createTheme, VinyasaProvider } from '@vinyasa/tokens';
81
+ <VinyasaProvider contrast="high">
82
+ <YourApp />
83
+ </VinyasaProvider>
84
+ ```
54
85
 
55
- const brandTheme = createTheme({ primary: '#7c3aed', onPrimary: '#ffffff' });
86
+ ### `density`
56
87
 
57
- <VinyasaProvider theme={brandTheme}>
88
+ ```tsx
89
+ <VinyasaProvider density="compact">
58
90
  <YourApp />
59
- </VinyasaProvider>;
91
+ </VinyasaProvider>
60
92
  ```
61
93
 
62
- Providers can be nested for scoped overrides — an inner `VinyasaProvider` only affects the subtree it wraps.
94
+ ### Combining props
63
95
 
64
- ### Reading the theme programmatically
96
+ All four combine freely — e.g. a dark, high-contrast, compact, sunrise-branded app:
65
97
 
66
98
  ```tsx
67
- import { useVinyasaTheme } from '@vinyasa/tokens';
99
+ <VinyasaProvider colorScheme="dark" brand="sunrise" contrast="high" density="compact">
100
+ <YourApp />
101
+ </VinyasaProvider>
102
+ ```
68
103
 
69
- function Component() {
70
- const theme = useVinyasaTheme();
71
- return <span style={{ color: theme.primary }}>...</span>;
72
- }
104
+ ### Nesting providers
105
+
106
+ An inner `VinyasaProvider` only affects the subtree it wraps — useful for a themed preview panel, an embedded widget, or a settings page previewing a different mode without affecting the rest of the app:
107
+
108
+ ```tsx
109
+ <VinyasaProvider>
110
+ <YourApp />
111
+ <VinyasaProvider colorScheme="dark">
112
+ <EmbeddedPreview />
113
+ </VinyasaProvider>
114
+ </VinyasaProvider>
73
115
  ```
74
116
 
75
- ### `colorScheme`, `contrast`, and `density`
117
+ ### Overriding individual values
118
+
119
+ `theme` is scoped by `colorScheme` and `contrast` — an override written under `light` is only ever applied when `colorScheme="light"` is also active, and never leaks into `dark` (or vice versa). Every level is optional; only fill in the branches you actually need:
76
120
 
77
121
  ```tsx
78
- <VinyasaProvider colorScheme="light" contrast="normal" density="comfortable">
122
+ <VinyasaProvider
123
+ theme={{
124
+ light: { normal: { primary: '#7c3aed' } },
125
+ dark: { normal: { primary: '#a78bfa' } },
126
+ }}
127
+ >
128
+ <YourApp />
129
+ </VinyasaProvider>
79
130
  ```
80
131
 
81
- - `colorScheme`: `'light'` (default) | `'dark'` | `'orange'` — three full peer themes, not "light/dark plus an orange accent." `orange` isn't a variant of light or dark; it has its own complete palette (surface, text, borders, shadow, and the brand color itself), warm throughout. `info`/`success`/`warning`/`error` stay on the same semantic hues (`dusk`/`moss`/`saffron`/`ember`) as every other theme — only the brand color and neutrals change per scheme.
82
- - `contrast`: `'normal'` (default) | `'high'` — every `colorScheme` × `contrast` combination is populated and WCAG-AAA-verified (7:1), not a placeholder; see `primitives.ts`'s and each `src/themes/*.ts` file's own comments for the exact ratios behind each override.
83
- - `density`: `'comfortable'` (default) | `'compact'` | `'spacious'`.
132
+ Only overriding one scheme is fine — the other keeps its normal resolved value:
84
133
 
85
- Each color scheme's values live in its own file under `src/themes/`: `base.ts` (the `VinyasaTheme` interface plus the structural scale — spacing/radius/type/motion — shared by every scheme), `light.ts`, `dark.ts`, `orange.ts`. Every export name is unique across these files (`lightTheme`/`darkTheme`/`orangeTheme`, `lightHighContrastTheme`/`darkHighContrastTheme`/`orangeHighContrastTheme`) — no two files export something with the same name. Adding a new scheme (e.g. `blue`) means a new `src/themes/blue.ts` spreading `base` and defining only the color/shadow keys that differ, one new entry in `src/themes.ts`'s matrix, and widening the `ColorScheme` union there — no other file needs to change.
134
+ ```tsx
135
+ <VinyasaProvider theme={{ light: { normal: { primary: '#7c3aed' } } }}>
136
+ <YourApp />
137
+ </VinyasaProvider>
138
+ ```
86
139
 
87
- `colorScheme`/`contrast` resolve together via `resolveBaseTheme`; `density` resolves separately via `resolveDensitySpacing` (it only ever varies the `space1`–`space6` keys, applied as an overlay after the base theme and before any explicit `theme` override) since color and spacing don't vary together. Requesting an unshipped combination falls back to `light`/`normal` with a console warning in development.
140
+ **To scope an override to one `brand`** (rather than one `colorScheme`), pick the override object yourself, the same way you already pick which `brand` to pass — there's no separate brand axis in `theme` itself, since you already hold that value directly:
88
141
 
89
- Storybook's own toolbar (`apps/storybook/.storybook/preview.tsx`) exposes all three `colorScheme` values as a global toggle, so any story can be browsed in any of them, not just the Foundations pages' own light/dark comparison.
142
+ ```tsx
143
+ const sunriseOverride = { light: { normal: { primary: '#ff5a2b' } } };
144
+
145
+ <VinyasaProvider brand={brand} theme={brand === 'sunrise' ? sunriseOverride : undefined}>
146
+ <YourApp />
147
+ </VinyasaProvider>;
148
+ ```
149
+
150
+ `createTheme` is a separate helper — it builds a complete, standalone `VinyasaTheme` object (based on `lightTheme`), not a `theme` prop value. Use it when you need a full theme object on its own (e.g. to pass to `ThemeContext` directly), not for scoped overrides:
151
+
152
+ ```tsx
153
+ import { createTheme } from '@vinyasa/tokens';
154
+
155
+ const customTheme = createTheme({ primary: '#7c3aed', onPrimary: '#ffffff' });
156
+ ```
157
+
158
+ ## Reading the theme programmatically
159
+
160
+ ```tsx
161
+ import { useVinyasaTheme } from '@vinyasa/tokens';
162
+
163
+ function Component() {
164
+ const theme = useVinyasaTheme();
165
+ return <span style={{ color: theme.primary }}>...</span>;
166
+ }
167
+ ```
168
+
169
+ Reads the resolved `VinyasaTheme` object from React context — the same values injected as CSS custom properties, available as plain strings for cases (canvas, SVG, inline `style` math) that can't take a `var(...)`.
90
170
 
91
171
  ## Consuming the contract in your own component
92
172
 
@@ -107,20 +187,72 @@ export const root = style({
107
187
 
108
188
  `pnpm create:package <name>` scaffolds new component packages wired to this contract by default.
109
189
 
110
- ## API
111
-
112
- | Export | Description |
113
- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
114
- | `VinyasaProvider` | React component. Props: `theme?`, `colorScheme?`, `contrast?`, `density?`, `children`. |
115
- | `useVinyasaTheme()` | Hook returning the currently resolved `VinyasaTheme` object. |
116
- | `themeContract` | The token contract — import as `vars` in a component's `.css.ts` file. |
117
- | `lightTheme`, `darkTheme`, `orangeTheme` | The normal-contrast values object for each color scheme. |
118
- | `lightHighContrastTheme`, `darkHighContrastTheme`, `orangeHighContrastTheme` | The high-contrast values object for each color scheme. |
119
- | `createTheme(overrides)` | Merges a partial theme onto `lightTheme`, returning a full `VinyasaTheme`. |
120
- | `resolveBaseTheme(colorScheme, contrast)` | Looks up the base theme for a scheme/contrast pair, with a dev-mode fallback warning. |
121
- | `resolveDensitySpacing(density)` | Looks up the `space1`–`space6` overlay for a density, with a dev-mode fallback warning. |
122
- | `breakpoints`, `grid` | Static layout values (not themeable — see above). From `layout.ts`. |
123
- | `VinyasaTheme`, `ColorScheme`, `Contrast`, `Density`, `VinyasaProviderProps` | Types. |
190
+ ### Wiring up the component's own ref
191
+
192
+ Any component that reads `themeContract` values on its own rendered node should use `useThemedRef` instead of a plain `useRef`/`forwardRef` ref — it merges the caller's own `ref` with an internal one used to dev-warn if the node ever renders outside a `VinyasaProvider` (e.g. via a portal that escapes its DOM subtree):
193
+
194
+ ```tsx
195
+ import { useThemedRef } from '@vinyasa/tokens';
196
+ import { forwardRef, type ComponentPropsWithoutRef } from 'react';
197
+
198
+ const YourComponent = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>((props, ref) => {
199
+ const themedRef = useThemedRef(ref);
200
+ return <div ref={themedRef} {...props} />;
201
+ });
202
+ ```
203
+
204
+ ### Focus rings
205
+
206
+ `focusRingStyle(vars)` returns the shared focus-visible style (a soft box-shadow halo, not a hard `outline` — outlines don't reliably follow `border-radius` across browsers) to spread into a component's own `:focus-visible` selector:
207
+
208
+ ```ts
209
+ import { recipe } from '@vanilla-extract/recipes';
210
+ import { focusRingStyle, themeContract as vars } from '@vinyasa/tokens';
211
+
212
+ export const button = recipe({
213
+ base: {
214
+ selectors: {
215
+ '&:focus-visible': focusRingStyle(vars),
216
+ },
217
+ },
218
+ });
219
+ ```
220
+
221
+ ### Breakpoints & grid
222
+
223
+ Static, not themeable — import and use directly in your own build-time styles:
224
+
225
+ ```ts
226
+ import { style } from '@vanilla-extract/css';
227
+ import { breakpoints } from '@vinyasa/tokens';
228
+
229
+ export const responsive = style({
230
+ '@media': {
231
+ [`(min-width: ${breakpoints.md})`]: {
232
+ flexDirection: 'row',
233
+ },
234
+ },
235
+ });
236
+ ```
237
+
238
+ ## Full API reference
239
+
240
+ | Export | Description |
241
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
242
+ | `VinyasaProvider` | React component. Props: `theme?`, `colorScheme?`, `brand?`, `contrast?`, `density?`, `children`. |
243
+ | `useVinyasaTheme()` | Hook returning the currently resolved `VinyasaTheme` object. |
244
+ | `useThemedRef(ref?)` | Hook merging a caller's ref with a dev-mode "rendered outside a provider" check. |
245
+ | `useAssertVinyasaProvider(ref)` | Lower-level dev-mode check `useThemedRef` builds on — rarely used directly. |
246
+ | `themeContract` | The token contract — import as `vars` in a component's `.css.ts` file. |
247
+ | `focusRingStyle(vars)` | Returns the shared `:focus-visible` style object. |
248
+ | `lightTheme`, `darkTheme` | The normal-contrast, default-brand values object for each scheme. |
249
+ | `lightHighContrastTheme`, `darkHighContrastTheme` | The high-contrast, default-brand values object for each scheme. |
250
+ | `createTheme(overrides)` | Merges a partial theme onto `lightTheme`, returning a full `VinyasaTheme`. |
251
+ | `resolveBaseTheme(colorScheme, contrast)` | Looks up the scheme theme for a `colorScheme`/`contrast` pair, with a dev-mode fallback warning. |
252
+ | `resolveDensitySpacing(density)` | Looks up the `space1`–`space8` overlay for a density, with a dev-mode fallback warning. |
253
+ | `duskBrand`, `sunriseBrand`, `yellowBrand` | The brand accent objects, each `{ light: { normal, high }, dark: { normal, high } }`. |
254
+ | `breakpoints`, `grid` | Static layout values (not themeable — see above). From `layout.ts`. |
255
+ | `VinyasaTheme`, `ColorScheme`, `Contrast`, `Density`, `BrandName`, `Brand`, `BrandAccent`, `ThemeOverride`, `VinyasaProviderProps` | Types. |
124
256
 
125
257
  ## Development
126
258
 
@@ -130,4 +262,5 @@ From the repository root:
130
262
  pnpm --filter @vinyasa/tokens build
131
263
  pnpm --filter @vinyasa/tokens test
132
264
  pnpm --filter @vinyasa/tokens lint
265
+ pnpm --filter @vinyasa/tokens typecheck
133
266
  ```