@xuefx/ui-theme 1.0.0

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.
@@ -0,0 +1,57 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+
3
+ const ThemeContext = createContext(null);
4
+
5
+ /**
6
+ * Access the current theme object provided by the nearest ThemeProvider.
7
+ * Returns `null` when no ThemeProvider is mounted (i.e. using default :root styles).
8
+ */
9
+ export function useTheme() {
10
+ return useContext(ThemeContext);
11
+ }
12
+
13
+ /**
14
+ * Inject a custom theme into a component subtree.
15
+ *
16
+ * CSS custom properties are set as inline styles on a wrapper <div>,
17
+ * scoping them to the subtree. When no ThemeProvider is used, the global
18
+ * `:root` variables from `theme/styles.css` serve as the default.
19
+ *
20
+ * @example
21
+ * import { ThemeProvider, createTheme } from 'theme';
22
+ *
23
+ * const brandTheme = createTheme({
24
+ * colors: { primary: '142 76% 36%', 'primary-foreground': '0 0% 100%' },
25
+ * });
26
+ *
27
+ * function App() {
28
+ * return (
29
+ * <ThemeProvider theme={brandTheme}>
30
+ * <Button>Brand Button</Button>
31
+ * </ThemeProvider>
32
+ * );
33
+ * }
34
+ */
35
+ export function ThemeProvider({ theme, children }) {
36
+ const style = useMemo(() => {
37
+ const vars = {};
38
+ if (!theme) return vars;
39
+ if (theme.colors) {
40
+ for (const [key, value] of Object.entries(theme.colors)) {
41
+ vars[`--${key}`] = value;
42
+ }
43
+ }
44
+ if (theme.radius) {
45
+ vars['--radius'] = theme.radius;
46
+ }
47
+ return vars;
48
+ }, [theme]);
49
+
50
+ return (
51
+ <ThemeContext.Provider value={theme}>
52
+ <div style={style} data-theme>
53
+ {children}
54
+ </div>
55
+ </ThemeContext.Provider>
56
+ );
57
+ }
@@ -0,0 +1,30 @@
1
+ import { colors, borderRadius, spacing, fontSize } from './tokens.js';
2
+
3
+ const defaultTokens = { colors, borderRadius, spacing, fontSize, radius: '0.5rem' };
4
+
5
+ /**
6
+ * Create a theme object by merging user overrides with the default design tokens.
7
+ *
8
+ * @param {Object} userConfig
9
+ * @param {Object} [userConfig.colors] - Partial color token overrides (HSL strings)
10
+ * @param {Object} [userConfig.borderRadius]
11
+ * @param {Object} [userConfig.spacing]
12
+ * @param {Object} [userConfig.fontSize]
13
+ * @param {string} [userConfig.radius] - Base border radius (e.g. '0.75rem')
14
+ * @returns {Object} A complete theme object
15
+ *
16
+ * @example
17
+ * const brandTheme = createTheme({
18
+ * colors: { primary: '142 76% 36%', 'primary-foreground': '0 0% 100%' },
19
+ * radius: '0.75rem',
20
+ * });
21
+ */
22
+ export function createTheme(userConfig = {}) {
23
+ return {
24
+ colors: { ...defaultTokens.colors, ...userConfig.colors },
25
+ borderRadius: { ...defaultTokens.borderRadius, ...userConfig.borderRadius },
26
+ spacing: { ...defaultTokens.spacing, ...userConfig.spacing },
27
+ fontSize: { ...defaultTokens.fontSize, ...userConfig.fontSize },
28
+ radius: userConfig.radius || defaultTokens.radius,
29
+ };
30
+ }
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './tokens.js';
2
+ export { default as tailwindPreset } from './tailwind-preset.js';
3
+ export { createTheme } from './create-theme.js';
4
+ export { ThemeProvider, useTheme } from './ThemeProvider.jsx';
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@xuefx/ui-theme",
3
+ "version": "1.0.0",
4
+ "description": "Design tokens, Tailwind preset, and theming tools for @xuefx/ui",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "files": ["*.js", "*.jsx", "*.css"],
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./tokens": "./tokens.js",
11
+ "./tailwind-preset": "./tailwind-preset.js",
12
+ "./styles.css": "./styles.css",
13
+ "./create-theme": "./create-theme.js",
14
+ "./ThemeProvider": "./ThemeProvider.jsx"
15
+ },
16
+ "peerDependencies": {
17
+ "react": ">=19.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "react": "^19.1.0",
21
+ "react-dom": "^19.1.0"
22
+ },
23
+ "license": "ISC"
24
+ }
package/styles.css ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CSS custom properties for the React UI design system.
3
+ *
4
+ * Color tokens are stored as HSL channels (hue saturation lightness),
5
+ * without the `hsl()` wrapper. The Tailwind preset wraps them:
6
+ * e.g. `primary: 'hsl(var(--primary))'`
7
+ *
8
+ * This allows Tailwind's opacity modifiers to work:
9
+ * `bg-primary/50` → background-color: hsl(var(--primary) / 0.5)
10
+ */
11
+
12
+ :root {
13
+ --background: 0 0% 100%;
14
+ --foreground: 222.2 84% 4.9%;
15
+ --card: 0 0% 100%;
16
+ --card-foreground: 222.2 84% 4.9%;
17
+ --popover: 0 0% 100%;
18
+ --popover-foreground: 222.2 84% 4.9%;
19
+ --primary: 222.2 47.4% 11.2%;
20
+ --primary-foreground: 210 40% 98%;
21
+ --secondary: 210 40% 96.1%;
22
+ --secondary-foreground: 222.2 47.4% 11.2%;
23
+ --muted: 210 40% 96.1%;
24
+ --muted-foreground: 215.4 16.3% 46.9%;
25
+ --accent: 210 40% 96.1%;
26
+ --accent-foreground: 222.2 47.4% 11.2%;
27
+ --destructive: 0 84.2% 60.2%;
28
+ --destructive-foreground: 210 40% 98%;
29
+ --border: 214.3 31.8% 91.4%;
30
+ --input: 214.3 31.8% 91.4%;
31
+ --ring: 222.2 84% 4.9%;
32
+ --radius: 0.5rem;
33
+ }
34
+
35
+ .dark {
36
+ --background: 222.2 84% 4.9%;
37
+ --foreground: 210 40% 98%;
38
+ --card: 222.2 84% 4.9%;
39
+ --card-foreground: 210 40% 98%;
40
+ --popover: 222.2 84% 4.9%;
41
+ --popover-foreground: 210 40% 98%;
42
+ --primary: 210 40% 98%;
43
+ --primary-foreground: 222.2 47.4% 11.2%;
44
+ --secondary: 217.2 32.6% 17.5%;
45
+ --secondary-foreground: 210 40% 98%;
46
+ --muted: 217.2 32.6% 17.5%;
47
+ --muted-foreground: 215 20.2% 65.1%;
48
+ --accent: 217.2 32.6% 17.5%;
49
+ --accent-foreground: 210 40% 98%;
50
+ --destructive: 0 62.8% 30.6%;
51
+ --destructive-foreground: 210 40% 98%;
52
+ --border: 217.2 32.6% 17.5%;
53
+ --input: 217.2 32.6% 17.5%;
54
+ --ring: 212.7 26.8% 83.9%;
55
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tailwind CSS preset that maps CSS custom properties from styles.css
3
+ * into Tailwind's theme namespace.
4
+ *
5
+ * Usage in tailwind.config.js:
6
+ * import { tailwindPreset } from 'theme';
7
+ * export default { presets: [tailwindPreset], ... }
8
+ */
9
+
10
+ export default {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ background: 'hsl(var(--background))',
15
+ foreground: 'hsl(var(--foreground))',
16
+ card: {
17
+ DEFAULT: 'hsl(var(--card))',
18
+ foreground: 'hsl(var(--card-foreground))',
19
+ },
20
+ popover: {
21
+ DEFAULT: 'hsl(var(--popover))',
22
+ foreground: 'hsl(var(--popover-foreground))',
23
+ },
24
+ primary: {
25
+ DEFAULT: 'hsl(var(--primary))',
26
+ foreground: 'hsl(var(--primary-foreground))',
27
+ },
28
+ secondary: {
29
+ DEFAULT: 'hsl(var(--secondary))',
30
+ foreground: 'hsl(var(--secondary-foreground))',
31
+ },
32
+ muted: {
33
+ DEFAULT: 'hsl(var(--muted))',
34
+ foreground: 'hsl(var(--muted-foreground))',
35
+ },
36
+ accent: {
37
+ DEFAULT: 'hsl(var(--accent))',
38
+ foreground: 'hsl(var(--accent-foreground))',
39
+ },
40
+ destructive: {
41
+ DEFAULT: 'hsl(var(--destructive))',
42
+ foreground: 'hsl(var(--destructive-foreground))',
43
+ },
44
+ border: 'hsl(var(--border))',
45
+ input: 'hsl(var(--input))',
46
+ ring: 'hsl(var(--ring))',
47
+ },
48
+ borderRadius: {
49
+ lg: 'var(--radius)',
50
+ md: 'calc(var(--radius) - 2px)',
51
+ sm: 'calc(var(--radius) - 4px)',
52
+ },
53
+ },
54
+ },
55
+ };
package/tokens.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Design tokens for the React UI component library.
3
+ * These values are the source of truth; the Tailwind preset and CSS custom
4
+ * properties are derived from them.
5
+ *
6
+ * Color tokens use HSL channels as space-separated strings — the format
7
+ * Tailwind's `hsl(var(--name))` expects. No `hsl()` wrapper here.
8
+ */
9
+ export const colors = {
10
+ background: '0 0% 100%',
11
+ foreground: '222.2 84% 4.9%',
12
+ card: '0 0% 100%',
13
+ 'card-foreground': '222.2 84% 4.9%',
14
+ popover: '0 0% 100%',
15
+ 'popover-foreground': '222.2 84% 4.9%',
16
+ primary: '222.2 47.4% 11.2%',
17
+ 'primary-foreground': '210 40% 98%',
18
+ secondary: '210 40% 96.1%',
19
+ 'secondary-foreground': '222.2 47.4% 11.2%',
20
+ muted: '210 40% 96.1%',
21
+ 'muted-foreground': '215.4 16.3% 46.9%',
22
+ accent: '210 40% 96.1%',
23
+ 'accent-foreground': '222.2 47.4% 11.2%',
24
+ destructive: '0 84.2% 60.2%',
25
+ 'destructive-foreground': '210 40% 98%',
26
+ border: '214.3 31.8% 91.4%',
27
+ input: '214.3 31.8% 91.4%',
28
+ ring: '222.2 84% 4.9%',
29
+ };
30
+
31
+ export const borderRadius = {
32
+ lg: 'var(--radius)',
33
+ md: 'calc(var(--radius) - 2px)',
34
+ sm: 'calc(var(--radius) - 4px)',
35
+ };
36
+
37
+ export const spacing = {
38
+ 0: '0px',
39
+ 1: '0.25rem',
40
+ 2: '0.5rem',
41
+ 3: '0.75rem',
42
+ 4: '1rem',
43
+ 5: '1.25rem',
44
+ 6: '1.5rem',
45
+ 8: '2rem',
46
+ 10: '2.5rem',
47
+ 12: '3rem',
48
+ 16: '4rem',
49
+ 20: '5rem',
50
+ 24: '6rem',
51
+ 32: '8rem',
52
+ 40: '10rem',
53
+ 48: '12rem',
54
+ 56: '14rem',
55
+ 64: '16rem',
56
+ 72: '18rem',
57
+ 80: '20rem',
58
+ 96: '24rem',
59
+ };
60
+
61
+ export const fontSize = {
62
+ xs: ['0.75rem', { lineHeight: '1rem' }],
63
+ sm: ['0.875rem', { lineHeight: '1.25rem' }],
64
+ base: ['1rem', { lineHeight: '1.5rem' }],
65
+ lg: ['1.125rem', { lineHeight: '1.75rem' }],
66
+ xl: ['1.25rem', { lineHeight: '1.75rem' }],
67
+ '2xl': ['1.5rem', { lineHeight: '2rem' }],
68
+ '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
69
+ '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
70
+ };