@metacells/mcellui-core 0.1.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,341 @@
1
+ /**
2
+ * Theme Presets
3
+ *
4
+ * Pre-designed color schemes for instant brand alignment.
5
+ * Each preset defines primary colors and derives a full semantic palette.
6
+ *
7
+ * Usage:
8
+ * ```tsx
9
+ * <ThemeProvider theme="rose">
10
+ * <ThemeProvider theme="violet">
11
+ * ```
12
+ */
13
+
14
+ import { ThemeColors } from './colors';
15
+
16
+ // =============================================================================
17
+ // Color Palettes (Tailwind-inspired)
18
+ // =============================================================================
19
+
20
+ const palettes = {
21
+ zinc: {
22
+ 50: '#fafafa',
23
+ 100: '#f4f4f5',
24
+ 200: '#e4e4e7',
25
+ 300: '#d4d4d8',
26
+ 400: '#a1a1aa',
27
+ 500: '#71717a',
28
+ 600: '#52525b',
29
+ 700: '#3f3f46',
30
+ 800: '#27272a',
31
+ 900: '#18181b',
32
+ 950: '#09090b',
33
+ },
34
+ slate: {
35
+ 50: '#f8fafc',
36
+ 100: '#f1f5f9',
37
+ 200: '#e2e8f0',
38
+ 300: '#cbd5e1',
39
+ 400: '#94a3b8',
40
+ 500: '#64748b',
41
+ 600: '#475569',
42
+ 700: '#334155',
43
+ 800: '#1e293b',
44
+ 900: '#0f172a',
45
+ 950: '#020617',
46
+ },
47
+ stone: {
48
+ 50: '#fafaf9',
49
+ 100: '#f5f5f4',
50
+ 200: '#e7e5e4',
51
+ 300: '#d6d3d1',
52
+ 400: '#a8a29e',
53
+ 500: '#78716c',
54
+ 600: '#57534e',
55
+ 700: '#44403c',
56
+ 800: '#292524',
57
+ 900: '#1c1917',
58
+ 950: '#0c0a09',
59
+ },
60
+ blue: {
61
+ 50: '#eff6ff',
62
+ 100: '#dbeafe',
63
+ 200: '#bfdbfe',
64
+ 300: '#93c5fd',
65
+ 400: '#60a5fa',
66
+ 500: '#3b82f6',
67
+ 600: '#2563eb',
68
+ 700: '#1d4ed8',
69
+ 800: '#1e40af',
70
+ 900: '#1e3a8a',
71
+ 950: '#172554',
72
+ },
73
+ green: {
74
+ 50: '#f0fdf4',
75
+ 100: '#dcfce7',
76
+ 200: '#bbf7d0',
77
+ 300: '#86efac',
78
+ 400: '#4ade80',
79
+ 500: '#22c55e',
80
+ 600: '#16a34a',
81
+ 700: '#15803d',
82
+ 800: '#166534',
83
+ 900: '#14532d',
84
+ 950: '#052e16',
85
+ },
86
+ rose: {
87
+ 50: '#fff1f2',
88
+ 100: '#ffe4e6',
89
+ 200: '#fecdd3',
90
+ 300: '#fda4af',
91
+ 400: '#fb7185',
92
+ 500: '#f43f5e',
93
+ 600: '#e11d48',
94
+ 700: '#be123c',
95
+ 800: '#9f1239',
96
+ 900: '#881337',
97
+ 950: '#4c0519',
98
+ },
99
+ orange: {
100
+ 50: '#fff7ed',
101
+ 100: '#ffedd5',
102
+ 200: '#fed7aa',
103
+ 300: '#fdba74',
104
+ 400: '#fb923c',
105
+ 500: '#f97316',
106
+ 600: '#ea580c',
107
+ 700: '#c2410c',
108
+ 800: '#9a3412',
109
+ 900: '#7c2d12',
110
+ 950: '#431407',
111
+ },
112
+ violet: {
113
+ 50: '#f5f3ff',
114
+ 100: '#ede9fe',
115
+ 200: '#ddd6fe',
116
+ 300: '#c4b5fd',
117
+ 400: '#a78bfa',
118
+ 500: '#8b5cf6',
119
+ 600: '#7c3aed',
120
+ 700: '#6d28d9',
121
+ 800: '#5b21b6',
122
+ 900: '#4c1d95',
123
+ 950: '#2e1065',
124
+ },
125
+ } as const;
126
+
127
+ // Neutral palette (used for backgrounds, text, borders)
128
+ const neutral = {
129
+ 0: '#ffffff',
130
+ 50: '#fafafa',
131
+ 100: '#f5f5f5',
132
+ 200: '#e5e5e5',
133
+ 300: '#d4d4d4',
134
+ 400: '#a3a3a3',
135
+ 500: '#737373',
136
+ 600: '#525252',
137
+ 700: '#404040',
138
+ 800: '#262626',
139
+ 900: '#171717',
140
+ 950: '#0a0a0a',
141
+ } as const;
142
+
143
+ // =============================================================================
144
+ // Theme Preset Types
145
+ // =============================================================================
146
+
147
+ export type ThemePreset = 'zinc' | 'slate' | 'stone' | 'blue' | 'green' | 'rose' | 'orange' | 'violet';
148
+
149
+ export interface PresetColors {
150
+ light: ThemeColors;
151
+ dark: ThemeColors;
152
+ }
153
+
154
+ // =============================================================================
155
+ // Preset Generation
156
+ // =============================================================================
157
+
158
+ type ColorPalette = {
159
+ 50: string;
160
+ 100: string;
161
+ 200: string;
162
+ 300: string;
163
+ 400: string;
164
+ 500: string;
165
+ 600: string;
166
+ 700: string;
167
+ 800: string;
168
+ 900: string;
169
+ 950: string;
170
+ };
171
+
172
+ /**
173
+ * Creates a complete color scheme from a primary color palette.
174
+ */
175
+ function createPresetColors(
176
+ primaryPalette: ColorPalette,
177
+ neutralPalette: ColorPalette = palettes.zinc
178
+ ): PresetColors {
179
+ return {
180
+ light: {
181
+ // Backgrounds
182
+ background: neutral[0],
183
+ backgroundSubtle: neutralPalette[50],
184
+ backgroundMuted: neutralPalette[100],
185
+ backgroundElevated: neutral[0],
186
+
187
+ // Foreground/Text
188
+ foreground: neutralPalette[900],
189
+ foregroundMuted: neutralPalette[500],
190
+ foregroundSubtle: neutralPalette[400],
191
+
192
+ // Borders
193
+ border: neutralPalette[200],
194
+ borderMuted: neutralPalette[100],
195
+ borderFocused: primaryPalette[500],
196
+
197
+ // Primary
198
+ primary: primaryPalette[500],
199
+ primaryForeground: neutral[0],
200
+ primaryMuted: primaryPalette[100],
201
+
202
+ // Secondary
203
+ secondary: neutralPalette[100],
204
+ secondaryForeground: neutralPalette[900],
205
+
206
+ // Destructive (always red)
207
+ destructive: '#ef4444',
208
+ destructiveForeground: neutral[0],
209
+
210
+ // Success (always green)
211
+ success: '#22c55e',
212
+ successForeground: neutral[0],
213
+ successMuted: '#dcfce7',
214
+
215
+ // Warning (always amber)
216
+ warning: '#f59e0b',
217
+ warningForeground: neutral[900],
218
+ warningMuted: '#fef3c7',
219
+
220
+ // Error (always red)
221
+ error: '#ef4444',
222
+ errorForeground: neutral[0],
223
+ errorMuted: '#fee2e2',
224
+
225
+ // Card
226
+ card: neutral[0],
227
+ cardForeground: neutralPalette[900],
228
+
229
+ // Input
230
+ input: neutral[0],
231
+ inputBorder: neutralPalette[200],
232
+ inputPlaceholder: neutralPalette[400],
233
+
234
+ // Overlay
235
+ overlay: 'rgba(0, 0, 0, 0.5)',
236
+ scrim: 'rgba(0, 0, 0, 0.3)',
237
+ },
238
+ dark: {
239
+ // Backgrounds
240
+ background: neutralPalette[950],
241
+ backgroundSubtle: neutralPalette[900],
242
+ backgroundMuted: neutralPalette[800],
243
+ backgroundElevated: neutralPalette[900],
244
+
245
+ // Foreground/Text
246
+ foreground: neutralPalette[50],
247
+ foregroundMuted: neutralPalette[400],
248
+ foregroundSubtle: neutralPalette[500],
249
+
250
+ // Borders
251
+ border: neutralPalette[800],
252
+ borderMuted: neutralPalette[900],
253
+ borderFocused: primaryPalette[400],
254
+
255
+ // Primary
256
+ primary: primaryPalette[400],
257
+ primaryForeground: neutralPalette[900],
258
+ primaryMuted: primaryPalette[900],
259
+
260
+ // Secondary
261
+ secondary: neutralPalette[800],
262
+ secondaryForeground: neutralPalette[50],
263
+
264
+ // Destructive
265
+ destructive: '#ef4444',
266
+ destructiveForeground: neutral[0],
267
+
268
+ // Success
269
+ success: '#4ade80',
270
+ successForeground: neutral[900],
271
+ successMuted: '#14532d',
272
+
273
+ // Warning
274
+ warning: '#fbbf24',
275
+ warningForeground: neutral[900],
276
+ warningMuted: '#78350f',
277
+
278
+ // Error
279
+ error: '#f87171',
280
+ errorForeground: neutral[0],
281
+ errorMuted: '#7f1d1d',
282
+
283
+ // Card
284
+ card: neutralPalette[900],
285
+ cardForeground: neutralPalette[50],
286
+
287
+ // Input
288
+ input: neutralPalette[800],
289
+ inputBorder: neutralPalette[700],
290
+ inputPlaceholder: neutralPalette[500],
291
+
292
+ // Overlay
293
+ overlay: 'rgba(0, 0, 0, 0.7)',
294
+ scrim: 'rgba(0, 0, 0, 0.5)',
295
+ },
296
+ };
297
+ }
298
+
299
+ // =============================================================================
300
+ // Theme Presets
301
+ // =============================================================================
302
+
303
+ /**
304
+ * All available theme presets.
305
+ */
306
+ export const themePresets: Record<ThemePreset, PresetColors> = {
307
+ // Neutral presets (use their own palette as neutral base)
308
+ zinc: createPresetColors(palettes.zinc, palettes.zinc),
309
+ slate: createPresetColors(palettes.slate, palettes.slate),
310
+ stone: createPresetColors(palettes.stone, palettes.stone),
311
+
312
+ // Color presets (use zinc as neutral base)
313
+ blue: createPresetColors(palettes.blue, palettes.zinc),
314
+ green: createPresetColors(palettes.green, palettes.zinc),
315
+ rose: createPresetColors(palettes.rose, palettes.zinc),
316
+ orange: createPresetColors(palettes.orange, palettes.zinc),
317
+ violet: createPresetColors(palettes.violet, palettes.zinc),
318
+ };
319
+
320
+ /**
321
+ * Default theme preset.
322
+ */
323
+ export const defaultThemePreset: ThemePreset = 'zinc';
324
+
325
+ /**
326
+ * Gets colors for a theme preset.
327
+ */
328
+ export function getPresetColors(preset: ThemePreset): PresetColors {
329
+ return themePresets[preset];
330
+ }
331
+
332
+ /**
333
+ * Gets light or dark colors for a theme preset.
334
+ */
335
+ export function getPresetColorsForMode(
336
+ preset: ThemePreset,
337
+ isDark: boolean
338
+ ): ThemeColors {
339
+ const presetColors = themePresets[preset];
340
+ return isDark ? presetColors.dark : presetColors.light;
341
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Border Radius Tokens
3
+ *
4
+ * Consistent border radius scale for all components.
5
+ * Use the `radius` prop on ThemeProvider to change the base radius
6
+ * and transform your entire app from sharp to rounded.
7
+ *
8
+ * Usage:
9
+ * ```tsx
10
+ * // Sharp, brutalist design
11
+ * <ThemeProvider radius="none">
12
+ *
13
+ * // Soft, friendly design
14
+ * <ThemeProvider radius="lg">
15
+ *
16
+ * // In components
17
+ * const { radius } = useTheme();
18
+ * <View style={{ borderRadius: radius.md }} />
19
+ * ```
20
+ */
21
+
22
+ // =============================================================================
23
+ // Radius Preset System
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Available radius presets.
28
+ * Each preset defines a base value that scales all radii.
29
+ */
30
+ export type RadiusPreset = 'none' | 'sm' | 'md' | 'lg' | 'full';
31
+
32
+ /**
33
+ * Base values for each radius preset.
34
+ */
35
+ export const radiusPresetBase: Record<RadiusPreset, number> = {
36
+ none: 0,
37
+ sm: 4,
38
+ md: 8,
39
+ lg: 12,
40
+ full: 9999,
41
+ };
42
+
43
+ /**
44
+ * Radius token structure.
45
+ */
46
+ export interface RadiusTokens {
47
+ none: number;
48
+ xs: number;
49
+ sm: number;
50
+ md: number;
51
+ lg: number;
52
+ xl: number;
53
+ '2xl': number;
54
+ full: number;
55
+ }
56
+
57
+ export type RadiusKey = keyof RadiusTokens;
58
+ export type RadiusValue = number;
59
+
60
+ /**
61
+ * Creates radius tokens from a preset or base value.
62
+ *
63
+ * @param preset - Radius preset name or custom base value
64
+ * @returns Scaled radius tokens
65
+ */
66
+ export function createRadius(preset: RadiusPreset | number = 'md'): RadiusTokens {
67
+ const base = typeof preset === 'number' ? preset : radiusPresetBase[preset];
68
+
69
+ // Special case: 'none' preset = all zeros except full
70
+ if (preset === 'none' || base === 0) {
71
+ return {
72
+ none: 0,
73
+ xs: 0,
74
+ sm: 0,
75
+ md: 0,
76
+ lg: 0,
77
+ xl: 0,
78
+ '2xl': 0,
79
+ full: 0,
80
+ };
81
+ }
82
+
83
+ // Special case: 'full' preset = all pill-shaped
84
+ if (preset === 'full' || base >= 9999) {
85
+ return {
86
+ none: 0,
87
+ xs: 9999,
88
+ sm: 9999,
89
+ md: 9999,
90
+ lg: 9999,
91
+ xl: 9999,
92
+ '2xl': 9999,
93
+ full: 9999,
94
+ };
95
+ }
96
+
97
+ // Scale from base value
98
+ return {
99
+ none: 0,
100
+ xs: Math.round(base * 0.25), // 2 when base=8
101
+ sm: Math.round(base * 0.5), // 4 when base=8
102
+ md: base, // 8 when base=8
103
+ lg: Math.round(base * 1.5), // 12 when base=8
104
+ xl: Math.round(base * 2), // 16 when base=8
105
+ '2xl': Math.round(base * 3), // 24 when base=8
106
+ full: 9999,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Default radius tokens (md preset).
112
+ */
113
+ export const radius = createRadius('md');
114
+
115
+ /**
116
+ * Default radius preset.
117
+ */
118
+ export const defaultRadiusPreset: RadiusPreset = 'md';
119
+
120
+ // =============================================================================
121
+ // Component Radius
122
+ // =============================================================================
123
+
124
+ /**
125
+ * Component-specific radius tokens structure.
126
+ */
127
+ export interface ComponentRadiusTokens {
128
+ button: number;
129
+ buttonSm: number;
130
+ buttonLg: number;
131
+ input: number;
132
+ card: number;
133
+ badge: number;
134
+ avatar: number;
135
+ checkbox: number;
136
+ switch: number;
137
+ modal: number;
138
+ tooltip: number;
139
+ }
140
+
141
+ export type ComponentRadiusKey = keyof ComponentRadiusTokens;
142
+
143
+ /**
144
+ * Pill radius constant - always fully rounded regardless of preset.
145
+ * Used for components that should always be pill-shaped (avatar, switch, badge).
146
+ */
147
+ const PILL_RADIUS = 9999;
148
+
149
+ /**
150
+ * Creates component-specific radius values from radius tokens.
151
+ *
152
+ * @param radiusTokens - Base radius tokens
153
+ * @returns Component-specific radius values
154
+ */
155
+ export function createComponentRadius(radiusTokens: RadiusTokens): ComponentRadiusTokens {
156
+ return {
157
+ button: radiusTokens.md,
158
+ buttonSm: radiusTokens.sm,
159
+ buttonLg: radiusTokens.lg,
160
+ input: radiusTokens.md,
161
+ card: radiusTokens.lg,
162
+ // Always pill-shaped regardless of preset
163
+ badge: PILL_RADIUS,
164
+ avatar: PILL_RADIUS,
165
+ checkbox: radiusTokens.sm,
166
+ switch: PILL_RADIUS,
167
+ modal: radiusTokens.xl,
168
+ tooltip: radiusTokens.md,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Default component radius values.
174
+ */
175
+ export const componentRadius = createComponentRadius(radius);
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Shadow Presets
3
+ *
4
+ * Consistent shadow tokens for depth and elevation.
5
+ * Platform-aware: iOS uses shadow properties, Android uses elevation.
6
+ *
7
+ * Inspired by iOS 17+ depth language and Linear App shadows.
8
+ */
9
+
10
+ import { Platform, ViewStyle } from 'react-native';
11
+
12
+ export interface ShadowStyle {
13
+ shadowColor: string;
14
+ shadowOffset: { width: number; height: number };
15
+ shadowOpacity: number;
16
+ shadowRadius: number;
17
+ elevation: number;
18
+ }
19
+
20
+ // Shadow presets for light mode
21
+ const lightShadows = {
22
+ /** Subtle shadow for cards and surfaces */
23
+ sm: {
24
+ shadowColor: '#000000',
25
+ shadowOffset: { width: 0, height: 1 },
26
+ shadowOpacity: 0.05,
27
+ shadowRadius: 2,
28
+ elevation: 1,
29
+ },
30
+
31
+ /** Default shadow for buttons and interactive elements */
32
+ md: {
33
+ shadowColor: '#000000',
34
+ shadowOffset: { width: 0, height: 2 },
35
+ shadowOpacity: 0.08,
36
+ shadowRadius: 4,
37
+ elevation: 3,
38
+ },
39
+
40
+ /** Elevated shadow for modals and popovers */
41
+ lg: {
42
+ shadowColor: '#000000',
43
+ shadowOffset: { width: 0, height: 4 },
44
+ shadowOpacity: 0.12,
45
+ shadowRadius: 8,
46
+ elevation: 6,
47
+ },
48
+
49
+ /** Prominent shadow for floating elements */
50
+ xl: {
51
+ shadowColor: '#000000',
52
+ shadowOffset: { width: 0, height: 8 },
53
+ shadowOpacity: 0.15,
54
+ shadowRadius: 16,
55
+ elevation: 12,
56
+ },
57
+
58
+ /** Maximum shadow for overlays */
59
+ '2xl': {
60
+ shadowColor: '#000000',
61
+ shadowOffset: { width: 0, height: 12 },
62
+ shadowOpacity: 0.2,
63
+ shadowRadius: 24,
64
+ elevation: 16,
65
+ },
66
+
67
+ /** No shadow */
68
+ none: {
69
+ shadowColor: 'transparent',
70
+ shadowOffset: { width: 0, height: 0 },
71
+ shadowOpacity: 0,
72
+ shadowRadius: 0,
73
+ elevation: 0,
74
+ },
75
+ } as const;
76
+
77
+ // Shadow presets for dark mode (more subtle)
78
+ const darkShadows = {
79
+ sm: {
80
+ shadowColor: '#000000',
81
+ shadowOffset: { width: 0, height: 1 },
82
+ shadowOpacity: 0.2,
83
+ shadowRadius: 2,
84
+ elevation: 1,
85
+ },
86
+
87
+ md: {
88
+ shadowColor: '#000000',
89
+ shadowOffset: { width: 0, height: 2 },
90
+ shadowOpacity: 0.3,
91
+ shadowRadius: 4,
92
+ elevation: 3,
93
+ },
94
+
95
+ lg: {
96
+ shadowColor: '#000000',
97
+ shadowOffset: { width: 0, height: 4 },
98
+ shadowOpacity: 0.4,
99
+ shadowRadius: 8,
100
+ elevation: 6,
101
+ },
102
+
103
+ xl: {
104
+ shadowColor: '#000000',
105
+ shadowOffset: { width: 0, height: 8 },
106
+ shadowOpacity: 0.5,
107
+ shadowRadius: 16,
108
+ elevation: 12,
109
+ },
110
+
111
+ '2xl': {
112
+ shadowColor: '#000000',
113
+ shadowOffset: { width: 0, height: 12 },
114
+ shadowOpacity: 0.6,
115
+ shadowRadius: 24,
116
+ elevation: 16,
117
+ },
118
+
119
+ none: {
120
+ shadowColor: 'transparent',
121
+ shadowOffset: { width: 0, height: 0 },
122
+ shadowOpacity: 0,
123
+ shadowRadius: 0,
124
+ elevation: 0,
125
+ },
126
+ } as const;
127
+
128
+ export type ShadowSize = keyof typeof lightShadows;
129
+
130
+ /**
131
+ * Get shadow styles for a given size and color scheme
132
+ */
133
+ export function getShadow(size: ShadowSize, isDark: boolean = false): ShadowStyle {
134
+ const shadows = isDark ? darkShadows : lightShadows;
135
+ return shadows[size];
136
+ }
137
+
138
+ /**
139
+ * Get platform-optimized shadow style
140
+ * Returns only the relevant properties for the current platform
141
+ */
142
+ export function getPlatformShadow(size: ShadowSize, isDark: boolean = false): ViewStyle {
143
+ const shadow = getShadow(size, isDark);
144
+
145
+ if (Platform.OS === 'ios') {
146
+ return {
147
+ shadowColor: shadow.shadowColor,
148
+ shadowOffset: shadow.shadowOffset,
149
+ shadowOpacity: shadow.shadowOpacity,
150
+ shadowRadius: shadow.shadowRadius,
151
+ };
152
+ }
153
+
154
+ if (Platform.OS === 'android') {
155
+ return {
156
+ elevation: shadow.elevation,
157
+ };
158
+ }
159
+
160
+ return shadow;
161
+ }
162
+
163
+ export const shadows = {
164
+ light: lightShadows,
165
+ dark: darkShadows,
166
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Spacing Tokens
3
+ *
4
+ * Consistent spacing scale inspired by Tailwind CSS.
5
+ * All values in logical pixels.
6
+ *
7
+ * Usage:
8
+ * ```tsx
9
+ * const { spacing } = useTheme();
10
+ * <View style={{ padding: spacing[4], gap: spacing[2] }} />
11
+ * ```
12
+ */
13
+
14
+ export const spacing = {
15
+ 0: 0,
16
+ 0.5: 2,
17
+ 1: 4,
18
+ 1.5: 6,
19
+ 2: 8,
20
+ 2.5: 10,
21
+ 3: 12,
22
+ 3.5: 14,
23
+ 4: 16,
24
+ 5: 20,
25
+ 6: 24,
26
+ 7: 28,
27
+ 8: 32,
28
+ 9: 36,
29
+ 10: 40,
30
+ 11: 44,
31
+ 12: 48,
32
+ 14: 56,
33
+ 16: 64,
34
+ 20: 80,
35
+ 24: 96,
36
+ 28: 112,
37
+ 32: 128,
38
+ } as const;
39
+
40
+ export type SpacingKey = keyof typeof spacing;
41
+ export type SpacingValue = (typeof spacing)[SpacingKey];