@pickle-ui/react 1.0.0-beta.7 → 1.0.0-beta.8

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
@@ -231,8 +231,8 @@ own:
231
231
  that does not use the components. It re-exports `@pickle-ui/tokens/theme.css`,
232
232
  which `@pickle-ui/react` installs as a dependency.
233
233
  - `@pickle-ui/react/base.css` holds everything else: animation and variant plugins,
234
- the `.dark` variant, the `Text` type scale, shared utilities, and element
235
- resets. Import it with a token set of your own instead of `theme.css`.
234
+ the `.dark` variant, the `Text` type scale (from
235
+ `@pickle-ui/tokens/typography.css`), shared utilities, and element resets. Import it with a token set of your own instead of `theme.css`.
236
236
 
237
237
  Tokens follow one naming rule: `--x` is a fill and `--x-foreground` is the text
238
238
  placed on that fill. Status colors (`destructive`, `info`, `success`,
@@ -273,6 +273,8 @@ and `Card.Description` keeps `text-sm`.
273
273
  `--primary-foreground`, `--ring`, and their sidebar counterparts) for both
274
274
  schemes from one color, picking a readable foreground automatically. Radius
275
275
  and fonts pass through. Everything else keeps the defaults from `theme.css`.
276
+ It comes from `@pickle-ui/tokens/theme`, so React Native apps derive the same
277
+ theme from the same color.
276
278
 
