@idealyst/theme 1.1.7 → 1.1.9
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/package.json +30 -1
- package/src/babel/index.ts +9 -0
- package/src/babel/plugin.js +883 -0
- package/src/babel/plugin.ts +187 -0
- package/src/babel/runtime.ts +94 -0
- package/src/babel/theme-analyzer.js +357 -0
- package/src/breakpoints.ts +112 -0
- package/src/builder.ts +90 -18
- package/src/componentStyles.ts +93 -0
- package/src/config/cli.ts +95 -0
- package/src/config/generator.ts +817 -0
- package/src/config/index.ts +10 -0
- package/src/config/types.ts +112 -0
- package/src/darkTheme.ts +27 -18
- package/src/extensions.ts +110 -0
- package/src/index.ts +21 -4
- package/src/lightTheme.ts +14 -5
- package/src/responsive.ts +123 -0
- package/src/styleBuilder.ts +112 -0
- package/src/theme/breakpoint.ts +30 -0
- package/src/theme/extensions.ts +13 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/structures.ts +7 -0
- package/src/theme/surface.ts +1 -1
- package/src/unistyles.ts +11 -15
- package/src/useResponsiveStyle.ts +282 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idealyst Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for defining theme configuration and generating
|
|
5
|
+
* flat, Unistyles-compatible style files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { defineConfig } from './types';
|
|
9
|
+
export { generateStyles } from './generator';
|
|
10
|
+
export type { IdealystConfig, ComponentExtensions, ComponentExtension } from './types';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idealyst Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* This module defines the structure for idealyst.config.ts files.
|
|
5
|
+
* The config is used to generate flat, Unistyles-compatible style files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Theme } from '../theme';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Component extension definition.
|
|
12
|
+
* Can be either static styles or a function that receives the theme.
|
|
13
|
+
*/
|
|
14
|
+
export type ComponentExtension<T = Record<string, any>> =
|
|
15
|
+
| T
|
|
16
|
+
| ((theme: Theme) => T);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extension definitions for all components.
|
|
20
|
+
*/
|
|
21
|
+
export interface ComponentExtensions {
|
|
22
|
+
View?: ComponentExtension<{
|
|
23
|
+
view?: Record<string, any>;
|
|
24
|
+
}>;
|
|
25
|
+
Button?: ComponentExtension<{
|
|
26
|
+
button?: Record<string, any>;
|
|
27
|
+
text?: Record<string, any>;
|
|
28
|
+
icon?: Record<string, any>;
|
|
29
|
+
iconContainer?: Record<string, any>;
|
|
30
|
+
}>;
|
|
31
|
+
Text?: ComponentExtension<{
|
|
32
|
+
text?: Record<string, any>;
|
|
33
|
+
}>;
|
|
34
|
+
Card?: ComponentExtension<{
|
|
35
|
+
card?: Record<string, any>;
|
|
36
|
+
}>;
|
|
37
|
+
Input?: ComponentExtension<{
|
|
38
|
+
wrapper?: Record<string, any>;
|
|
39
|
+
input?: Record<string, any>;
|
|
40
|
+
label?: Record<string, any>;
|
|
41
|
+
hint?: Record<string, any>;
|
|
42
|
+
}>;
|
|
43
|
+
Screen?: ComponentExtension<{
|
|
44
|
+
screen?: Record<string, any>;
|
|
45
|
+
screenContent?: Record<string, any>;
|
|
46
|
+
}>;
|
|
47
|
+
// Add more components as needed...
|
|
48
|
+
[key: string]: ComponentExtension | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Theme definition in the config.
|
|
53
|
+
*/
|
|
54
|
+
export interface ThemeDefinition {
|
|
55
|
+
/**
|
|
56
|
+
* The built theme object.
|
|
57
|
+
* Created using createTheme() or fromTheme() builders.
|
|
58
|
+
*/
|
|
59
|
+
theme: Theme;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional name for this theme variant.
|
|
63
|
+
* Defaults to 'light' for the first theme, 'dark' for the second.
|
|
64
|
+
*/
|
|
65
|
+
name?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Main Idealyst configuration structure.
|
|
70
|
+
*/
|
|
71
|
+
export interface IdealystConfig {
|
|
72
|
+
/**
|
|
73
|
+
* Theme definitions.
|
|
74
|
+
* At minimum, define light and dark themes.
|
|
75
|
+
*/
|
|
76
|
+
themes: {
|
|
77
|
+
light: Theme;
|
|
78
|
+
dark: Theme;
|
|
79
|
+
[key: string]: Theme;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Global component style extensions.
|
|
84
|
+
* These are merged with base component styles.
|
|
85
|
+
*/
|
|
86
|
+
extensions?: ComponentExtensions;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Output configuration.
|
|
90
|
+
*/
|
|
91
|
+
output?: {
|
|
92
|
+
/**
|
|
93
|
+
* Directory to output generated style files.
|
|
94
|
+
* Relative to config file location.
|
|
95
|
+
* @default './generated'
|
|
96
|
+
*/
|
|
97
|
+
dir?: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether to generate TypeScript or JavaScript.
|
|
101
|
+
* @default 'typescript'
|
|
102
|
+
*/
|
|
103
|
+
format?: 'typescript' | 'javascript';
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Helper to define a config with full type inference.
|
|
109
|
+
*/
|
|
110
|
+
export function defineConfig(config: IdealystConfig): IdealystConfig {
|
|
111
|
+
return config;
|
|
112
|
+
}
|
package/src/darkTheme.ts
CHANGED
|
@@ -54,15 +54,15 @@ export const darkTheme = createTheme()
|
|
|
54
54
|
// Shadows (higher opacity for dark backgrounds)
|
|
55
55
|
.addShadow('none', {})
|
|
56
56
|
.addShadow('sm', {
|
|
57
|
-
elevation:
|
|
57
|
+
elevation: 2,
|
|
58
58
|
shadowColor: '#000000',
|
|
59
59
|
shadowOffset: { width: 0, height: 1 },
|
|
60
60
|
shadowOpacity: 0.18,
|
|
61
|
-
shadowRadius:
|
|
61
|
+
shadowRadius: 2,
|
|
62
62
|
boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.18)',
|
|
63
63
|
})
|
|
64
64
|
.addShadow('md', {
|
|
65
|
-
elevation:
|
|
65
|
+
elevation: 4,
|
|
66
66
|
shadowColor: '#000000',
|
|
67
67
|
shadowOffset: { width: 0, height: 3 },
|
|
68
68
|
shadowOpacity: 0.2,
|
|
@@ -70,7 +70,7 @@ export const darkTheme = createTheme()
|
|
|
70
70
|
boxShadow: '0px 3px 9.3px rgba(0, 0, 0, 0.2)',
|
|
71
71
|
})
|
|
72
72
|
.addShadow('lg', {
|
|
73
|
-
elevation:
|
|
73
|
+
elevation: 8,
|
|
74
74
|
shadowColor: '#000000',
|
|
75
75
|
shadowOffset: { width: 0, height: 6 },
|
|
76
76
|
shadowOpacity: 0.23,
|
|
@@ -85,30 +85,31 @@ export const darkTheme = createTheme()
|
|
|
85
85
|
shadowRadius: 16,
|
|
86
86
|
boxShadow: '0px 12px 32px rgba(0, 0, 0, 0.25)',
|
|
87
87
|
})
|
|
88
|
-
// Colors
|
|
88
|
+
// Colors (neutral grays, not blue-tinted)
|
|
89
89
|
.setColors({
|
|
90
90
|
pallet: generateDarkColorPallette(),
|
|
91
91
|
surface: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
screen: '#121212',
|
|
93
|
+
primary: '#1e1e1e',
|
|
94
|
+
secondary: '#1e1e1e',
|
|
95
|
+
tertiary: '#2a2a2a',
|
|
95
96
|
inverse: '#ffffff',
|
|
96
97
|
'inverse-secondary': 'rgba(255, 255, 255, 0.9)',
|
|
97
98
|
'inverse-tertiary': 'rgba(255, 255, 255, 0.7)',
|
|
98
99
|
},
|
|
99
100
|
text: {
|
|
100
|
-
primary: '#
|
|
101
|
-
secondary: '#
|
|
102
|
-
tertiary: '#
|
|
103
|
-
inverse: '#
|
|
104
|
-
'inverse-secondary': 'rgba(
|
|
105
|
-
'inverse-tertiary': 'rgba(
|
|
101
|
+
primary: '#f5f5f5',
|
|
102
|
+
secondary: '#a3a3a3',
|
|
103
|
+
tertiary: '#737373',
|
|
104
|
+
inverse: '#121212',
|
|
105
|
+
'inverse-secondary': 'rgba(18, 18, 18, 0.9)',
|
|
106
|
+
'inverse-tertiary': 'rgba(18, 18, 18, 0.7)',
|
|
106
107
|
},
|
|
107
108
|
border: {
|
|
108
|
-
primary: '#
|
|
109
|
-
secondary: '#
|
|
110
|
-
tertiary: '#
|
|
111
|
-
disabled: '#
|
|
109
|
+
primary: '#333333',
|
|
110
|
+
secondary: '#404040',
|
|
111
|
+
tertiary: '#525252',
|
|
112
|
+
disabled: '#1e1e1e',
|
|
112
113
|
},
|
|
113
114
|
})
|
|
114
115
|
// Sizes (reuse from light theme)
|
|
@@ -123,6 +124,14 @@ export const darkTheme = createTheme()
|
|
|
123
124
|
disabled: 0.4,
|
|
124
125
|
},
|
|
125
126
|
})
|
|
127
|
+
// Breakpoints (same as light theme)
|
|
128
|
+
.setBreakpoints({
|
|
129
|
+
xs: 0, // Extra small devices (portrait phones)
|
|
130
|
+
sm: 576, // Small devices (landscape phones)
|
|
131
|
+
md: 768, // Medium devices (tablets)
|
|
132
|
+
lg: 992, // Large devices (desktops)
|
|
133
|
+
xl: 1200, // Extra large devices (large desktops)
|
|
134
|
+
})
|
|
126
135
|
.build();
|
|
127
136
|
|
|
128
137
|
// =============================================================================
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extensions runtime for Idealyst theme.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the runtime helper used by the Babel plugin
|
|
5
|
+
* to merge extensions into component styles.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { __withExtension } from './babel/runtime';
|
|
9
|
+
|
|
10
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
11
|
+
import type { Theme } from './theme';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// ThemeStyleWrapper - Type utility for style definitions
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
// Check if T is an object we can iterate over (not array, not function, not primitive)
|
|
18
|
+
type IsIterableObject<T> = T extends object
|
|
19
|
+
? T extends any[]
|
|
20
|
+
? false
|
|
21
|
+
: T extends (...args: any[]) => any
|
|
22
|
+
? false
|
|
23
|
+
: true
|
|
24
|
+
: false;
|
|
25
|
+
|
|
26
|
+
// Get value type from an object - works for both Record and named-key objects
|
|
27
|
+
type ObjectValue<T> = T extends { [key: string]: infer V }
|
|
28
|
+
? V
|
|
29
|
+
: T[keyof T];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wraps a Theme type to add $iterator properties for any object types.
|
|
33
|
+
*
|
|
34
|
+
* For each property that is an object (dictionary-like), adds a sibling $propertyName
|
|
35
|
+
* that represents the value type. This enables Babel to expand iterations
|
|
36
|
+
* over theme values (like intents, typography sizes, etc.)
|
|
37
|
+
*
|
|
38
|
+
* Works with:
|
|
39
|
+
* - Record<string, V> types
|
|
40
|
+
* - Objects with named keys like { primary: V, success: V, ... }
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { ThemeStyleWrapper } from '@idealyst/theme/extensions';
|
|
45
|
+
* import type { Theme as BaseTheme } from '@idealyst/theme';
|
|
46
|
+
*
|
|
47
|
+
* type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
48
|
+
*
|
|
49
|
+
* // Now you can use $iterator properties:
|
|
50
|
+
* // theme.$intents.primary -> Babel expands to all intent keys
|
|
51
|
+
* // theme.sizes.$typography.fontSize -> Babel expands to all typography keys
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export type ThemeStyleWrapper<T> = T extends object
|
|
55
|
+
? {
|
|
56
|
+
[K in keyof T]: ThemeStyleWrapper<T[K]>
|
|
57
|
+
} & {
|
|
58
|
+
[K in keyof T as IsIterableObject<T[K]> extends true ? `$${K & string}` : never]:
|
|
59
|
+
ThemeStyleWrapper<ObjectValue<T[K]>>
|
|
60
|
+
}
|
|
61
|
+
: T;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Theme type with $iterator properties for use in style definitions.
|
|
65
|
+
* Use this instead of the base Theme type when using $iterator syntax.
|
|
66
|
+
*/
|
|
67
|
+
export type IteratorTheme = ThemeStyleWrapper<Theme>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Style callback type that accepts IteratorTheme.
|
|
71
|
+
* The variants structure is relaxed to accept $iterator patterns.
|
|
72
|
+
*/
|
|
73
|
+
export type IteratorStyleCallback<TStyles> = (theme: IteratorTheme) => TStyles;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a stylesheet with $iterator expansion support.
|
|
77
|
+
*
|
|
78
|
+
* This is a typed wrapper around StyleSheet.create that:
|
|
79
|
+
* 1. Accepts the IteratorTheme type (with $intents, $sizes, etc.)
|
|
80
|
+
* 2. Allows relaxed variant structures for $iterator patterns
|
|
81
|
+
*
|
|
82
|
+
* The Babel plugin will expand $iterator patterns before Unistyles processes the styles.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* import { createIteratorStyles } from '@idealyst/theme';
|
|
87
|
+
*
|
|
88
|
+
* export const styles = createIteratorStyles((theme) => ({
|
|
89
|
+
* box: {
|
|
90
|
+
* borderRadius: theme.radii.md,
|
|
91
|
+
* variants: {
|
|
92
|
+
* intent: {
|
|
93
|
+
* backgroundColor: theme.$intents.light,
|
|
94
|
+
* borderColor: theme.$intents.primary,
|
|
95
|
+
* },
|
|
96
|
+
* size: {
|
|
97
|
+
* padding: theme.sizes.$button.paddingVertical,
|
|
98
|
+
* },
|
|
99
|
+
* },
|
|
100
|
+
* },
|
|
101
|
+
* }));
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function createIteratorStyles<TStyles extends Record<string, any>>(
|
|
105
|
+
callback: IteratorStyleCallback<TStyles>
|
|
106
|
+
): ReturnType<typeof StyleSheet.create> {
|
|
107
|
+
// This function is transformed by Babel - it sees StyleSheet.create and processes it
|
|
108
|
+
// The callback is passed through with $iterator patterns expanded
|
|
109
|
+
return StyleSheet.create(callback as any);
|
|
110
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './lightTheme';
|
|
3
|
-
export * from './darkTheme';
|
|
1
|
+
// Core types first (no dependencies)
|
|
4
2
|
export * from './theme';
|
|
5
3
|
export * from './variants';
|
|
6
4
|
export * from './components';
|
|
5
|
+
|
|
6
|
+
// Builder (depends on theme types)
|
|
7
|
+
export * from './builder';
|
|
8
|
+
|
|
9
|
+
// Themes (depend on builder)
|
|
10
|
+
export * from './lightTheme';
|
|
11
|
+
export * from './darkTheme';
|
|
12
|
+
|
|
13
|
+
// Unistyles declaration (depends on theme)
|
|
14
|
+
export * from './unistyles';
|
|
15
|
+
|
|
16
|
+
// Helpers and styles
|
|
7
17
|
export * from './styles';
|
|
8
18
|
export * from './helpers';
|
|
9
|
-
export * from './
|
|
19
|
+
export * from './styleBuilder';
|
|
20
|
+
export * from './extensions';
|
|
21
|
+
export * from './componentStyles';
|
|
22
|
+
|
|
23
|
+
// Responsive utilities
|
|
24
|
+
export * from './responsive';
|
|
25
|
+
export * from './breakpoints';
|
|
26
|
+
export * from './useResponsiveStyle';
|
package/src/lightTheme.ts
CHANGED
|
@@ -88,6 +88,7 @@ export const lightTheme = createTheme()
|
|
|
88
88
|
.setColors({
|
|
89
89
|
pallet: generateColorPallette(),
|
|
90
90
|
surface: {
|
|
91
|
+
screen: '#ffffff',
|
|
91
92
|
primary: '#ffffff',
|
|
92
93
|
secondary: '#f5f5f5',
|
|
93
94
|
tertiary: '#e0e0e0',
|
|
@@ -155,11 +156,11 @@ export const lightTheme = createTheme()
|
|
|
155
156
|
xl: { radioSize: 26, radioDotSize: 20, fontSize: 20, gap: 12 },
|
|
156
157
|
},
|
|
157
158
|
select: {
|
|
158
|
-
xs: { paddingHorizontal: 8, minHeight: 28, fontSize: 12, iconSize: 16 },
|
|
159
|
-
sm: { paddingHorizontal: 10, minHeight: 36, fontSize: 14, iconSize: 18 },
|
|
160
|
-
md: { paddingHorizontal: 12, minHeight: 44, fontSize: 16, iconSize: 20 },
|
|
161
|
-
lg: { paddingHorizontal: 16, minHeight: 52, fontSize: 18, iconSize: 24 },
|
|
162
|
-
xl: { paddingHorizontal: 20, minHeight: 60, fontSize: 20, iconSize: 28 },
|
|
159
|
+
xs: { paddingHorizontal: 8, minHeight: 28, fontSize: 12, iconSize: 16, borderRadius: 4 },
|
|
160
|
+
sm: { paddingHorizontal: 10, minHeight: 36, fontSize: 14, iconSize: 18, borderRadius: 4 },
|
|
161
|
+
md: { paddingHorizontal: 12, minHeight: 44, fontSize: 16, iconSize: 20, borderRadius: 4 },
|
|
162
|
+
lg: { paddingHorizontal: 16, minHeight: 52, fontSize: 18, iconSize: 24, borderRadius: 4 },
|
|
163
|
+
xl: { paddingHorizontal: 20, minHeight: 60, fontSize: 20, iconSize: 28, borderRadius: 4 },
|
|
163
164
|
},
|
|
164
165
|
slider: {
|
|
165
166
|
xs: { trackHeight: 2, thumbSize: 12, thumbIconSize: 8, markHeight: 6, labelFontSize: 10 },
|
|
@@ -290,6 +291,14 @@ export const lightTheme = createTheme()
|
|
|
290
291
|
disabled: 0.5,
|
|
291
292
|
},
|
|
292
293
|
})
|
|
294
|
+
// Breakpoints
|
|
295
|
+
.setBreakpoints({
|
|
296
|
+
xs: 0, // Extra small devices (portrait phones)
|
|
297
|
+
sm: 576, // Small devices (landscape phones)
|
|
298
|
+
md: 768, // Medium devices (tablets)
|
|
299
|
+
lg: 992, // Large devices (desktops)
|
|
300
|
+
xl: 1200, // Extra large devices (large desktops)
|
|
301
|
+
})
|
|
293
302
|
.build();
|
|
294
303
|
|
|
295
304
|
// =============================================================================
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
|
|
2
|
+
import { UnistylesRuntime } from 'react-native-unistyles';
|
|
3
|
+
import { Breakpoint, BreakpointsRecord } from './theme/breakpoint';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Makes a value responsive - can be either a direct value or
|
|
7
|
+
* an object mapping breakpoint names to values.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // Direct value
|
|
12
|
+
* const size: Responsive<Size> = 'md';
|
|
13
|
+
*
|
|
14
|
+
* // Responsive value
|
|
15
|
+
* const size: Responsive<Size> = { xs: 'sm', md: 'lg' };
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type guard to check if a value is a responsive object (breakpoint map).
|
|
22
|
+
*
|
|
23
|
+
* @param value - The value to check
|
|
24
|
+
* @returns True if the value is an object with breakpoint keys
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const size: Responsive<Size> = { xs: 'sm', md: 'lg' };
|
|
29
|
+
*
|
|
30
|
+
* if (isResponsiveValue(size)) {
|
|
31
|
+
* // size is Partial<Record<Breakpoint, Size>>
|
|
32
|
+
* console.log(size.xs); // 'sm'
|
|
33
|
+
* } else {
|
|
34
|
+
* // size is Size
|
|
35
|
+
* console.log(size); // 'md'
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function isResponsiveValue<T>(value: Responsive<T>): value is Partial<Record<Breakpoint, T>> {
|
|
40
|
+
return (
|
|
41
|
+
typeof value === 'object' &&
|
|
42
|
+
value !== null &&
|
|
43
|
+
!Array.isArray(value) &&
|
|
44
|
+
!('$$typeof' in value) // Not a React element
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Style object where each property can be a responsive value.
|
|
50
|
+
*/
|
|
51
|
+
export type ResponsiveStyle = {
|
|
52
|
+
[K in keyof ViewStyle]?: Responsive<ViewStyle[K]>;
|
|
53
|
+
} & {
|
|
54
|
+
[K in keyof TextStyle]?: Responsive<TextStyle[K]>;
|
|
55
|
+
} & {
|
|
56
|
+
[K in keyof ImageStyle]?: Responsive<ImageStyle[K]>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a single responsive value to its current breakpoint value.
|
|
61
|
+
* Uses CSS cascade behavior - falls back to the nearest smaller breakpoint.
|
|
62
|
+
*/
|
|
63
|
+
function resolveValue<T>(
|
|
64
|
+
value: Responsive<T>,
|
|
65
|
+
screenWidth: number,
|
|
66
|
+
breakpoints: BreakpointsRecord,
|
|
67
|
+
sortedBps: Breakpoint[]
|
|
68
|
+
): T | undefined {
|
|
69
|
+
if (!isResponsiveValue(value)) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Find the largest breakpoint that matches current screen width
|
|
74
|
+
// and has a defined value (CSS cascade behavior)
|
|
75
|
+
for (const bp of sortedBps) {
|
|
76
|
+
if (screenWidth >= breakpoints[bp] && value[bp] !== undefined) {
|
|
77
|
+
return value[bp];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a responsive style object to concrete style values based on current breakpoint.
|
|
86
|
+
*
|
|
87
|
+
* This is a non-hook version for use outside of React components.
|
|
88
|
+
*
|
|
89
|
+
* @param style - Style object with responsive values
|
|
90
|
+
* @returns Resolved style object for the current breakpoint
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const style = resolveResponsiveStyle({
|
|
95
|
+
* flexDirection: { xs: 'column', md: 'row' },
|
|
96
|
+
* padding: { xs: 8, lg: 16 },
|
|
97
|
+
* backgroundColor: '#fff', // Non-responsive values pass through
|
|
98
|
+
* });
|
|
99
|
+
* // On tablet: { flexDirection: 'row', padding: 8, backgroundColor: '#fff' }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function resolveResponsiveStyle(style: ResponsiveStyle): ViewStyle & TextStyle & ImageStyle {
|
|
103
|
+
const breakpoints = UnistylesRuntime.breakpoints as BreakpointsRecord;
|
|
104
|
+
const screenWidth = UnistylesRuntime.screen.width;
|
|
105
|
+
|
|
106
|
+
// Sort breakpoints by value descending for cascade lookup
|
|
107
|
+
const sortedBps = Object.entries(breakpoints)
|
|
108
|
+
.sort(([, a], [, b]) => b - a)
|
|
109
|
+
.map(([name]) => name as Breakpoint);
|
|
110
|
+
|
|
111
|
+
const resolved: Record<string, unknown> = {};
|
|
112
|
+
|
|
113
|
+
for (const [key, value] of Object.entries(style)) {
|
|
114
|
+
if (value === undefined) continue;
|
|
115
|
+
|
|
116
|
+
const resolvedValue = resolveValue(value, screenWidth, breakpoints, sortedBps);
|
|
117
|
+
if (resolvedValue !== undefined) {
|
|
118
|
+
resolved[key] = resolvedValue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return resolved as ViewStyle & TextStyle & ImageStyle;
|
|
123
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StyleBuilder - Declarative styling system with extension support
|
|
3
|
+
*
|
|
4
|
+
* All transformations happen at BUILD TIME via Babel plugin.
|
|
5
|
+
* These functions are replaced with StyleSheet.create calls.
|
|
6
|
+
*
|
|
7
|
+
* For extensions to work, import your extension file BEFORE components:
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // App.tsx
|
|
11
|
+
* import './style-extensions'; // FIRST
|
|
12
|
+
* import { Text } from '@idealyst/components'; // SECOND
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
17
|
+
import type { ComponentStyleRegistry, ExtendStyleDef, OverrideStyleDef } from './componentStyles';
|
|
18
|
+
|
|
19
|
+
export type ComponentName =
|
|
20
|
+
| 'Accordion'
|
|
21
|
+
| 'ActivityIndicator'
|
|
22
|
+
| 'Alert'
|
|
23
|
+
| 'Avatar'
|
|
24
|
+
| 'Badge'
|
|
25
|
+
| 'Breadcrumb'
|
|
26
|
+
| 'Button'
|
|
27
|
+
| 'Card'
|
|
28
|
+
| 'Checkbox'
|
|
29
|
+
| 'Chip'
|
|
30
|
+
| 'DatePickerCalendar'
|
|
31
|
+
| 'DateTimeInput'
|
|
32
|
+
| 'DateTimePicker'
|
|
33
|
+
| 'Dialog'
|
|
34
|
+
| 'Divider'
|
|
35
|
+
| 'Icon'
|
|
36
|
+
| 'Image'
|
|
37
|
+
| 'Input'
|
|
38
|
+
| 'Link'
|
|
39
|
+
| 'List'
|
|
40
|
+
| 'Menu'
|
|
41
|
+
| 'Popover'
|
|
42
|
+
| 'Pressable'
|
|
43
|
+
| 'Progress'
|
|
44
|
+
| 'RadioButton'
|
|
45
|
+
| 'Screen'
|
|
46
|
+
| 'Select'
|
|
47
|
+
| 'Skeleton'
|
|
48
|
+
| 'Slider'
|
|
49
|
+
| 'SVGImage'
|
|
50
|
+
| 'Switch'
|
|
51
|
+
| 'TabBar'
|
|
52
|
+
| 'Table'
|
|
53
|
+
| 'Text'
|
|
54
|
+
| 'TextArea'
|
|
55
|
+
| 'TimePicker'
|
|
56
|
+
| 'Tooltip'
|
|
57
|
+
| 'Video'
|
|
58
|
+
| 'View';
|
|
59
|
+
|
|
60
|
+
export type StyleCallback<TTheme, TStyles> = (theme: TTheme) => TStyles;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Define base styles for a component.
|
|
64
|
+
* Babel transforms this to StyleSheet.create with merged extensions.
|
|
65
|
+
*/
|
|
66
|
+
export function defineStyle<TTheme, TStyles>(
|
|
67
|
+
_componentName: ComponentName,
|
|
68
|
+
styles: StyleCallback<TTheme, TStyles>
|
|
69
|
+
): ReturnType<typeof StyleSheet.create> {
|
|
70
|
+
// Babel replaces this - runtime fallback for dev
|
|
71
|
+
return StyleSheet.create(styles as any);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extend existing component styles (merged at build time).
|
|
76
|
+
* Import BEFORE components for extensions to apply.
|
|
77
|
+
*/
|
|
78
|
+
export function extendStyle<K extends keyof ComponentStyleRegistry>(
|
|
79
|
+
componentName: K,
|
|
80
|
+
styles: (theme: any) => ExtendStyleDef<K>
|
|
81
|
+
): void;
|
|
82
|
+
export function extendStyle<K extends string>(
|
|
83
|
+
componentName: K,
|
|
84
|
+
styles: (theme: any) => Record<string, any>
|
|
85
|
+
): void;
|
|
86
|
+
export function extendStyle(
|
|
87
|
+
_componentName: string,
|
|
88
|
+
_styles: (theme: any) => any
|
|
89
|
+
): void {
|
|
90
|
+
// Babel removes this call and merges into defineStyle
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Override component styles completely (replaces base at build time).
|
|
95
|
+
* Import BEFORE components for overrides to apply.
|
|
96
|
+
*/
|
|
97
|
+
export function overrideStyle<K extends keyof ComponentStyleRegistry>(
|
|
98
|
+
componentName: K,
|
|
99
|
+
styles: (theme: any) => OverrideStyleDef<K>
|
|
100
|
+
): void;
|
|
101
|
+
export function overrideStyle<K extends string>(
|
|
102
|
+
componentName: K,
|
|
103
|
+
styles: (theme: any) => Record<string, any>
|
|
104
|
+
): void;
|
|
105
|
+
export function overrideStyle(
|
|
106
|
+
_componentName: string,
|
|
107
|
+
_styles: (theme: any) => any
|
|
108
|
+
): void {
|
|
109
|
+
// Babel removes this call and replaces defineStyle
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type { ThemeStyleWrapper } from './extensions';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RegisteredTheme } from './extensions';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* All available breakpoint names.
|
|
5
|
+
* Derived from your registered theme's breakpoints.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // With default theme: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
10
|
+
* const bp: Breakpoint = 'md';
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export type Breakpoint = RegisteredTheme['theme'] extends { breakpoints: infer B }
|
|
14
|
+
? B extends Record<string, number>
|
|
15
|
+
? keyof B & string
|
|
16
|
+
: never
|
|
17
|
+
: never;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the breakpoints record type from the theme.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // { xs: number; sm: number; md: number; lg: number; xl: number; }
|
|
25
|
+
* const bps: BreakpointsRecord = theme.breakpoints;
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export type BreakpointsRecord = RegisteredTheme['theme'] extends { breakpoints: infer B }
|
|
29
|
+
? B
|
|
30
|
+
: Record<string, number>;
|
package/src/theme/extensions.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface DefaultTheme {
|
|
|
15
15
|
surface: Record<string, ColorValue>;
|
|
16
16
|
text: Record<string, ColorValue>;
|
|
17
17
|
border: Record<string, ColorValue>;
|
|
18
|
+
card: Record<string, ColorValue>;
|
|
18
19
|
};
|
|
19
20
|
sizes: {
|
|
20
21
|
button: Record<string, ButtonSizeValue>;
|
|
@@ -42,6 +43,18 @@ export interface DefaultTheme {
|
|
|
42
43
|
typography: Record<Typography, TypographyValue>;
|
|
43
44
|
};
|
|
44
45
|
interaction: InteractionConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Responsive breakpoints for width-based styling.
|
|
48
|
+
* First breakpoint MUST have value 0.
|
|
49
|
+
*/
|
|
50
|
+
breakpoints: Record<string, number>;
|
|
51
|
+
/**
|
|
52
|
+
* Component style extensions.
|
|
53
|
+
* Populated by the extension system when extendComponent is called.
|
|
54
|
+
* Used by the Babel plugin to merge extensions into component styles.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
__extensions?: Record<string, Record<string, any>>;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
/**
|