277
279
  ```ts
278
280
  import { createTheme, themeToCss } from '@pickle-ui/react/theme';
@@ -1,3 +1,3 @@
1
- export { createTheme, themeToCss } from '../lib/theme.js';
2
- export type { Theme, ThemeCssOptions, ThemeFonts, ThemeOptions, ThemeVariables, } from '../lib/theme.js';
1
+ export { createTheme, themeToCss } from '@pickle-ui/tokens/theme';
2
+ export type { Theme, ThemeCssOptions, ThemeFonts, ThemeOptions, ThemeVariables, } from '@pickle-ui/tokens/theme';
3
3
  //# sourceMappingURL=theme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/entries/theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EACV,KAAK,EACL,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/entries/theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EACV,KAAK,EACL,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,yBAAyB,CAAC"}
@@ -1,2 +1,2 @@
1
- export { createTheme, themeToCss } from '../lib/theme.js';
1
+ export { createTheme, themeToCss } from '@pickle-ui/tokens/theme';
2
2
  //# sourceMappingURL=theme.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/entries/theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/entries/theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pickle-ui/react",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.8",
4
4
  "type": "module",
5
5
  "description": "React component library built with Base UI, shadcn/ui, Tailwind CSS, and reui.",
6
6
  "main": "dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "culori": "^4.0.2",
57
57
  "tailwind-merge": "^3.6.0",
58
58
  "tw-animate-css": "^1.4.0",
59
- "@pickle-ui/tokens": "1.0.0-beta.6"
59
+ "@pickle-ui/tokens": "1.0.0-beta.7"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@fontsource-variable/geist": "5.2.9",
package/src/base.css CHANGED
@@ -6,6 +6,13 @@
6
6
 
7
7
  @import 'tw-animate-css';
8
8
 
9
+ /**
10
+ * Type scale behind `Text`'s variants and the `text-<variant>` utilities,
11
+ * shared with React Native. It comes from here rather than `theme.css`
12
+ * because the components need it whatever token set an app brings.
13
+ */
14
+ @import '@pickle-ui/tokens/typography.css';
15
+
9
16
  @source './components/**/*.{ts,tsx}';
10
17
 
11
18
  /**
@@ -52,31 +59,6 @@
52
59
 
53
60
  @custom-variant dark (&:where(.dark, .dark *));
54
61
 
55
- /**
56
- * Type scale behind `Text`'s variants and the `text-<variant>` utilities.
57
- * It lives here rather than in `theme.css` because the components need it
58
- * whatever token set an app brings. Not inlined, so each size stays a CSS
59
- * variable an app can override from `:root` or its own `@theme`.
60
- */
61
- @theme {
62
- --text-h1: 1.875rem;
63
- --text-h1--line-height: calc(2.25 / 1.875);
64
- --text-h2: 1.5rem;
65
- --text-h2--line-height: calc(2 / 1.5);
66
- --text-h3: 1.25rem;
67
- --text-h3--line-height: calc(1.75 / 1.25);
68
- --text-h4: 1.125rem;
69
- --text-h4--line-height: calc(1.75 / 1.125);
70
- --text-lead: 1rem;
71
- --text-lead--line-height: 1.5;
72
- --text-body: 0.875rem;
73
- --text-body--line-height: 1.5;
74
- --text-small: 0.75rem;
75
- --text-small--line-height: 1;
76
- --text-code: 0.75rem;
77
- --text-code--line-height: calc(1 / 0.75);
78
- }
79
-
80
62
  /**
81
63
  * Shared focus-visible ring for components that opt into focus-ring.
82
64
  */
@@ -1,8 +1,8 @@
1
- export { createTheme, themeToCss } from '../lib/theme';
1
+ export { createTheme, themeToCss } from '@pickle-ui/tokens/theme';
2
2
  export type {
3
3
  Theme,
4
4
  ThemeCssOptions,
5
5
  ThemeFonts,
6
6
  ThemeOptions,
7
7
  ThemeVariables,
8
- } from '../lib/theme';
8
+ } from '@pickle-ui/tokens/theme';
@@ -1,40 +0,0 @@
1
- /**
2
- * Theme generator: derives Pickle's brand-dependent tokens from a single
3
- * primary color so consumers can theme the library without hand-writing CSS
4
- * variables. Tokens that do not depend on the brand (neutrals, status colors)
5
- * keep the values from `@pickle-ui/react/theme.css`.
6
- */
7
- type Theme = {
8
- dark: ThemeVariables;
9
- light: ThemeVariables;
10
- };
11
- type ThemeCssOptions = {
12
- darkSelector?: string;
13
- lightSelector?: string;
14
- };
15
- type ThemeFonts = {
16
- heading?: string;
17
- mono?: string;
18
- sans?: string;
19
- };
20
- type ThemeOptions = {
21
- /** Font stacks, written to `--font-sans`, `--font-heading` and `--font-mono`. */
22
- fonts?: ThemeFonts;
23
- /** Brand color in any CSS notation (hex, rgb, hsl, oklch, …). */
24
- primary: string;
25
- /** Brand color for the dark scheme. Defaults to a lighter version of `primary`. */
26
- primaryDark?: string;
27
- /** Base radius, e.g. `'0.5rem'`. Every `rounded-*` scale derives from it. */
28
- radius?: string;
29
- };
30
- type ThemeVariables = Record<`--${string}`, string>;
31
- /**
32
- * Build the light and dark token sets for a brand color. Apply the result
33
- * with `themeToCss` in a stylesheet, or spread `theme.light` / `theme.dark`
34
- * as the `style` of a wrapper element.
35
- */
36
- export declare function createTheme(options: ThemeOptions): Theme;
37
- /** Serialize a theme as CSS rules for the light and dark selectors. */
38
- export declare function themeToCss(theme: Theme, { darkSelector, lightSelector }?: ThemeCssOptions): string;
39
- export type { Theme, ThemeCssOptions, ThemeFonts, ThemeOptions, ThemeVariables, };
40
- //# sourceMappingURL=theme.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/lib/theme.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,iFAAiF;IACjF,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,cAAc,GAAG,MAAM,CAAC,KAAK,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;AAOpD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAWxD;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CACxB,KAAK,EAAE,KAAK,EACZ,EAAE,YAAsB,EAAE,aAAuB,EAAE,GAAE,eAAoB,GACxE,MAAM,CAOR;AAyDD,YAAY,EACV,KAAK,EACL,eAAe,EACf,UAAU,EACV,YAAY,EACZ,cAAc,GACf,CAAC"}
package/dist/lib/theme.js DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Theme generator: derives Pickle's brand-dependent tokens from a single
3
- * primary color so consumers can theme the library without hand-writing CSS
4
- * variables. Tokens that do not depend on the brand (neutrals, status colors)
5
- * keep the values from `@pickle-ui/react/theme.css`.
6
- */
7
- import { clampChroma, converter, formatCss, parse, wcagContrast, } from 'culori';
8
- const toOklch = converter('oklch');
9
- const LIGHT_FOREGROUND = { c: 0, h: 0, l: 1, mode: 'oklch' };
10
- const DARK_FOREGROUND = { c: 0.006, h: 285.9, l: 0.21, mode: 'oklch' };
11
- /**
12
- * Build the light and dark token sets for a brand color. Apply the result
13
- * with `themeToCss` in a stylesheet, or spread `theme.light` / `theme.dark`
14
- * as the `style` of a wrapper element.
15
- */
16
- export function createTheme(options) {
17
- const primary = parseColor(options.primary, 'primary');
18
- const primaryDark = options.primaryDark
19
- ? parseColor(options.primaryDark, 'primaryDark')
20
- : lightenForDark(primary);
21
- const shared = sharedVariables(options);
22
- return {
23
- dark: { ...shared, ...brandVariables(primaryDark) },
24
- light: { ...shared, ...brandVariables(primary) },
25
- };
26
- }
27
- /** Serialize a theme as CSS rules for the light and dark selectors. */
28
- export function themeToCss(theme, { darkSelector = '.dark', lightSelector = ':root' } = {}) {
29
- const block = (selector, variables) => `${selector} {\n${Object.entries(variables)
30
- .map(([name, value]) => ` ${name}: ${value};`)
31
- .join('\n')}\n}`;
32
- return `${block(lightSelector, theme.light)}\n\n${block(darkSelector, theme.dark)}\n`;
33
- }
34
- function brandVariables(primary) {
35
- const foreground = foregroundFor(primary);
36
- return {
37
- '--primary': format(primary),
38
- '--primary-foreground': format(foreground),
39
- '--ring': format({ ...primary, c: primary.c * 0.6 }),
40
- '--sidebar-primary': format(primary),
41
- '--sidebar-primary-foreground': format(foreground),
42
- '--sidebar-ring': format({ ...primary, c: primary.c * 0.6 }),
43
- };
44
- }
45
- /** White or near-black, whichever reads better on the given fill. */
46
- function foregroundFor(fill) {
47
- return wcagContrast(fill, LIGHT_FOREGROUND) >=
48
- wcagContrast(fill, DARK_FOREGROUND)
49
- ? LIGHT_FOREGROUND
50
- : DARK_FOREGROUND;
51
- }
52
- function format(color) {
53
- return formatCss(clampChroma(color, 'oklch'));
54
- }
55
- /** Lift a brand color so it stays vivid on dark surfaces. */
56
- function lightenForDark(color) {
57
- return {
58
- ...color,
59
- c: color.c * 0.92,
60
- l: Math.min(Math.max(color.l + 0.12, 0.6), 0.85),
61
- };
62
- }
63
- function parseColor(value, name) {
64
- const color = parse(value);
65
- if (!color) {
66
- throw new Error(`createTheme: ${name} is not a valid CSS color: ${value}`);
67
- }
68
- return toOklch(color);
69
- }
70
- function sharedVariables({ fonts, radius }) {
71
- const variables = {};
72
- if (radius)
73
- variables['--radius'] = radius;
74
- if (fonts?.sans)
75
- variables['--font-sans'] = fonts.sans;
76
- if (fonts?.heading)
77
- variables['--font-heading'] = fonts.heading;
78
- if (fonts?.mono)
79
- variables['--font-mono'] = fonts.mono;
80
- return variables;
81
- }
82
- //# sourceMappingURL=theme.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/lib/theme.ts"],"names":[],"mappings":"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"}
@@ -1,64 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { createTheme, themeToCss } from './theme';
4
-
5
- describe('createTheme', () => {
6
- it('derives primary tokens for both schemes', () => {
7
- const theme = createTheme({ primary: '#16a34a' });
8
-
9
- expect(theme.light['--primary']).toMatch(/^oklch\(/);
10
- expect(theme.dark['--primary']).toMatch(/^oklch\(/);
11
- expect(theme.light['--primary']).not.toBe(theme.dark['--primary']);
12
- expect(theme.light['--sidebar-primary']).toBe(theme.light['--primary']);
13
- });
14
-
15
- it('picks a readable foreground for light and dark fills', () => {
16
- const onDarkFill = createTheme({ primary: '#1e3a8a' });
17
- const onLightFill = createTheme({ primary: '#fde047' });
18
-
19
- expect(onDarkFill.light['--primary-foreground']).toBe('oklch(1 0 0)');
20
- expect(onLightFill.light['--primary-foreground']).not.toBe('oklch(1 0 0)');
21
- });
22
-
23
- it('uses primaryDark as given and passes radius and fonts through', () => {
24
- const theme = createTheme({
25
- fonts: { mono: 'Menlo', sans: 'Inter' },
26
- primary: 'rebeccapurple',
27
- primaryDark: 'oklch(0.8 0.1 300)',
28
- radius: '1rem',
29
- });
30
-
31
- expect(theme.dark['--primary']).toBe('oklch(0.8 0.1 300)');
32
- expect(theme.light['--radius']).toBe('1rem');
33
- expect(theme.dark['--radius']).toBe('1rem');
34
- expect(theme.light['--font-sans']).toBe('Inter');
35
- expect(theme.light['--font-mono']).toBe('Menlo');
36
- expect(theme.light['--font-heading']).toBeUndefined();
37
- });
38
-
39
- it('rejects colors it cannot parse', () => {
40
- expect(() => createTheme({ primary: 'not-a-color' })).toThrow(
41
- /not a valid CSS color/,
42
- );
43
- });
44
- });
45
-
46
- describe('themeToCss', () => {
47
- it('serializes both schemes under their selectors', () => {
48
- const css = themeToCss(createTheme({ primary: '#000', radius: '4px' }));
49
-
50
- expect(css).toMatch(/^:root \{\n {2}--radius: 4px;\n/);
51
- expect(css).toContain('\n.dark {\n');
52
- expect(css).toContain('--primary-foreground: oklch(1 0 0);');
53
- });
54
-
55
- it('accepts custom selectors', () => {
56
- const css = themeToCss(createTheme({ primary: '#000' }), {
57
- darkSelector: '[data-theme="dark"]',
58
- lightSelector: 'html',
59
- });
60
-
61
- expect(css.startsWith('html {')).toBe(true);
62
- expect(css).toContain('[data-theme="dark"] {');
63
- });
64
- });
package/src/lib/theme.ts DELETED
@@ -1,143 +0,0 @@
1
- /**
2
- * Theme generator: derives Pickle's brand-dependent tokens from a single
3
- * primary color so consumers can theme the library without hand-writing CSS
4
- * variables. Tokens that do not depend on the brand (neutrals, status colors)
5
- * keep the values from `@pickle-ui/react/theme.css`.
6
- */
7
-
8
- import {
9
- clampChroma,
10
- converter,
11
- formatCss,
12
- type Oklch,
13
- parse,
14
- wcagContrast,
15
- } from 'culori';
16
-
17
- type Theme = {
18
- dark: ThemeVariables;
19
- light: ThemeVariables;
20
- };
21
-
22
- type ThemeCssOptions = {
23
- darkSelector?: string;
24
- lightSelector?: string;
25
- };
26
-
27
- type ThemeFonts = {
28
- heading?: string;
29
- mono?: string;
30
- sans?: string;
31
- };
32
-
33
- type ThemeOptions = {
34
- /** Font stacks, written to `--font-sans`, `--font-heading` and `--font-mono`. */
35
- fonts?: ThemeFonts;
36
- /** Brand color in any CSS notation (hex, rgb, hsl, oklch, …). */
37
- primary: string;
38
- /** Brand color for the dark scheme. Defaults to a lighter version of `primary`. */
39
- primaryDark?: string;
40
- /** Base radius, e.g. `'0.5rem'`. Every `rounded-*` scale derives from it. */
41
- radius?: string;
42
- };
43
-
44
- type ThemeVariables = Record<`--${string}`, string>;
45
-
46
- const toOklch = converter('oklch');
47
-
48
- const LIGHT_FOREGROUND: Oklch = { c: 0, h: 0, l: 1, mode: 'oklch' };
49
- const DARK_FOREGROUND: Oklch = { c: 0.006, h: 285.9, l: 0.21, mode: 'oklch' };
50
-
51
- /**
52
- * Build the light and dark token sets for a brand color. Apply the result
53
- * with `themeToCss` in a stylesheet, or spread `theme.light` / `theme.dark`
54
- * as the `style` of a wrapper element.
55
- */
56
- export function createTheme(options: ThemeOptions): Theme {
57
- const primary = parseColor(options.primary, 'primary');
58
- const primaryDark = options.primaryDark
59
- ? parseColor(options.primaryDark, 'primaryDark')
60
- : lightenForDark(primary);
61
- const shared = sharedVariables(options);
62
-
63
- return {
64
- dark: { ...shared, ...brandVariables(primaryDark) },
65
- light: { ...shared, ...brandVariables(primary) },
66
- };
67
- }
68
-
69
- /** Serialize a theme as CSS rules for the light and dark selectors. */
70
- export function themeToCss(
71
- theme: Theme,
72
- { darkSelector = '.dark', lightSelector = ':root' }: ThemeCssOptions = {},
73
- ): string {
74
- const block = (selector: string, variables: ThemeVariables) =>
75
- `${selector} {\n${Object.entries(variables)
76
- .map(([name, value]) => ` ${name}: ${value};`)
77
- .join('\n')}\n}`;
78
-
79
- return `${block(lightSelector, theme.light)}\n\n${block(darkSelector, theme.dark)}\n`;
80
- }
81
-
82
- function brandVariables(primary: Oklch): ThemeVariables {
83
- const foreground = foregroundFor(primary);
84
-
85
- return {
86
- '--primary': format(primary),
87
- '--primary-foreground': format(foreground),
88
- '--ring': format({ ...primary, c: primary.c * 0.6 }),
89
- '--sidebar-primary': format(primary),
90
- '--sidebar-primary-foreground': format(foreground),
91
- '--sidebar-ring': format({ ...primary, c: primary.c * 0.6 }),
92
- };
93
- }
94
-
95
- /** White or near-black, whichever reads better on the given fill. */
96
- function foregroundFor(fill: Oklch) {
97
- return wcagContrast(fill, LIGHT_FOREGROUND) >=
98
- wcagContrast(fill, DARK_FOREGROUND)
99
- ? LIGHT_FOREGROUND
100
- : DARK_FOREGROUND;
101
- }
102
-
103
- function format(color: Oklch) {
104
- return formatCss(clampChroma(color, 'oklch'));
105
- }
106
-
107
- /** Lift a brand color so it stays vivid on dark surfaces. */
108
- function lightenForDark(color: Oklch): Oklch {
109
- return {
110
- ...color,
111
- c: color.c * 0.92,
112
- l: Math.min(Math.max(color.l + 0.12, 0.6), 0.85),
113
- };
114
- }
115
-
116
- function parseColor(value: string, name: string): Oklch {
117
- const color = parse(value);
118
-
119
- if (!color) {
120
- throw new Error(`createTheme: ${name} is not a valid CSS color: ${value}`);
121
- }
122
-
123
- return toOklch(color);
124
- }
125
-
126
- function sharedVariables({ fonts, radius }: ThemeOptions): ThemeVariables {
127
- const variables: ThemeVariables = {};
128
-
129
- if (radius) variables['--radius'] = radius;
130
- if (fonts?.sans) variables['--font-sans'] = fonts.sans;
131
- if (fonts?.heading) variables['--font-heading'] = fonts.heading;
132
- if (fonts?.mono) variables['--font-mono'] = fonts.mono;
133
-
134
- return variables;
135
- }
136
-
137
- export type {
138
- Theme,
139
- ThemeCssOptions,
140
- ThemeFonts,
141
- ThemeOptions,
142
- ThemeVariables,
143
- };